mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
Merge pull request #455 from OHTU-German-learning-website/Hotfix-tag-replacement-logic
Hotfix for tag replacement logic
This commit is contained in:
@@ -4,38 +4,97 @@ import {
|
|||||||
GlossaryListItem,
|
GlossaryListItem,
|
||||||
} from "@/components/ui/glossary/GlossaryText";
|
} from "@/components/ui/glossary/GlossaryText";
|
||||||
|
|
||||||
export default function RenderHTML({ data }) {
|
// ?NOTE The <container> tag still throws an error, but for now I've chosen not to go down that rabbit hole
|
||||||
if (!data) {
|
// ?It seems slightly too complicated for this last sprint.
|
||||||
return null;
|
|
||||||
|
/**
|
||||||
|
* Normalize HTML attributes for use in React elements.
|
||||||
|
*
|
||||||
|
* Converts common HTML attribute names to their React equivalents (for
|
||||||
|
* example `class` -> `className`) and returns a shallow copy so the
|
||||||
|
* original attribute object is not mutated.
|
||||||
|
*
|
||||||
|
* @param {Object} attributes - Raw attributes from the HTML parser.
|
||||||
|
* @returns {Object} A normalized attributes object safe to spread on React elements.
|
||||||
|
*/
|
||||||
|
const normalizeAttribs = (attributes = {}) => {
|
||||||
|
if (!attributes) return {};
|
||||||
|
const normalized = { ...attributes };
|
||||||
|
// Map common HTML attribute names to React prop names.
|
||||||
|
// Some HTML parsers produce `class`, others produce `classname` (lowercase).
|
||||||
|
// Ensure we normalize both to `className` and remove the original keys so
|
||||||
|
// invalid DOM props (like `classname`) are not spread onto React elements.
|
||||||
|
if (normalized.classname && !normalized.className) {
|
||||||
|
normalized.className = normalized.classname;
|
||||||
|
delete normalized.classname;
|
||||||
}
|
}
|
||||||
|
if (normalized.class && !normalized.className) {
|
||||||
|
normalized.className = normalized.class;
|
||||||
|
delete normalized.class;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
// Parse HTML and replace custom glossary tags with React components
|
/**
|
||||||
const parsedContent = parse(data, {
|
* RenderHTML
|
||||||
replace: (domNode) => {
|
*
|
||||||
// Only process element nodes, not text or comment nodes
|
* Safely parses HTML produced by the Quill editor (or other sources) and
|
||||||
if (domNode.type !== "tag") {
|
* replaces a small set of custom tags with React components or safer
|
||||||
return;
|
* equivalents. Uses `html-react-parser` under the hood and processes
|
||||||
}
|
* nested content via a recursive replacer.
|
||||||
|
*
|
||||||
|
* Supported tag replacements:
|
||||||
|
* - `glossaryparagraph` -> `GlossaryParagraph` (React component)
|
||||||
|
* - `glossarylistitem` -> `GlossaryListItem` (React component)
|
||||||
|
* - `anchor` -> `span.glossary-anchor[data-href=...]` (avoids nested <a> issues)
|
||||||
|
*
|
||||||
|
* @param {{data: string}} props - Component props containing raw HTML string.
|
||||||
|
* @returns {JSX.Element|null} Parsed React nodes wrapped in a `div`, or `null`.
|
||||||
|
*/
|
||||||
|
export default function RenderHTML({ data }) {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
// Handle GlossaryParagraph custom tags
|
// Handlers for specific custom tags. Each handler receives normalized
|
||||||
if (domNode.name === "glossaryparagraph") {
|
// attributes and the already-converted children (as React nodes).
|
||||||
// Extract and convert child nodes to React, preserving HTML structure
|
const handlers = {
|
||||||
const children = domNode.children ? domToReact(domNode.children) : null;
|
glossaryparagraph: (attributes, children) => (
|
||||||
return (
|
<GlossaryParagraph {...attributes}>{children}</GlossaryParagraph>
|
||||||
<GlossaryParagraph {...domNode.attribs}>{children}</GlossaryParagraph>
|
),
|
||||||
);
|
glossarylistitem: (attributes, children) => (
|
||||||
}
|
<GlossaryListItem {...attributes}>{children}</GlossaryListItem>
|
||||||
|
),
|
||||||
// Handle GlossaryListItem custom tags
|
column: (attributes, children) => {
|
||||||
if (domNode.name === "glossarylistitem") {
|
const safeAttribs = { ...attributes };
|
||||||
// Extract and convert child nodes to React, preserving HTML structure
|
safeAttribs.className = [safeAttribs.className, "glossary-column"]
|
||||||
const children = domNode.children ? domToReact(domNode.children) : null;
|
.filter(Boolean)
|
||||||
return (
|
.join(" ");
|
||||||
<GlossaryListItem {...domNode.attribs}>{children}</GlossaryListItem>
|
return <div {...safeAttribs}>{children}</div>; // This replacement should maybe use <Column> if it exists
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
anchor: (attributes, children) => {
|
||||||
|
const safeAttribs = { ...attributes };
|
||||||
|
const href = safeAttribs.href || safeAttribs["data-href"];
|
||||||
|
if (href) {
|
||||||
|
safeAttribs["data-href"] = href;
|
||||||
|
delete safeAttribs.href;
|
||||||
|
}
|
||||||
|
safeAttribs.className = [safeAttribs.className, "glossary-anchor"]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
return <span {...safeAttribs}>{children}</span>; // This replacement logic should maybe use <Anchor> if it exists
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const replacer = (domNode) => {
|
||||||
|
if (domNode.type !== "tag") return;
|
||||||
|
const name = domNode.name;
|
||||||
|
const normalizedAttributes = normalizeAttribs(domNode.attribs || {});
|
||||||
|
const children = domNode.children
|
||||||
|
? domToReact(domNode.children, { replace: replacer })
|
||||||
|
: null;
|
||||||
|
const handler = handlers[name];
|
||||||
|
return handler ? handler(normalizedAttributes, children) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedContent = parse(data, { replace: replacer });
|
||||||
return <div>{parsedContent}</div>;
|
return <div>{parsedContent}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,24 @@ describe("RenderHTML Component", () => {
|
|||||||
expect(code).toBeTruthy();
|
expect(code).toBeTruthy();
|
||||||
expect(code.textContent).toBe("console.log()");
|
expect(code.textContent).toBe("console.log()");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render custom anchor tags as span.glossary-anchor with data-href preserved", () => {
|
||||||
|
const html = '<anchor href="/test">Click me</anchor>';
|
||||||
|
const { container } = render(<RenderHTML data={html} />);
|
||||||
|
const anchorSpan = container.querySelector("span.glossary-anchor");
|
||||||
|
expect(anchorSpan).toBeTruthy();
|
||||||
|
// data-href becomes dataset.href
|
||||||
|
expect(anchorSpan.dataset.href).toBe("/test");
|
||||||
|
expect(anchorSpan.textContent).toBe("Click me");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render custom column tags as div.glossary-column and preserve classes/children", () => {
|
||||||
|
const html = '<column class="chapter">Hello Column</column>';
|
||||||
|
const { container } = render(<RenderHTML data={html} />);
|
||||||
|
const columnDiv = container.querySelector("div.chapter.glossary-column");
|
||||||
|
expect(columnDiv).toBeTruthy();
|
||||||
|
expect(columnDiv.textContent).toContain("Hello Column");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Performance scenarios", () => {
|
describe("Performance scenarios", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user