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.
This commit is contained in:
Amruth Pillai
2026-10-02 01:18:16 +02:00
parent 0a0970160b
commit 581dafdfd9
2 changed files with 57 additions and 3 deletions
+29
View File
@@ -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<string, unknown>; 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(
<Document>
<Page>
<Text style={{ backgroundColor: "#24346E", borderBottomWidth: 1, paddingTop: 4, color: "#ffffff" }}>Bar</Text>
</Page>
</Document>,
);
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");
});
});
+28 -3
View File
@@ -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<string, unknown> = {};
const text: Record<string, unknown> = {};
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);