diff --git a/gradesa/src/components/ui/render-html/render-html.jsx b/gradesa/src/components/ui/render-html/render-html.jsx
index a497113..0307bff 100644
--- a/gradesa/src/components/ui/render-html/render-html.jsx
+++ b/gradesa/src/components/ui/render-html/render-html.jsx
@@ -4,38 +4,97 @@ import {
GlossaryListItem,
} from "@/components/ui/glossary/GlossaryText";
-export default function RenderHTML({ data }) {
- if (!data) {
- return null;
+// ?NOTE The 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 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 (
- {children}
- );
- }
-
- // 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 (
- {children}
- );
- }
+ // Handlers for specific custom tags. Each handler receives normalized
+ // attributes and the already-converted children (as React nodes).
+ const handlers = {
+ glossaryparagraph: (attributes, children) => (
+ {children}
+ ),
+ glossarylistitem: (attributes, children) => (
+ {children}
+ ),
+ column: (attributes, children) => {
+ const safeAttribs = { ...attributes };
+ safeAttribs.className = [safeAttribs.className, "glossary-column"]
+ .filter(Boolean)
+ .join(" ");
+ return {children}
; // This replacement should maybe use 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 {children}; // This replacement logic should maybe use 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 {parsedContent}
;
}
diff --git a/gradesa/src/components/ui/render-html/render-html.test.jsx b/gradesa/src/components/ui/render-html/render-html.test.jsx
index aa55dc9..78e33a3 100644
--- a/gradesa/src/components/ui/render-html/render-html.test.jsx
+++ b/gradesa/src/components/ui/render-html/render-html.test.jsx
@@ -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 = 'Click me';
+ const { container } = render();
+ 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 = 'Hello Column';
+ const { container } = render();
+ const columnDiv = container.querySelector("div.chapter.glossary-column");
+ expect(columnDiv).toBeTruthy();
+ expect(columnDiv.textContent).toContain("Hello Column");
+ });
});
describe("Performance scenarios", () => {