mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-24 08:54:05 +10:00
fix: improper rendering of text blocks in PDFs
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"@reactive-resume/fonts": "workspace:*",
|
||||
"@reactive-resume/schema": "workspace:*",
|
||||
"@reactive-resume/utils": "workspace:*",
|
||||
"node-html-parser": "^7.1.0",
|
||||
"phosphor-icons-react-pdf": "^0.1.3",
|
||||
"react": "^19.2.6",
|
||||
"react-pdf-html": "^2.1.5",
|
||||
@@ -30,7 +31,7 @@
|
||||
"@react-pdf/types": "^2.11.1",
|
||||
"@reactive-resume/config": "workspace:*",
|
||||
"@types/react": "^19.2.14",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260508.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260510.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Node } from "node-html-parser";
|
||||
import { NodeType, parse } from "node-html-parser";
|
||||
|
||||
const inlineTags = new Set([
|
||||
"a",
|
||||
"abbr",
|
||||
"b",
|
||||
"br",
|
||||
"button",
|
||||
"cite",
|
||||
"code",
|
||||
"dfn",
|
||||
"em",
|
||||
"i",
|
||||
"label",
|
||||
"q",
|
||||
"s",
|
||||
"span",
|
||||
"strong",
|
||||
"sub",
|
||||
"sup",
|
||||
"u",
|
||||
]);
|
||||
|
||||
const getTagName = (node: Node) => node.rawTagName.toLowerCase();
|
||||
|
||||
const hasBlockDescendant = (node: Node): boolean =>
|
||||
node.childNodes.some((child) => child.nodeType === NodeType.ELEMENT_NODE && !isInlineNode(child));
|
||||
|
||||
const isInlineNode = (node: Node): boolean => {
|
||||
if (node.nodeType === NodeType.TEXT_NODE || node.nodeType === NodeType.COMMENT_NODE) return true;
|
||||
if (node.nodeType !== NodeType.ELEMENT_NODE) return false;
|
||||
|
||||
return inlineTags.has(getTagName(node)) && !hasBlockDescendant(node);
|
||||
};
|
||||
|
||||
export const normalizeRichTextHtml = (html: string): string => {
|
||||
const root = parse(html.trim(), { comment: false });
|
||||
const normalized: string[] = [];
|
||||
let inlineNodes: string[] = [];
|
||||
|
||||
const flushInlineNodes = () => {
|
||||
const inlineHtml = inlineNodes.join("").trim();
|
||||
|
||||
if (inlineHtml) normalized.push(`<p>${inlineHtml}</p>`);
|
||||
|
||||
inlineNodes = [];
|
||||
};
|
||||
|
||||
for (const node of root.childNodes) {
|
||||
const nodeHtml = node.toString();
|
||||
|
||||
if (isInlineNode(node)) {
|
||||
inlineNodes.push(nodeHtml);
|
||||
continue;
|
||||
}
|
||||
|
||||
flushInlineNodes();
|
||||
normalized.push(nodeHtml);
|
||||
}
|
||||
|
||||
flushInlineNodes();
|
||||
|
||||
return normalized.join("");
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeRichTextHtml } from "./rich-text-html";
|
||||
|
||||
describe("normalizeRichTextHtml", () => {
|
||||
it("wraps top-level inline rich text in a paragraph", () => {
|
||||
const html =
|
||||
"Passionate game developer with 5+ years of professional experience</strong> creating engaging gameplay. <a href='https://www.google.com'>Specialized</a> in Unity.";
|
||||
|
||||
expect(normalizeRichTextHtml(html)).toBe(
|
||||
"<p>Passionate game developer with 5+ years of professional experience creating engaging gameplay. <a href='https://www.google.com'>Specialized</a> in Unity.</p>",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves existing block rich text", () => {
|
||||
expect(normalizeRichTextHtml("<p>Existing paragraph.</p><ul><li><p>Existing item.</p></li></ul>")).toBe(
|
||||
"<p>Existing paragraph.</p><ul><li><p>Existing item.</p></li></ul>",
|
||||
);
|
||||
});
|
||||
|
||||
it("wraps inline runs around top-level blocks", () => {
|
||||
expect(normalizeRichTextHtml("Intro <strong>text</strong><ul><li><p>Item</p></li></ul>Outro")).toBe(
|
||||
"<p>Intro <strong>text</strong></p><ul><li><p>Item</p></li></ul><p>Outro</p>",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { Text as PdfText, View } from "@react-pdf/renderer";
|
||||
import { Html } from "react-pdf-html";
|
||||
import { useTemplateStyle } from "./context";
|
||||
import { safeTextStyle } from "./primitives";
|
||||
import { normalizeRichTextHtml } from "./rich-text-html";
|
||||
import { composeStyles, mergeLinkStyles, mergeStyles } from "./styles";
|
||||
|
||||
const richListItemContentStackStyle = {
|
||||
@@ -17,7 +18,9 @@ export const RichText = ({ children }: { children: string }) => {
|
||||
const richListItemMarkerStyle = useTemplateStyle("richListItemMarker");
|
||||
const richListItemContentStyle = useTemplateStyle("richListItemContent");
|
||||
|
||||
if (!children.trim()) return null;
|
||||
const html = normalizeRichTextHtml(children);
|
||||
|
||||
if (!html) return null;
|
||||
|
||||
return (
|
||||
<Html
|
||||
@@ -54,7 +57,7 @@ export const RichText = ({ children }: { children: string }) => {
|
||||
a: mergeLinkStyles(linkStyle, safeTextStyle),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{html}
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user