fix: improper rendering of text blocks in PDFs

This commit is contained in:
Amruth Pillai
2026-05-10 13:22:21 +02:00
parent 62f4532157
commit 42e83cc676
20 changed files with 974 additions and 844 deletions
+16 -16
View File
@@ -17,10 +17,10 @@
},
"dependencies": {
"@base-ui/react": "^1.4.1",
"@better-auth/api-key": "^1.6.9",
"@better-auth/api-key": "^1.6.10",
"@better-auth/infra": "^0.2.6",
"@better-auth/oauth-provider": "^1.6.9",
"@better-auth/passkey": "^1.6.9",
"@better-auth/oauth-provider": "^1.6.10",
"@better-auth/passkey": "^1.6.10",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -46,30 +46,30 @@
"@reactive-resume/schema": "workspace:*",
"@reactive-resume/ui": "workspace:*",
"@reactive-resume/utils": "workspace:*",
"@tailwindcss/vite": "^4.2.4",
"@tanstack/react-form": "^1.29.1",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-form": "^1.31.0",
"@tanstack/react-hotkeys": "^0.10.0",
"@tanstack/react-query": "^5.100.9",
"@tanstack/react-router": "^1.169.2",
"@tanstack/react-router-ssr-query": "^1.166.12",
"@tanstack/react-start": "^1.167.65",
"@tiptap/extension-color": "^3.22.5",
"@tiptap/extension-highlight": "^3.22.5",
"@tiptap/extension-table": "^3.22.5",
"@tiptap/extension-text-align": "^3.22.5",
"@tiptap/extension-text-style": "^3.22.5",
"@tiptap/pm": "^3.22.5",
"@tiptap/react": "^3.22.5",
"@tiptap/starter-kit": "^3.22.5",
"@tiptap/extension-color": "^3.23.1",
"@tiptap/extension-highlight": "^3.23.1",
"@tiptap/extension-table": "^3.23.1",
"@tiptap/extension-text-align": "^3.23.1",
"@tiptap/extension-text-style": "^3.23.1",
"@tiptap/pm": "^3.23.1",
"@tiptap/react": "^3.23.1",
"@tiptap/starter-kit": "^3.23.1",
"@types/js-cookie": "^3.0.6",
"@uiw/color-convert": "^2.10.1",
"@uiw/react-color-colorful": "^2.10.1",
"better-auth": "1.6.9",
"better-auth": "1.6.10",
"cmdk": "^1.1.1",
"drizzle-orm": "1.0.0-beta.22",
"es-toolkit": "^1.46.1",
"fuse.js": "^7.3.0",
"immer": "^11.1.7",
"immer": "^11.1.8",
"js-cookie": "^3.0.5",
"motion": "^12.38.0",
"pdfjs-dist": "5.7.284",
@@ -97,7 +97,7 @@
"@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-macros": "^3.1.0",
"nitro": "3.0.260429-beta",
+6 -6
View File
@@ -13,7 +13,7 @@
"type": "git",
"url": "https://github.com/amruthpillai/reactive-resume.git"
},
"packageManager": "pnpm@11.0.8+sha512.4c4097e1dd2d42372c4e7fa5a791ff28fc75a484c7ac192e64b1df0fdef17594ba982f9b4fed9adfb3c757846f565b799b2763fb3733d1de1bcb82cf46684912",
"packageManager": "pnpm@11.0.9",
"workspaces": [
"apps/*",
"packages/*"
@@ -36,9 +36,9 @@
"test:agent": "turbo run test:agent"
},
"devDependencies": {
"@biomejs/biome": "^2.4.14",
"@commitlint/cli": "^20.5.3",
"@commitlint/config-conventional": "^20.5.3",
"@biomejs/biome": "^2.4.15",
"@commitlint/cli": "^21.0.0",
"@commitlint/config-conventional": "^21.0.0",
"@reactive-resume/config": "workspace:*",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
@@ -47,10 +47,10 @@
"@types/node": "^25.6.2",
"@vitest/coverage-v8": "^4.1.5",
"jsdom": "^29.1.1",
"knip": "^6.12.1",
"knip": "^6.12.2",
"lefthook": "^2.1.6",
"npm-check-updates": "^22.1.1",
"turbo": "^2.9.10",
"turbo": "^2.9.12",
"typescript": "^6.0.3",
"vitest": "^4.1.5"
}
+2 -2
View File
@@ -23,14 +23,14 @@
"@reactive-resume/utils": "workspace:*",
"deepmerge-ts": "^7.1.5",
"fast-json-patch": "^3.1.1",
"immer": "^11.1.7",
"immer": "^11.1.8",
"jsonrepair": "^3.14.0",
"zod": "^4.4.3",
"zustand": "^5.0.13"
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"typescript": "^6.0.3"
}
}
+4 -4
View File
@@ -19,7 +19,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.76",
"@ai-sdk/google": "^3.0.70",
"@ai-sdk/google": "^3.0.71",
"@ai-sdk/openai": "^3.0.63",
"@ai-sdk/openai-compatible": "^2.0.47",
"@aws-sdk/client-s3": "^3.1045.0",
@@ -33,9 +33,9 @@
"@reactive-resume/schema": "workspace:*",
"@reactive-resume/utils": "workspace:*",
"@tanstack/react-start": "^1.167.65",
"ai": "^6.0.176",
"ai": "^6.0.177",
"bcrypt": "^6.0.0",
"better-auth": "1.6.9",
"better-auth": "1.6.10",
"drizzle-orm": "1.0.0-beta.22",
"drizzle-zod": "1.0.0-beta.14-a36c63d",
"es-toolkit": "^1.46.1",
@@ -49,7 +49,7 @@
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@types/bcrypt": "^6.0.0",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"typescript": "^6.0.3"
}
}
+6 -6
View File
@@ -16,18 +16,18 @@
"test:agent": "vitest run --reporter=agent --reporter=json --outputFile.json=reports/vitest-results.json --passWithNoTests"
},
"dependencies": {
"@better-auth/api-key": "^1.6.9",
"@better-auth/drizzle-adapter": "^1.6.9",
"@better-auth/api-key": "^1.6.10",
"@better-auth/drizzle-adapter": "^1.6.10",
"@better-auth/infra": "^0.2.6",
"@better-auth/oauth-provider": "^1.6.9",
"@better-auth/passkey": "^1.6.9",
"@better-auth/oauth-provider": "^1.6.10",
"@better-auth/passkey": "^1.6.10",
"@reactive-resume/db": "workspace:*",
"@reactive-resume/email": "workspace:*",
"@reactive-resume/env": "workspace:*",
"@reactive-resume/utils": "workspace:*",
"@tanstack/react-start": "^1.167.65",
"bcrypt": "^6.0.0",
"better-auth": "1.6.9",
"better-auth": "1.6.10",
"drizzle-orm": "1.0.0-beta.22",
"jose": "^6.2.3",
"react": "^19.2.6",
@@ -37,7 +37,7 @@
"@reactive-resume/config": "workspace:*",
"@types/bcrypt": "^6.0.0",
"@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"
}
}
+1 -1
View File
@@ -31,7 +31,7 @@
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@types/pg": "^8.20.0",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"drizzle-kit": "1.0.0-beta.22",
"typescript": "^6.0.3"
}
+1 -1
View File
@@ -26,7 +26,7 @@
"@reactive-resume/config": "workspace:*",
"@types/nodemailer": "^8.0.0",
"@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"
}
}
+1 -1
View File
@@ -22,7 +22,7 @@
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@types/node": "^25.6.2",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"typescript": "^6.0.3"
}
}
+1 -1
View File
@@ -16,7 +16,7 @@
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"typescript": "^6.0.3"
}
}
+1 -1
View File
@@ -22,7 +22,7 @@
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"typescript": "^6.0.3"
}
}
+2 -1
View File
@@ -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>
);
};
+1 -1
View File
@@ -22,7 +22,7 @@
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"typescript": "^6.0.3"
}
}
+2 -2
View File
@@ -460,7 +460,7 @@ export const sampleResumeData: ResumeData = {
},
customSections: [
{
title: "Experience",
title: "",
columns: 1,
hidden: false,
id: "019becaf-0b87-769d-98a6-46ccf558c0e8",
@@ -551,7 +551,7 @@ export const sampleResumeData: ResumeData = {
marginX: 16,
marginY: 16,
format: "a4",
locale: "de-DE",
locale: "en-US",
hideIcons: false,
},
design: {
+1 -1
View File
@@ -12,7 +12,7 @@
"@reactive-resume/config": "workspace:*",
"@reactive-resume/env": "workspace:*",
"@types/pg": "^8.20.0",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"drizzle-orm": "1.0.0-beta.22",
"pg": "^8.20.0",
"tsx": "^4.21.0"
+2 -2
View File
@@ -38,8 +38,8 @@
"@tailwindcss/typography": "^0.5.19",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"tailwindcss": "^4.2.4",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3"
}
}
+1 -1
View File
@@ -45,7 +45,7 @@
"devDependencies": {
"@reactive-resume/config": "workspace:*",
"@types/node": "^25.6.2",
"@typescript/native-preview": "7.0.0-dev.20260508.1",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"typescript": "^6.0.3"
}
}
+831 -795
View File
File diff suppressed because it is too large Load Diff