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,
|
||||
} from "@/components/ui/glossary/GlossaryText";
|
||||
|
||||
export default function RenderHTML({ data }) {
|
||||
if (!data) {
|
||||
return null;
|
||||
// ?NOTE The <container> tag still throws an error, but for now I've chosen not to go down that rabbit hole
|
||||
// ?It seems slightly too complicated for this last sprint.
|
||||
|
||||
/**
|
||||
* 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, {
|
||||
replace: (domNode) => {
|
||||
// Only process element nodes, not text or comment nodes
|
||||
if (domNode.type !== "tag") {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* RenderHTML
|
||||
*
|
||||
* Safely parses HTML produced by the Quill editor (or other sources) and
|
||||
* replaces a small set of custom tags with React components or safer
|
||||
* 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
|
||||
if (domNode.name === "glossaryparagraph") {
|
||||
// Extract and convert child nodes to React, preserving HTML structure
|
||||
const children = domNode.children ? domToReact(domNode.children) : null;
|
||||
return (
|
||||
<GlossaryParagraph {...domNode.attribs}>{children}</GlossaryParagraph>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle GlossaryListItem custom tags
|
||||
if (domNode.name === "glossarylistitem") {
|
||||
// Extract and convert child nodes to React, preserving HTML structure
|
||||
const children = domNode.children ? domToReact(domNode.children) : null;
|
||||
return (
|
||||
<GlossaryListItem {...domNode.attribs}>{children}</GlossaryListItem>
|
||||
);
|
||||
}
|
||||
// Handlers for specific custom tags. Each handler receives normalized
|
||||
// attributes and the already-converted children (as React nodes).
|
||||
const handlers = {
|
||||
glossaryparagraph: (attributes, children) => (
|
||||
<GlossaryParagraph {...attributes}>{children}</GlossaryParagraph>
|
||||
),
|
||||
glossarylistitem: (attributes, children) => (
|
||||
<GlossaryListItem {...attributes}>{children}</GlossaryListItem>
|
||||
),
|
||||
column: (attributes, children) => {
|
||||
const safeAttribs = { ...attributes };
|
||||
safeAttribs.className = [safeAttribs.className, "glossary-column"]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -243,6 +243,24 @@ describe("RenderHTML Component", () => {
|
||||
expect(code).toBeTruthy();
|
||||
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", () => {
|
||||
|
||||
Reference in New Issue
Block a user