mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 17:34:52 +10:00
fix: enhance rich text handling in PDF generation
- Bump @tanstack/react-form version to 1.32.0 in package.json. - Refactor rich-input component to simplify highlight configuration. - Improve rich text HTML normalization to handle <mark> elements and apply styles correctly in PDF output. - Update global CSS for WYSIWYG to adjust paragraph and list margins.
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeRichTextHtml } from "./rich-text-html";
|
||||
import { Text as PdfText } from "@react-pdf/renderer";
|
||||
import { renderHtml } from "react-pdf-html";
|
||||
import { normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html";
|
||||
|
||||
type PdfElement = ReactElement<{ children?: unknown; element?: { tag: string } }>;
|
||||
|
||||
const getPdfElementProps = (element: unknown) => (element as PdfElement).props;
|
||||
|
||||
describe("normalizeRichTextHtml", () => {
|
||||
it("wraps loose inline content in a <p>", () => {
|
||||
@@ -31,6 +38,33 @@ describe("normalizeRichTextHtml", () => {
|
||||
expect(normalizeRichTextHtml("<span>x</span>")).toBe("<p><span>x</span></p>");
|
||||
});
|
||||
|
||||
it("maps <mark> to a styled inline span", () => {
|
||||
expect(normalizeRichTextHtml('<mark class="rounded-md">highlighted</mark> text')).toBe(
|
||||
'<p><span class="rounded-md rr-pdf-mark">highlighted</span> text</p>',
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps highlighted text in the same react-pdf-html inline text bucket", () => {
|
||||
const root = renderHtml(normalizeRichTextHtml("before <mark>highlighted</mark> after"), {
|
||||
resetStyles: true,
|
||||
stylesheet: {
|
||||
[`.${richTextMarkClassName}`]: { backgroundColor: "#ffff00" },
|
||||
},
|
||||
});
|
||||
|
||||
const paragraph = getPdfElementProps(root).children;
|
||||
const textBucket = getPdfElementProps(paragraph).children as PdfElement;
|
||||
const textChildren = getPdfElementProps(textBucket).children as unknown[];
|
||||
const highlightedSpan = textChildren[1];
|
||||
|
||||
expect(textBucket.type).toBe(PdfText);
|
||||
expect(textChildren).toHaveLength(3);
|
||||
expect(textChildren[0]).toBe("before ");
|
||||
expect(getPdfElementProps(highlightedSpan).element?.tag).toBe("span");
|
||||
expect(getPdfElementProps(highlightedSpan).children).toBe("highlighted");
|
||||
expect(textChildren[2]).toBe(" after");
|
||||
});
|
||||
|
||||
it("trims input whitespace", () => {
|
||||
expect(normalizeRichTextHtml(" text ")).toBe("<p>text</p>");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Node } from "node-html-parser";
|
||||
import { NodeType, parse } from "node-html-parser";
|
||||
|
||||
export const richTextMarkClassName = "rr-pdf-mark";
|
||||
|
||||
const inlineTags = new Set([
|
||||
"a",
|
||||
"abbr",
|
||||
@@ -27,6 +29,20 @@ const getTagName = (node: Node) => node.rawTagName.toLowerCase();
|
||||
const hasBlockDescendant = (node: Node): boolean =>
|
||||
node.childNodes.some((child) => child.nodeType === NodeType.ELEMENT_NODE && !isInlineNode(child));
|
||||
|
||||
const mergeClassNames = (...classNames: (string | undefined)[]): string =>
|
||||
classNames
|
||||
.flatMap((className) => className?.split(/\s+/) ?? [])
|
||||
.filter(Boolean)
|
||||
.filter((className, index, classNames) => classNames.indexOf(className) === index)
|
||||
.join(" ");
|
||||
|
||||
const normalizeMarkElements = (root: ReturnType<typeof parse>) => {
|
||||
for (const mark of root.querySelectorAll("mark")) {
|
||||
mark.tagName = "span";
|
||||
mark.setAttribute("class", mergeClassNames(mark.getAttribute("class"), richTextMarkClassName));
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -39,6 +55,8 @@ export const normalizeRichTextHtml = (html: string): string => {
|
||||
const normalized: string[] = [];
|
||||
let inlineNodes: string[] = [];
|
||||
|
||||
normalizeMarkElements(root);
|
||||
|
||||
const flushInlineNodes = () => {
|
||||
const inlineHtml = inlineNodes.join("").trim();
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse } from "node-html-parser";
|
||||
import {
|
||||
createRichTextProseSpacing,
|
||||
getRichTextEdgeTrimStyle,
|
||||
isRichTextElementInsideListItem,
|
||||
isRichTextElementInsideOrderedList,
|
||||
resolveRichTextBodyLineHeight,
|
||||
stripRichTextVerticalMargins,
|
||||
} from "./rich-text-spacing";
|
||||
|
||||
const requireElement = <T>(element: T | null | undefined): T => {
|
||||
expect(element).toBeDefined();
|
||||
if (!element) throw new Error("Expected element to exist.");
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
describe("createRichTextProseSpacing", () => {
|
||||
it("uses 0.2x the configured body line height for each paragraph and list-item side margin", () => {
|
||||
const spacing = createRichTextProseSpacing(15);
|
||||
|
||||
expect(spacing.paragraph.marginTop).toBeCloseTo(3);
|
||||
expect(spacing.paragraph.marginBottom).toBeCloseTo(3);
|
||||
expect(spacing.listItem.marginTop).toBeCloseTo(3);
|
||||
expect(spacing.listItem.marginBottom).toBeCloseTo(3);
|
||||
expect((spacing.paragraph.marginBottom as number) + (spacing.paragraph.marginTop as number)).toBeCloseTo(6);
|
||||
});
|
||||
|
||||
it("does not invent spacing when no configured body line height is available", () => {
|
||||
expect(createRichTextProseSpacing(undefined)).toEqual({
|
||||
paragraph: {},
|
||||
listItem: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRichTextBodyLineHeight", () => {
|
||||
it("derives the configured body line height from body font size and line-height multiplier", () => {
|
||||
expect(resolveRichTextBodyLineHeight({ fontSize: 10, lineHeight: 1.5 })).toBe(15);
|
||||
});
|
||||
|
||||
it("uses the last resolved numeric font size and line height from template styles", () => {
|
||||
expect(
|
||||
resolveRichTextBodyLineHeight({ fontSize: 9, lineHeight: 1.2 }, [
|
||||
{ color: "#111" },
|
||||
{ fontSize: 11, lineHeight: 1.5 },
|
||||
]),
|
||||
).toBe(16.5);
|
||||
});
|
||||
|
||||
it("parses px font-size and line-height strings when styles contain them", () => {
|
||||
expect(resolveRichTextBodyLineHeight({ fontSize: "12px", lineHeight: "18px" })).toBe(18);
|
||||
});
|
||||
|
||||
it("returns undefined when configured body line height cannot be resolved", () => {
|
||||
expect(resolveRichTextBodyLineHeight({ fontSize: 10 })).toBeUndefined();
|
||||
expect(resolveRichTextBodyLineHeight({ lineHeight: 1.5 })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRichTextEdgeTrimStyle", () => {
|
||||
it("trims both edges for a single paragraph", () => {
|
||||
const [paragraph] = parse("<p>Only paragraph.</p>").querySelectorAll("p");
|
||||
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(paragraph))).toEqual({ marginTop: 0, marginBottom: 0 });
|
||||
});
|
||||
|
||||
it("trims only the outer edges for multiple paragraphs", () => {
|
||||
const [firstParagraph, secondParagraph] = parse("<p>First.</p><p>Second.</p>").querySelectorAll("p");
|
||||
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(firstParagraph))).toEqual({ marginTop: 0 });
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(secondParagraph))).toEqual({ marginBottom: 0 });
|
||||
});
|
||||
|
||||
it("trims only the outer edges for list items", () => {
|
||||
const [firstItem, secondItem] = parse("<ul><li>First.</li><li>Second.</li></ul>").querySelectorAll("li");
|
||||
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(firstItem))).toEqual({ marginTop: 0 });
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(secondItem))).toEqual({ marginBottom: 0 });
|
||||
});
|
||||
|
||||
it("trims across mixed paragraph and list content", () => {
|
||||
const root = parse("<p>Intro.</p><ul><li><p>Nested item paragraph.</p></li></ul>");
|
||||
const introParagraph = root.querySelector("p");
|
||||
const listItem = root.querySelector("li");
|
||||
const nestedParagraph = root.querySelector("li p");
|
||||
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(introParagraph))).toEqual({ marginTop: 0 });
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(listItem))).toEqual({ marginBottom: 0 });
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(nestedParagraph))).toEqual({});
|
||||
});
|
||||
|
||||
it("normalizes uppercase and mixed-case tags before trimming edges", () => {
|
||||
const [firstParagraph, secondItem] = parse("<P>Intro.</P><UL><LI>Item.</LI></UL>").querySelectorAll("p, li");
|
||||
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(firstParagraph))).toEqual({ marginTop: 0 });
|
||||
expect(getRichTextEdgeTrimStyle(requireElement(secondItem))).toEqual({ marginBottom: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripRichTextVerticalMargins", () => {
|
||||
it("removes vertical margin properties from list content styles and preserves text styles", () => {
|
||||
expect(
|
||||
stripRichTextVerticalMargins({
|
||||
color: "#222",
|
||||
fontSize: 10,
|
||||
margin: 0,
|
||||
marginTop: 1,
|
||||
marginBottom: 2,
|
||||
marginVertical: 3,
|
||||
}),
|
||||
).toEqual({
|
||||
color: "#222",
|
||||
fontSize: 10,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRichTextElementInsideListItem", () => {
|
||||
it("detects paragraphs nested directly inside list items", () => {
|
||||
const paragraph = parse("<ul><li><p>Nested item paragraph.</p></li></ul>").querySelector("p");
|
||||
|
||||
expect(isRichTextElementInsideListItem(requireElement(paragraph))).toBe(true);
|
||||
});
|
||||
|
||||
it("detects paragraphs nested deeper inside list item content", () => {
|
||||
const paragraph = parse("<ul><li><section><p>Nested item paragraph.</p></section></li></ul>").querySelector("p");
|
||||
|
||||
expect(isRichTextElementInsideListItem(requireElement(paragraph))).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes uppercase and mixed-case tags for list item ancestor detection", () => {
|
||||
const paragraph = parse("<UL><LI><Section><P>Nested item paragraph.</P></Section></LI></UL>").querySelector("p");
|
||||
|
||||
expect(isRichTextElementInsideListItem(requireElement(paragraph))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not treat top-level paragraphs as list item content", () => {
|
||||
const paragraph = parse("<p>Top-level paragraph.</p>").querySelector("p");
|
||||
|
||||
expect(isRichTextElementInsideListItem(requireElement(paragraph))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRichTextElementInsideOrderedList", () => {
|
||||
it("normalizes uppercase and mixed-case ordered-list ancestors", () => {
|
||||
const firstItem = parse("<OL><LI>First item.</LI></OL>").querySelector("li");
|
||||
const nestedParagraph = parse("<Ol><Li><Section><P>Nested item paragraph.</P></Section></Li></Ol>").querySelector(
|
||||
"p",
|
||||
);
|
||||
|
||||
expect(isRichTextElementInsideOrderedList(requireElement(firstItem))).toBe(true);
|
||||
expect(isRichTextElementInsideOrderedList(requireElement(nestedParagraph))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not treat unordered-list ancestors as ordered lists", () => {
|
||||
const item = parse("<UL><LI>First item.</LI></UL>").querySelector("li");
|
||||
|
||||
expect(isRichTextElementInsideOrderedList(requireElement(item))).toBe(false);
|
||||
});
|
||||
|
||||
it("uses the nearest list ancestor when ordered and unordered lists are nested", () => {
|
||||
const unorderedNestedInOrdered = parse("<ol><li><ul><li>Nested unordered item.</li></ul></li></ol>").querySelector(
|
||||
"ul li",
|
||||
);
|
||||
const orderedNestedInUnordered = parse("<ul><li><ol><li>Nested ordered item.</li></ol></li></ul>").querySelector(
|
||||
"ol li",
|
||||
);
|
||||
|
||||
expect(isRichTextElementInsideOrderedList(requireElement(unorderedNestedInOrdered))).toBe(false);
|
||||
expect(isRichTextElementInsideOrderedList(requireElement(orderedNestedInUnordered))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import type { Style } from "@react-pdf/types";
|
||||
import type { StyleInput } from "./styles";
|
||||
import { NodeType } from "node-html-parser";
|
||||
import { composeStyles } from "./styles";
|
||||
|
||||
type RichTextSpacingElement = {
|
||||
nodeType: number;
|
||||
tag?: string;
|
||||
localName?: string;
|
||||
rawTagName?: string;
|
||||
tagName?: string;
|
||||
parentNode?: RichTextSpacingElement | null;
|
||||
childNodes?: unknown[];
|
||||
};
|
||||
|
||||
type RichTextProseSpacing = {
|
||||
paragraph: Style;
|
||||
listItem: Style;
|
||||
};
|
||||
|
||||
const parseFiniteNumber = (value: unknown): number | undefined =>
|
||||
typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
|
||||
const parsePxValue = (value: unknown): number | undefined => {
|
||||
if (typeof value !== "string" || !value.endsWith("px")) return undefined;
|
||||
|
||||
const parsedValue = Number.parseFloat(value);
|
||||
return Number.isFinite(parsedValue) ? parsedValue : undefined;
|
||||
};
|
||||
|
||||
const parseFontSize = (fontSize: Style["fontSize"]): number | undefined =>
|
||||
parseFiniteNumber(fontSize) ?? parsePxValue(fontSize);
|
||||
|
||||
const parseLineHeight = (
|
||||
lineHeight: Style["lineHeight"],
|
||||
): { type: "multiplier" | "absolute"; value: number } | undefined => {
|
||||
const multiplier = parseFiniteNumber(lineHeight);
|
||||
if (multiplier !== undefined) return { type: "multiplier", value: multiplier };
|
||||
|
||||
const absoluteLineHeight = parsePxValue(lineHeight);
|
||||
if (absoluteLineHeight !== undefined) return { type: "absolute", value: absoluteLineHeight };
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isElementNode = (node: unknown): node is RichTextSpacingElement =>
|
||||
typeof node === "object" && node !== null && "nodeType" in node && node.nodeType === NodeType.ELEMENT_NODE;
|
||||
|
||||
const readTagName = (
|
||||
element: RichTextSpacingElement,
|
||||
key: "tag" | "localName" | "rawTagName" | "tagName",
|
||||
): string | undefined => {
|
||||
try {
|
||||
const tagName = element[key];
|
||||
|
||||
return typeof tagName === "string" ? tagName : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeTagName = (element: RichTextSpacingElement): string | undefined =>
|
||||
(
|
||||
readTagName(element, "tag") ??
|
||||
readTagName(element, "localName") ??
|
||||
readTagName(element, "rawTagName") ??
|
||||
readTagName(element, "tagName")
|
||||
)?.toLowerCase();
|
||||
|
||||
const isRichTextTag = (element: RichTextSpacingElement, ...tagNames: string[]): boolean => {
|
||||
const normalizedTagName = normalizeTagName(element);
|
||||
|
||||
return normalizedTagName !== undefined && tagNames.includes(normalizedTagName);
|
||||
};
|
||||
|
||||
const getRootElement = (element: RichTextSpacingElement): RichTextSpacingElement => {
|
||||
let root = element;
|
||||
|
||||
while (root.parentNode) {
|
||||
root = root.parentNode;
|
||||
}
|
||||
|
||||
return root;
|
||||
};
|
||||
|
||||
const getTopLevelFlowElements = (root: RichTextSpacingElement): RichTextSpacingElement[] =>
|
||||
(root.childNodes ?? []).filter(isElementNode).flatMap((child) => {
|
||||
if (isRichTextTag(child, "p")) return [child];
|
||||
if (!isRichTextTag(child, "ul", "ol")) return [];
|
||||
|
||||
return (child.childNodes ?? []).filter(isElementNode).filter((listChild) => isRichTextTag(listChild, "li"));
|
||||
});
|
||||
|
||||
export const createRichTextProseSpacing = (bodyLineHeight: number | undefined): RichTextProseSpacing => {
|
||||
if (bodyLineHeight === undefined) return { paragraph: {}, listItem: {} };
|
||||
|
||||
const sideMargin = bodyLineHeight * 0.2;
|
||||
|
||||
return {
|
||||
paragraph: {
|
||||
marginTop: sideMargin,
|
||||
marginBottom: sideMargin,
|
||||
},
|
||||
listItem: {
|
||||
marginTop: sideMargin,
|
||||
marginBottom: sideMargin,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveRichTextBodyLineHeight = (...styles: StyleInput[]): number | undefined => {
|
||||
let bodyFontSize: number | undefined;
|
||||
let bodyLineHeight: ReturnType<typeof parseLineHeight>;
|
||||
|
||||
for (const style of composeStyles(...styles)) {
|
||||
const fontSize = parseFontSize(style.fontSize);
|
||||
if (fontSize !== undefined) bodyFontSize = fontSize;
|
||||
|
||||
const lineHeight = parseLineHeight(style.lineHeight);
|
||||
if (lineHeight !== undefined) bodyLineHeight = lineHeight;
|
||||
}
|
||||
|
||||
if (bodyFontSize === undefined || bodyLineHeight === undefined) return undefined;
|
||||
|
||||
return bodyLineHeight.type === "multiplier" ? bodyFontSize * bodyLineHeight.value : bodyLineHeight.value;
|
||||
};
|
||||
|
||||
export const getRichTextEdgeTrimStyle = (element: RichTextSpacingElement): Style => {
|
||||
const flowElements = getTopLevelFlowElements(getRootElement(element));
|
||||
const flowIndex = flowElements.indexOf(element);
|
||||
|
||||
if (flowIndex === -1) return {};
|
||||
|
||||
return {
|
||||
...(flowIndex === 0 ? { marginTop: 0 } : {}),
|
||||
...(flowIndex === flowElements.length - 1 ? { marginBottom: 0 } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const isRichTextElementInsideListItem = (element: RichTextSpacingElement): boolean => {
|
||||
let current = element.parentNode;
|
||||
|
||||
while (current) {
|
||||
if (isRichTextTag(current, "li")) return true;
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isRichTextElementInsideOrderedList = (element: RichTextSpacingElement): boolean => {
|
||||
let current = element.parentNode;
|
||||
|
||||
while (current) {
|
||||
if (isRichTextTag(current, "ol")) return true;
|
||||
if (isRichTextTag(current, "ul")) return false;
|
||||
current = current.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const stripRichTextVerticalMargins = (style: Style): Style => {
|
||||
const {
|
||||
margin: _margin,
|
||||
marginTop: _marginTop,
|
||||
marginBottom: _marginBottom,
|
||||
marginVertical: _marginVertical,
|
||||
...rest
|
||||
} = style;
|
||||
|
||||
return rest;
|
||||
};
|
||||
@@ -3,13 +3,32 @@ 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 { normalizeRichTextHtml, richTextMarkClassName } from "./rich-text-html";
|
||||
import {
|
||||
createRichTextProseSpacing,
|
||||
getRichTextEdgeTrimStyle,
|
||||
isRichTextElementInsideListItem,
|
||||
isRichTextElementInsideOrderedList,
|
||||
resolveRichTextBodyLineHeight,
|
||||
stripRichTextVerticalMargins,
|
||||
} from "./rich-text-spacing";
|
||||
import { composeStyles, mergeLinkStyles, mergeStyles } from "./styles";
|
||||
|
||||
const richListItemContentStackStyle = {
|
||||
flexDirection: "column",
|
||||
} satisfies Style;
|
||||
|
||||
const richMarkStyle = {
|
||||
backgroundColor: "#ffff00",
|
||||
} satisfies Style;
|
||||
|
||||
const toStyleArray = (style: Style | Style[] | undefined): Style[] => {
|
||||
if (!style) return [];
|
||||
if (Array.isArray(style)) return style.filter(Boolean);
|
||||
|
||||
return [style];
|
||||
};
|
||||
|
||||
export const RichText = ({ children }: { children: string }) => {
|
||||
const boldStyle = useTemplateStyle("bold");
|
||||
const linkStyle = useTemplateStyle("link");
|
||||
@@ -17,6 +36,8 @@ export const RichText = ({ children }: { children: string }) => {
|
||||
const richListItemRowStyle = useTemplateStyle("richListItemRow");
|
||||
const richListItemMarkerStyle = useTemplateStyle("richListItemMarker");
|
||||
const richListItemContentStyle = useTemplateStyle("richListItemContent");
|
||||
const bodyLineHeight = resolveRichTextBodyLineHeight(richParagraphStyle, richListItemContentStyle);
|
||||
const proseSpacing = createRichTextProseSpacing(bodyLineHeight);
|
||||
|
||||
const html = normalizeRichTextHtml(children);
|
||||
|
||||
@@ -27,19 +48,26 @@ export const RichText = ({ children }: { children: string }) => {
|
||||
resetStyles
|
||||
renderers={{
|
||||
b: ({ children }) => <PdfText style={composeStyles(boldStyle, safeTextStyle)}>{children}</PdfText>,
|
||||
p: ({ element, style, children }) => {
|
||||
const paragraphStyles = isRichTextElementInsideListItem(element)
|
||||
? toStyleArray(style).map(stripRichTextVerticalMargins)
|
||||
: style;
|
||||
|
||||
return <View style={composeStyles(paragraphStyles, getRichTextEdgeTrimStyle(element))}>{children}</View>;
|
||||
},
|
||||
li: ({ element, style, children }) => {
|
||||
const list = element.closest("ol, ul");
|
||||
const isOrderedList = list?.rawTagName === "ol" || element.parentNode.tag === "ol";
|
||||
const isOrderedList = isRichTextElementInsideOrderedList(element);
|
||||
const marker = isOrderedList ? `${element.indexOfType + 1}.` : "•";
|
||||
const itemStyle = Array.isArray(style) ? (style as Style[]) : [style as Style | undefined];
|
||||
const itemStyles = toStyleArray(style);
|
||||
const contentItemStyles = itemStyles.map(stripRichTextVerticalMargins);
|
||||
|
||||
return (
|
||||
<View style={composeStyles(richListItemRowStyle)}>
|
||||
<View style={composeStyles(richListItemRowStyle, itemStyles, getRichTextEdgeTrimStyle(element))}>
|
||||
<PdfText style={composeStyles(richListItemMarkerStyle)}>{marker}</PdfText>
|
||||
<View
|
||||
style={composeStyles(
|
||||
richListItemContentStyle,
|
||||
...itemStyle,
|
||||
contentItemStyles,
|
||||
richListItemContentStackStyle,
|
||||
safeTextStyle,
|
||||
)}
|
||||
@@ -53,7 +81,9 @@ export const RichText = ({ children }: { children: string }) => {
|
||||
stylesheet={{
|
||||
b: mergeStyles(boldStyle, safeTextStyle),
|
||||
strong: mergeStyles(boldStyle, safeTextStyle),
|
||||
p: mergeStyles(richParagraphStyle, safeTextStyle),
|
||||
li: mergeStyles(proseSpacing.listItem),
|
||||
[`.${richTextMarkClassName}`]: mergeStyles(richMarkStyle, safeTextStyle),
|
||||
p: mergeStyles(richParagraphStyle, safeTextStyle, proseSpacing.paragraph),
|
||||
a: mergeLinkStyles(linkStyle, safeTextStyle),
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user