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>");
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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 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>) => {
|
||||
for (const listItem of root.querySelectorAll("li")) {
|
||||
const meaningfulChildren = listItem.childNodes.filter(isMeaningfulNode);
|
||||
@@ -135,13 +160,14 @@ export const normalizeRichTextHtml = (
|
||||
const normalized: string[] = [];
|
||||
let inlineNodes: string[] = [];
|
||||
|
||||
normalizeBoldBoundaryWhitespace(root);
|
||||
normalizeMarkElements(root);
|
||||
unwrapSingleParagraphListItems(root);
|
||||
|
||||
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 = [];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user