mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-15 02:53:25 +10:00
fix(pdf): missing spaces around bold rich text in PDFs (#3273)
* fix(pdf): missing spaces around bold rich text in PDFs * fix(pdf): make bold tag matching quote-aware * fix(pdf): preserve quoted bold tag attributes * fix(pdf): handle encoded non-breaking spaces in bold boundaries * fix(pdf): preserve top-level bold boundary spaces --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
co-authored by
Amruth Pillai
parent
7eb6d3bdbf
commit
69961210bd
@@ -17,6 +17,51 @@ describe("normalizeRichTextHtml", () => {
|
|||||||
expect(normalizeRichTextHtml("<strong>bold</strong> text")).toBe("<p><strong>bold</strong> text</p>");
|
expect(normalizeRichTextHtml("<strong>bold</strong> text")).toBe("<p><strong>bold</strong> text</p>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("moves trailing whitespace outside bold tags", () => {
|
||||||
|
expect(normalizeRichTextHtml("<p><strong>Built </strong>and deployed</p>")).toBe(
|
||||||
|
"<p><strong>Built</strong> and deployed</p>",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves leading whitespace outside bold tags", () => {
|
||||||
|
expect(normalizeRichTextHtml("<p>Built<strong> and deployed</strong></p>")).toBe(
|
||||||
|
"<p>Built <strong>and deployed</strong></p>",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves whitespace moved outside top-level bold tags", () => {
|
||||||
|
expect(normalizeRichTextHtml("<strong>Built </strong>")).toBe("<p><strong>Built</strong> </p>");
|
||||||
|
expect(normalizeRichTextHtml("<strong> Built</strong>")).toBe("<p> <strong>Built</strong></p>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([" ", " ", " "])(
|
||||||
|
"moves encoded non-breaking spaces outside bold boundaries: %s",
|
||||||
|
(whitespace) => {
|
||||||
|
expect(normalizeRichTextHtml(`<p>Built<strong>${whitespace}and deployed</strong></p>`)).toBe(
|
||||||
|
`<p>Built${whitespace}<strong>and deployed</strong></p>`,
|
||||||
|
);
|
||||||
|
expect(normalizeRichTextHtml(`<p><strong>Built${whitespace}</strong>and deployed</p>`)).toBe(
|
||||||
|
`<p><strong>Built</strong>${whitespace}and deployed</p>`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("preserves > characters inside quoted bold-tag attributes", () => {
|
||||||
|
expect(normalizeRichTextHtml('<p>Built<strong title="1 > 0"> and deployed</strong></p>')).toBe(
|
||||||
|
'<p>Built <strong title="1 > 0">and deployed</strong></p>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves closing bold tags inside quoted attributes", () => {
|
||||||
|
expect(normalizeRichTextHtml('<p><strong title="Use </strong> here">Built </strong>next</p>')).toBe(
|
||||||
|
'<p><strong title="Use </strong> here">Built</strong> next</p>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves whitespace inside bold text", () => {
|
||||||
|
expect(normalizeRichTextHtml("<p><strong>two words</strong></p>")).toBe("<p><strong>two words</strong></p>");
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves block-level <p> as-is", () => {
|
it("preserves block-level <p> as-is", () => {
|
||||||
expect(normalizeRichTextHtml("<p>Already wrapped</p>")).toBe("<p>Already wrapped</p>");
|
expect(normalizeRichTextHtml("<p>Already wrapped</p>")).toBe("<p>Already wrapped</p>");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,6 +69,31 @@ const isMeaningfulNode = (node: Node): boolean =>
|
|||||||
|
|
||||||
const isElement = (node: Node): node is HTMLElement => node.nodeType === NodeType.ELEMENT_NODE;
|
const isElement = (node: Node): node is HTMLElement => node.nodeType === NodeType.ELEMENT_NODE;
|
||||||
|
|
||||||
|
const LEADING_BOLD_BOUNDARY_WHITESPACE = /^(?:[\u0020\u00a0]| | | )+/i;
|
||||||
|
const TRAILING_BOLD_BOUNDARY_WHITESPACE = /(?:[\u0020\u00a0]| | | )+$/i;
|
||||||
|
|
||||||
|
const normalizeBoldBoundaryWhitespace = (root: ReturnType<typeof parse>) => {
|
||||||
|
for (const bold of root.querySelectorAll("strong,b").reverse()) {
|
||||||
|
const firstChild = bold.childNodes[0];
|
||||||
|
if (firstChild?.nodeType === NodeType.TEXT_NODE) {
|
||||||
|
const whitespace = firstChild.rawText.match(LEADING_BOLD_BOUNDARY_WHITESPACE)?.[0];
|
||||||
|
if (whitespace) {
|
||||||
|
firstChild.rawText = firstChild.rawText.slice(whitespace.length);
|
||||||
|
bold.insertAdjacentHTML("beforebegin", whitespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastChild = bold.childNodes[bold.childNodes.length - 1];
|
||||||
|
if (lastChild?.nodeType === NodeType.TEXT_NODE) {
|
||||||
|
const whitespace = lastChild.rawText.match(TRAILING_BOLD_BOUNDARY_WHITESPACE)?.[0];
|
||||||
|
if (whitespace) {
|
||||||
|
lastChild.rawText = lastChild.rawText.slice(0, -whitespace.length);
|
||||||
|
bold.insertAdjacentHTML("afterend", whitespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const unwrapSingleParagraphListItems = (root: ReturnType<typeof parse>) => {
|
const unwrapSingleParagraphListItems = (root: ReturnType<typeof parse>) => {
|
||||||
for (const listItem of root.querySelectorAll("li")) {
|
for (const listItem of root.querySelectorAll("li")) {
|
||||||
const meaningfulChildren = listItem.childNodes.filter(isMeaningfulNode);
|
const meaningfulChildren = listItem.childNodes.filter(isMeaningfulNode);
|
||||||
@@ -135,13 +160,14 @@ export const normalizeRichTextHtml = (
|
|||||||
const normalized: string[] = [];
|
const normalized: string[] = [];
|
||||||
let inlineNodes: string[] = [];
|
let inlineNodes: string[] = [];
|
||||||
|
|
||||||
|
normalizeBoldBoundaryWhitespace(root);
|
||||||
normalizeMarkElements(root);
|
normalizeMarkElements(root);
|
||||||
unwrapSingleParagraphListItems(root);
|
unwrapSingleParagraphListItems(root);
|
||||||
|
|
||||||
const flushInlineNodes = () => {
|
const flushInlineNodes = () => {
|
||||||
const inlineHtml = inlineNodes.join("").trim();
|
const inlineHtml = inlineNodes.join("");
|
||||||
|
|
||||||
if (inlineHtml) normalized.push(`<p>${inlineHtml}</p>`);
|
if (inlineHtml.trim()) normalized.push(`<p>${inlineHtml}</p>`);
|
||||||
|
|
||||||
inlineNodes = [];
|
inlineNodes = [];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user