From 581dafdfd93970777d83edcbad7ee23f71b93256 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Fri, 2 Oct 2026 01:18:16 +0200 Subject: [PATCH] fix(pdf): paint background, border and padding on block text Forme's Text draws no background, border or padding of its own. A block text carrying any of them is now wrapped in a box that paints them, the way texts inside rows already were. Section headings rendered as filled bars or underlined titles vanished whenever section icons were hidden (Kakuna, Meowth, Rhyhorn, Chikorita, Leafish, Gengar, Ditgar, Glalie and Pikachu lost their heading rules), and text-only contact links lost their box styles. The 1pt gap between a section icon and its title, already requested by the shared heading code, now takes effect too. --- packages/pdf/src/forme/to-forme.test.tsx | 29 ++++++++++++++++++++++ packages/pdf/src/forme/to-forme.tsx | 31 +++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 packages/pdf/src/forme/to-forme.test.tsx diff --git a/packages/pdf/src/forme/to-forme.test.tsx b/packages/pdf/src/forme/to-forme.test.tsx new file mode 100644 index 000000000..26db59f2e --- /dev/null +++ b/packages/pdf/src/forme/to-forme.test.tsx @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { Document, Page, Text } from "./primitives"; +import { renderHostTree } from "./reconciler"; +import { toFormeDocument } from "./to-forme"; + +type SerializedNode = { kind: { type: string }; style: Record; children: SerializedNode[] }; + +describe("toFormeDocument", () => { + // Forme's Text paints no background, border or padding, so a section heading drawn as a filled bar or an underlined + // title vanished whenever its icon (which made it a box) was hidden. + it("puts a text's background, border and padding on a box around it", () => { + const tree = renderHostTree( + + + Bar + + , + ); + const { document } = toFormeDocument(tree) as unknown as { document: { children: SerializedNode[] } }; + const [box] = document.children[0]?.children ?? []; + + expect(box?.kind.type).toBe("View"); + expect(box?.style).toMatchObject({ backgroundColor: expect.anything(), padding: { top: 4 } }); + expect(box?.style).toHaveProperty("borderWidth"); + expect(box?.children[0]?.kind.type).toBe("Text"); + expect(box?.children[0]?.style).not.toHaveProperty("backgroundColor"); + expect(box?.children[0]?.style).toHaveProperty("color"); + }); +}); diff --git a/packages/pdf/src/forme/to-forme.tsx b/packages/pdf/src/forme/to-forme.tsx index 687d91643..49a995ebb 100644 --- a/packages/pdf/src/forme/to-forme.tsx +++ b/packages/pdf/src/forme/to-forme.tsx @@ -307,6 +307,29 @@ const ROW_ITEM_KEYS = new Set([ "breakBefore", ]); +// What Forme's Text ignores: it paints no background, border or padding of its own, so a text that has them sits in +// a box that does. +const TEXT_BOX_KEYS = new Set([ + "backgroundColor", + ...PADDING_KEYS, + "borderWidth", + "borderTopWidth", + "borderRightWidth", + "borderBottomWidth", + "borderLeftWidth", + "borderColor", + "borderTopColor", + "borderRightColor", + "borderBottomColor", + "borderLeftColor", + "borderStyle", + "borderRadius", + "borderTopLeftRadius", + "borderTopRightRadius", + "borderBottomRightRadius", + "borderBottomLeftRadius", +]); + const isEmptyText = (node: HostNode): boolean => "text" in node ? node.text.length === 0 @@ -545,12 +568,14 @@ function convertNode(node: HostNode, parentContext: Context, key: number): React const children = convertChildren(node.children, childContext(context, converted, true)); const textStyle: FormeStyle = { ...context.textDefaults, ...flowStyle(props, style) }; // Forme 0.25 loses the rest of the page (boxes at y -1.8e308) when a row with a text as a direct child breaks - // across pages. The text sits in a box that takes its place in the row. - if (context.rowParent && !context.inText) { + // across pages. The text sits in a box that takes its place in the row; a text with a background, border or + // padding sits in a box that paints them (see `TEXT_BOX_KEYS`). + const paintsBox = Object.keys(textStyle).some((property) => TEXT_BOX_KEYS.has(property)); + if ((context.rowParent || paintsBox) && !context.inText) { const box: Record = {}; const text: Record = {}; for (const [property, value] of Object.entries(textStyle)) - (ROW_ITEM_KEYS.has(property) ? box : text)[property] = value; + (ROW_ITEM_KEYS.has(property) || TEXT_BOX_KEYS.has(property) ? box : text)[property] = value; const inner = createElement(FormeText, { style: text as FormeStyle, ...(href ? { href } : {}) }, ...children); tagNode(inner, props, context); const wrapper = createElement(FormeView, { key, style: box as FormeStyle }, inner);