render-tag 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"render-tag.umd.js","names":[],"sources":["../src/dom.ts","../src/parse.ts","../src/css-resolver.ts","../src/layout.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["/**\n * DOM parser resolution — the only place render-tag looks for a DOM.\n *\n * Resolution order:\n * 1. A parser injected via setDOMParser() (explicit always wins).\n * 2. The ambient global DOMParser (browsers, jsdom/happy-dom environments).\n * 3. Throw with guidance — render-tag has zero dependencies, so in Node the\n * consumer must inject a parser (e.g. linkedom's or jsdom's DOMParser).\n */\n\nexport interface DOMParserLike {\n /**\n * Must behave like the standard DOMParser for 'text/html' input. The return\n * type is intentionally loose so non-browser DOM libraries (linkedom,\n * jsdom) type-check without casts — their Document types are structurally\n * different from the TS lib's.\n */\n parseFromString(markup: string, type: string): unknown;\n}\n\nlet explicitParser: DOMParserLike | null = null;\nlet ambientParser: DOMParserLike | null = null;\n\n/**\n * Inject the DOM parser render-tag uses to parse HTML input.\n * Required in non-browser environments (Node.js). Pass null to reset.\n *\n * import { DOMParser } from 'linkedom';\n * setDOMParser(new DOMParser());\n */\nexport function setDOMParser(parser: DOMParserLike | null): void {\n explicitParser = parser;\n // Reset the ambient cache too, so tests/harnesses that tear down a DOM\n // polyfill (jsdom etc.) don't keep measuring against a stale realm.\n if (parser === null) ambientParser = null;\n}\n\n/**\n * Create a measurement 2D context from whatever canvas source the environment\n * offers. `preferDocument` preserves each entry point's historical source\n * (block layout: document canvas; path: OffscreenCanvas) so existing pixel\n * baselines don't move.\n */\nexport function createFallbackMeasureCtx(preferDocument: boolean): CanvasRenderingContext2D {\n const hasDocument = typeof document !== 'undefined';\n const hasOffscreen = typeof OffscreenCanvas !== 'undefined';\n if (hasDocument && (preferDocument || !hasOffscreen)) {\n return document.createElement('canvas').getContext('2d')! as CanvasRenderingContext2D;\n }\n if (hasOffscreen) {\n return new OffscreenCanvas(1, 1).getContext('2d')! as unknown as CanvasRenderingContext2D;\n }\n throw new Error(\n 'render-tag: no canvas available for text measurement. ' +\n 'In a non-browser environment, pass config.ctx (a 2D context).'\n );\n}\n\n/** @internal */\nexport function resolveDOMParser(): DOMParserLike {\n if (explicitParser) return explicitParser;\n if (typeof DOMParser !== 'undefined') {\n // DOMParser instances are stateless — cache one.\n if (!ambientParser) ambientParser = new DOMParser();\n return ambientParser;\n }\n throw new Error(\n 'render-tag: no DOM parser available. In a non-browser environment, ' +\n 'inject one via setDOMParser(new DOMParser()) using a DOM library ' +\n 'such as linkedom or jsdom.'\n );\n}\n","import { resolveDOMParser } from './dom.js';\n\n/**\n * Parse HTML string and extract inline <style> blocks.\n * Returns the content element and combined CSS text.\n */\nexport function parseHTML(html: string): { fragment: DocumentFragment; css: string } {\n const parser = resolveDOMParser();\n // Wrap in a full document: browsers do this implicitly for fragments, but\n // non-browser parsers (linkedom) need the body to exist explicitly.\n const doc = parser.parseFromString(\n `<!DOCTYPE html><html><head></head><body>${html}</body></html>`,\n 'text/html'\n ) as Document;\n\n // Extract all <style> tag contents\n const styleTags = doc.querySelectorAll('style');\n let css = '';\n for (const tag of styleTags) {\n css += tag.textContent + '\\n';\n tag.remove();\n }\n\n // Merge adjacent text nodes. Browsers already parse `big&nbsp;text` into a\n // single text node, but linkedom emits a node per entity boundary — which\n // would let the tokenizer break lines at entity seams. Normalizing in both\n // environments keeps parsing parity by construction. Scoped to body (all\n // content lives there); parseHTML can run in per-pixel fit loops.\n doc.body.normalize();\n\n // Move body children into a fragment owned by the same document — no\n // adoption needed (and non-browser DOMs may not implement adoptNode).\n const fragment = doc.createDocumentFragment();\n while (doc.body.firstChild) {\n fragment.appendChild(doc.body.firstChild);\n }\n\n return { fragment, css };\n}\n","import type { DecorationEntry, ResolvedStyle, StyledNode } from './types.js';\n\n// Node.TEXT_NODE / Node.ELEMENT_NODE without the ambient `Node` global\n// (unavailable in non-browser environments).\nconst ELEMENT_NODE = 1;\nconst TEXT_NODE = 3;\n\n// ─── CSS Parser ──────────────────────────────────────────────────────\n\ninterface CSSDeclaration {\n property: string;\n value: string;\n}\n\ninterface CSSRule {\n selectors: string[];\n declarations: CSSDeclaration[];\n}\n\n/**\n * Parse a simple CSS string into rules.\n * Supports: tag, .class, parent > child, comma-separated selectors.\n * Extracts @font-face rules separately for injection into the document.\n */\nfunction parseCSS(css: string): { rules: CSSRule[]; fontFaceRules: string[] } {\n const rules: CSSRule[] = [];\n const fontFaceRules: string[] = [];\n // Remove comments\n css = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n\n let i = 0;\n while (i < css.length) {\n // Skip whitespace\n while (i < css.length && /\\s/.test(css[i])) i++;\n if (i >= css.length) break;\n\n // Handle at-rules (@font-face, @media, etc.)\n if (css[i] === '@') {\n const atStart = i;\n let braceDepth = 0;\n while (i < css.length) {\n if (css[i] === '{') braceDepth++;\n if (css[i] === '}') {\n braceDepth--;\n if (braceDepth <= 0) { i++; break; }\n }\n i++;\n }\n // Capture @font-face rules for injection\n const atRule = css.slice(atStart, i);\n if (atRule.startsWith('@font-face')) {\n fontFaceRules.push(atRule);\n }\n continue;\n }\n\n // Read selector(s) up to '{'\n const selectorStart = i;\n while (i < css.length && css[i] !== '{') i++;\n if (i >= css.length) break;\n const selectorStr = css.slice(selectorStart, i).trim();\n i++; // skip '{'\n\n // Read declarations up to '}'\n const declStart = i;\n while (i < css.length && css[i] !== '}') i++;\n const declStr = css.slice(declStart, i).trim();\n i++; // skip '}'\n\n if (!selectorStr) continue;\n\n // Parse selectors (comma-separated)\n const selectors = selectorStr.split(',').map(s => s.trim()).filter(Boolean);\n\n // Parse declarations\n const declarations: CSSDeclaration[] = [];\n for (const decl of declStr.split(';')) {\n const colonIdx = decl.indexOf(':');\n if (colonIdx === -1) continue;\n const property = decl.slice(0, colonIdx).trim().toLowerCase();\n const value = decl.slice(colonIdx + 1).trim();\n if (property && value) {\n declarations.push({ property, value });\n }\n }\n\n if (selectors.length > 0 && declarations.length > 0) {\n rules.push({ selectors, declarations });\n }\n }\n\n return { rules, fontFaceRules };\n}\n\n// ─── Selector Matching ───────────────────────────────────────────────\n\ninterface ElementContext {\n tagName: string;\n classes: Set<string>;\n parent: ElementContext | null;\n el: Element;\n}\n\n/**\n * Compute specificity for a simple selector.\n * Returns [ids, classes, tags] tuple.\n */\nfunction selectorSpecificity(selector: string): [number, number, number] {\n // Remove pseudo-elements for specificity calculation\n const sel = selector.replace(/::[\\w-]+/g, '');\n const parts = sel.split(/\\s*>\\s*|\\s+/);\n let ids = 0, classes = 0, tags = 0;\n for (const part of parts) {\n // Count #id\n const idMatches = part.match(/#[\\w-]+/g);\n if (idMatches) ids += idMatches.length;\n // Count .class\n const classMatches = part.match(/\\.[\\w-]+/g);\n if (classMatches) classes += classMatches.length;\n // Count tag (strip classes/ids/pseudo)\n const tagPart = part.replace(/[#.][\\w-]+/g, '').replace(/:[\\w-]+/g, '').trim();\n if (tagPart && tagPart !== '*') tags++;\n }\n return [ids, classes, tags];\n}\n\n/** Parsed representation of a simple selector part (e.g. \"ul.foo\") */\ninterface ParsedPart {\n tag: string; // '' if no tag, or the tag name\n classes: string[]; // class names without the dot\n}\n\nfunction parsePart(part: string): ParsedPart {\n const classMatches = part.match(/\\.[\\w-]+/g) || [];\n const tag = part.replace(/\\.[\\w-]+/g, '').replace(/:[\\w-]+/g, '').trim();\n return {\n tag: (tag && tag !== '*') ? tag : '',\n classes: classMatches.map(c => c.slice(1)),\n };\n}\n\n/**\n * Check if a parsed selector part matches an element context.\n */\nfunction matchesPart(part: string, ctx: ElementContext): boolean {\n const classMatches = part.match(/\\.[\\w-]+/g) || [];\n const tag = part.replace(/\\.[\\w-]+/g, '').replace(/:[\\w-]+/g, '').trim();\n\n if (tag && tag !== '*' && tag !== ctx.tagName) return false;\n for (const cls of classMatches) {\n if (!ctx.classes.has(cls.slice(1))) return false;\n }\n return true;\n}\n\nfunction matchesParsedPart(part: ParsedPart, ctx: ElementContext): boolean {\n if (part.tag && part.tag !== ctx.tagName) return false;\n for (const cls of part.classes) {\n if (!ctx.classes.has(cls)) return false;\n }\n return true;\n}\n\n/** Pre-parsed selector ready for fast matching */\ninterface ParsedSelector {\n /** Parsed parts, rightmost first (match order) */\n parts: ParsedPart[];\n /** Combinators between parts[i] and parts[i+1]: '>' or ' ' */\n combinators: string[];\n /** Rightmost part — used for index lookup */\n rightmost: ParsedPart;\n /** Whether the rightmost part is 'html' or 'body' (matches root) */\n rightmostIsRoot: boolean;\n /** Original specificity */\n spec: [number, number, number];\n /**\n * Pseudo-element on the rightmost compound. Currently we only honor\n * `::marker` — its declarations apply to the marker box of `<li>`,\n * not to the element body.\n */\n pseudoElement?: 'marker';\n}\n\n/**\n * Pre-parse and tokenize a selector string into a ParsedSelector.\n * Returns null for selectors we can't handle (pseudo-classes; pseudo-elements\n * other than `::marker`).\n */\nfunction parseSelector(selector: string): ParsedSelector | null {\n // Detect `::marker` on the rightmost compound. Other pseudo-elements\n // (::before, ::after, ::first-line, …) are still rejected.\n let pseudoElement: 'marker' | undefined;\n if (selector.includes('::')) {\n // Allow only a single trailing ::marker on the rightmost compound.\n // Anything else is unsupported.\n const otherPseudo = selector.replace(/::marker\\b/g, '');\n if (otherPseudo.includes('::')) return null;\n if (/::marker\\b/.test(selector)) {\n pseudoElement = 'marker';\n // Bare `::marker` (start of input or after whitespace/`>`) → `*`\n // so descendant/child combinators are preserved.\n selector = selector.replace(/(^|[\\s>])::marker\\b/g, '$1*');\n // `::marker` glued to a tag/class compound → strip the pseudo only.\n selector = selector.replace(/::marker\\b/g, '');\n } else {\n return null;\n }\n }\n if (/:(?:nth-|hover|focus|active|visited|first-child|last-child)/.test(selector)) return null;\n\n const tokens: string[] = [];\n const combinators: string[] = [];\n\n const raw = selector.trim().split(/\\s+/);\n for (let i = 0; i < raw.length; i++) {\n if (raw[i] === '>') {\n combinators.push('>');\n } else {\n if (tokens.length > combinators.length + 1) {\n combinators.push(' ');\n }\n tokens.push(raw[i]);\n }\n }\n while (combinators.length < tokens.length - 1) {\n combinators.push(' ');\n }\n\n if (tokens.length === 0) return null;\n\n const parts = tokens.map(parsePart);\n const rightmost = parts[parts.length - 1];\n const rightmostTag = rightmost.tag;\n\n return {\n parts,\n combinators,\n rightmost,\n rightmostIsRoot: rightmostTag === 'html' || rightmostTag === 'body',\n spec: selectorSpecificity(selector),\n pseudoElement,\n };\n}\n\n/**\n * Match a pre-parsed selector against an element context.\n */\nfunction matchesParsedSelector(sel: ParsedSelector, ctx: ElementContext): boolean {\n // Quick check: rightmost part must match current element\n if (sel.rightmostIsRoot) {\n if (ctx.parent !== null) return false; // html/body only match root\n } else {\n if (!matchesParsedPart(sel.rightmost, ctx)) return false;\n }\n\n // Single-part selector — already matched\n if (sel.parts.length === 1) return true;\n\n // Walk ancestors for remaining parts (right-to-left)\n let current: ElementContext | null = ctx.parent;\n for (let ti = sel.parts.length - 2; ti >= 0; ti--) {\n if (!current) return false;\n const part = sel.parts[ti];\n const combinator = sel.combinators[ti];\n\n if (combinator === '>') {\n // Direct child: current ancestor must match\n const isRoot = current.parent === null;\n if (isRoot && (part.tag === 'html' || part.tag === 'body')) {\n current = current.parent;\n } else if (matchesParsedPart(part, current)) {\n current = current.parent;\n } else {\n return false;\n }\n } else {\n // Descendant: any ancestor must match\n let found = false;\n while (current) {\n const isRoot = current.parent === null;\n if (isRoot && (part.tag === 'html' || part.tag === 'body')) {\n current = current.parent;\n found = true;\n break;\n }\n if (matchesParsedPart(part, current)) {\n current = current.parent;\n found = true;\n break;\n }\n current = current.parent;\n }\n if (!found) return false;\n }\n }\n\n return true;\n}\n\n// ─── Style Resolution ────────────────────────────────────────────────\n\n/** Default values for all ResolvedStyle properties */\nfunction defaultStyle(): ResolvedStyle {\n return {\n // Browsers default unstyled text to the UA serif font (Times). Match it so\n // HTML without an explicit font-family wraps/positions like the browser.\n fontFamily: 'serif',\n fontSize: 16,\n fontWeight: 400,\n fontStyle: 'normal',\n fontVariantCaps: 'normal',\n color: 'rgb(0, 0, 0)',\n textAlign: 'start',\n textAlignLast: 'auto',\n textIndent: 0,\n textTransform: 'none',\n textDecorationLine: 'none',\n textDecorationStyle: 'solid',\n textDecorationColor: 'rgb(0, 0, 0)',\n textDecorations: [],\n textUnderlineOffset: null,\n textDecorationThickness: null,\n textShadow: 'none',\n webkitTextStrokeWidth: 0,\n webkitTextStrokeColor: '',\n webkitTextStrokeImage: 'none',\n webkitTextFillColor: '',\n paintOrder: 'normal',\n strokeLinejoin: 'round',\n webkitBackgroundClip: '',\n backgroundImage: 'none',\n letterSpacing: 0,\n wordSpacing: 0,\n fontKerning: 'auto',\n lineHeight: 0,\n verticalAlign: 'baseline',\n whiteSpace: 'normal',\n wordBreak: 'normal',\n overflowWrap: 'normal',\n unicodeBidi: 'normal',\n direction: 'ltr',\n display: 'block',\n width: 0,\n minHeight: 0,\n paddingTop: 0,\n paddingRight: 0,\n paddingBottom: 0,\n paddingLeft: 0,\n marginTop: 0,\n marginRight: 0,\n marginBottom: 0,\n marginLeft: 0,\n backgroundColor: 'rgba(0, 0, 0, 0)',\n borderTopWidth: 0,\n borderTopColor: 'rgb(0, 0, 0)',\n borderTopStyle: 'none',\n borderRightWidth: 0,\n borderRightColor: 'rgb(0, 0, 0)',\n borderRightStyle: 'none',\n borderBottomWidth: 0,\n borderBottomColor: 'rgb(0, 0, 0)',\n borderBottomStyle: 'none',\n borderLeftWidth: 0,\n borderLeftColor: 'rgb(0, 0, 0)',\n borderLeftStyle: 'none',\n flexDirection: 'row',\n gap: 0,\n flexGrow: 0,\n listStyleType: 'disc',\n lineClamp: 0,\n };\n}\n\n/** Tag-level default display values and browser default margins */\nconst TAG_DEFAULTS: Record<string, Partial<ResolvedStyle>> = {\n span: { display: 'inline' },\n a: { display: 'inline' },\n strong: { display: 'inline', fontWeight: 700 },\n b: { display: 'inline', fontWeight: 700 },\n em: { display: 'inline', fontStyle: 'italic' },\n i: { display: 'inline', fontStyle: 'italic' },\n u: { display: 'inline', textDecorationLine: 'underline' },\n s: { display: 'inline', textDecorationLine: 'line-through' },\n strike: { display: 'inline', textDecorationLine: 'line-through' },\n del: { display: 'inline', textDecorationLine: 'line-through' },\n sub: { display: 'inline', verticalAlign: 'sub', fontSize: 0.83 },\n sup: { display: 'inline', verticalAlign: 'super', fontSize: 0.83 },\n code: { display: 'inline', fontFamily: 'monospace' },\n cite: { display: 'inline', fontStyle: 'italic' },\n bdo: { display: 'inline', unicodeBidi: 'bidi-override' },\n bdi: { display: 'inline', unicodeBidi: 'isolate' },\n p: { display: 'block', marginTop: -1, marginBottom: -1 }, // -1 = 1em, resolved later\n div: { display: 'block' },\n h1: { display: 'block', fontSize: 2, fontWeight: 700, marginTop: -0.67, marginBottom: -0.67 },\n h2: { display: 'block', fontSize: 1.5, fontWeight: 700, marginTop: -0.83, marginBottom: -0.83 },\n h3: { display: 'block', fontSize: 1.17, fontWeight: 700, marginTop: -1, marginBottom: -1 },\n h4: { display: 'block', fontSize: 1, fontWeight: 700, marginTop: -1.33, marginBottom: -1.33 },\n h5: { display: 'block', fontSize: 0.83, fontWeight: 700, marginTop: -1.67, marginBottom: -1.67 },\n h6: { display: 'block', fontSize: 0.67, fontWeight: 700, marginTop: -2.33, marginBottom: -2.33 },\n ul: { display: 'block', listStyleType: 'disc', marginTop: -1, marginBottom: -1 },\n ol: { display: 'block', listStyleType: 'decimal', marginTop: -1, marginBottom: -1 },\n li: { display: 'list-item' },\n blockquote: { display: 'block', marginTop: -1, marginBottom: -1, marginLeft: 40, marginRight: 40 },\n pre: { display: 'block', whiteSpace: 'pre', fontFamily: 'monospace', marginTop: -1, marginBottom: -1 },\n table: { display: 'table' },\n tr: { display: 'table-row' },\n td: { display: 'table-cell' },\n th: { display: 'table-cell', fontWeight: 700 },\n br: { display: 'inline' },\n hr: {\n display: 'block',\n borderTopWidth: 1,\n borderTopStyle: 'solid',\n borderTopColor: 'gray',\n marginTop: -0.5,\n marginBottom: -0.5,\n },\n};\n\n/**\n * Parse a CSS value to pixels given a parent font size for em/% resolution.\n */\nfunction parseValue(value: string, parentFontSize: number, containerWidth: number): number {\n if (!value || value === 'normal' || value === 'auto' || value === 'none') return 0;\n const trimmed = value.trim();\n\n if (trimmed.endsWith('em')) {\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : num * parentFontSize;\n }\n if (trimmed.endsWith('%')) {\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : (num / 100) * containerWidth;\n }\n if (trimmed.endsWith('px')) {\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : num;\n }\n // Bare number (for line-height, etc.)\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : num;\n}\n\nfunction parseFontWeight(value: string): number {\n if (value === 'bold') return 700;\n if (value === 'normal') return 400;\n const num = parseInt(value, 10);\n return isNaN(num) ? 400 : num;\n}\n\n/**\n * Resolve `paint-order` to whether stroke is painted before fill.\n * Per CSS spec, missing tokens append in order: fill, stroke, markers.\n * So `stroke` alone implies `stroke fill markers` (stroke first).\n */\nexport function paintOrderHasStrokeFirst(paintOrder: string): boolean {\n const v = paintOrder.trim().toLowerCase();\n if (!v || v === 'normal') return false;\n const tokens = v.split(/\\s+/).filter(t => t === 'fill' || t === 'stroke');\n const strokeIdx = tokens.indexOf('stroke');\n const fillIdx = tokens.indexOf('fill');\n if (strokeIdx === -1) return false;\n if (fillIdx === -1) return true;\n return strokeIdx < fillIdx;\n}\n\n/** Split on whitespace, but only at paren-depth 0 — keeps `rgb(1, 2, 3)` intact. */\nfunction splitTopLevelWhitespace(value: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let cur = '';\n for (let i = 0; i < value.length; i++) {\n const ch = value[i];\n if (ch === '(') { depth++; cur += ch; }\n else if (ch === ')') { depth = Math.max(0, depth - 1); cur += ch; }\n else if (depth === 0 && /\\s/.test(ch)) {\n if (cur) { parts.push(cur); cur = ''; }\n } else cur += ch;\n }\n if (cur) parts.push(cur);\n return parts;\n}\n\n/**\n * Expand shorthand properties into individual ones.\n * E.g., margin: 10px 20px → marginTop/Right/Bottom/Left\n */\nexport function expandShorthand(property: string, value: string): CSSDeclaration[] {\n if (property === 'margin' || property === 'padding') {\n const parts = value.trim().split(/\\s+/);\n let top: string, right: string, bottom: string, left: string;\n if (parts.length === 1) {\n top = right = bottom = left = parts[0];\n } else if (parts.length === 2) {\n top = bottom = parts[0];\n right = left = parts[1];\n } else if (parts.length === 3) {\n top = parts[0]; right = left = parts[1]; bottom = parts[2];\n } else {\n top = parts[0]; right = parts[1]; bottom = parts[2]; left = parts[3];\n }\n return [\n { property: `${property}-top`, value: top },\n { property: `${property}-right`, value: right },\n { property: `${property}-bottom`, value: bottom },\n { property: `${property}-left`, value: left },\n ];\n }\n\n if (property === 'border' || property === 'border-top' || property === 'border-right' ||\n property === 'border-bottom' || property === 'border-left') {\n const parts = value.trim().split(/\\s+/);\n const borderStyles = ['solid', 'dashed', 'dotted', 'double', 'none', 'hidden'];\n const width = parts.find(p => p.endsWith('px') || /^\\d/.test(p)) || '0';\n const style = parts.find(p => borderStyles.includes(p)) || 'none';\n const color = parts.find(p => !p.endsWith('px') && !/^\\d/.test(p) && !borderStyles.includes(p)) || 'currentColor';\n const result: CSSDeclaration[] = [];\n const sides = property === 'border'\n ? ['top', 'right', 'bottom', 'left']\n : [property.replace('border-', '')];\n for (const side of sides) {\n result.push({ property: `border-${side}-width`, value: width });\n result.push({ property: `border-${side}-style`, value: style });\n result.push({ property: `border-${side}-color`, value: color });\n }\n return result;\n }\n\n if (property === 'list-style') {\n // list-style: none → list-style-type: none\n if (value === 'none') {\n return [{ property: 'list-style-type', value: 'none' }];\n }\n return [{ property: 'list-style-type', value }];\n }\n\n if (property === 'text-decoration') {\n const v = value.trim();\n // Thickness is a longhand of this shorthand (css-text-decor-4): the\n // shorthand resets it to `auto` when no thickness token is present. The\n // reset is emitted FIRST so an explicit token parsed below overrides it.\n if (v === 'inherit' || v === 'none') {\n return [\n { property: 'text-decoration-line', value: 'none' },\n { property: 'text-decoration-thickness', value: 'auto' },\n ];\n }\n // Extract color functions (rgb(...), hsl(...)) before splitting on whitespace,\n // because they contain spaces internally (e.g. \"rgb(231, 76, 60)\").\n let colorValue = '';\n const withoutColorFn = v.replace(/\\b(rgba?\\([^)]*\\)|hsla?\\([^)]*\\))/i, (match) => {\n colorValue = match;\n return '';\n });\n const parts = withoutColorFn.split(/\\s+/).filter(Boolean);\n const lineValues = ['underline', 'overline', 'line-through'];\n const styleValues = ['solid', 'double', 'dotted', 'dashed', 'wavy'];\n const result: CSSDeclaration[] = [\n { property: 'text-decoration-thickness', value: 'auto' },\n ];\n const lines: string[] = [];\n for (const p of parts) {\n if (lineValues.includes(p)) lines.push(p);\n else if (styleValues.includes(p)) result.push({ property: 'text-decoration-style', value: p });\n // Thickness values (2px, .5em, 10%) and its keywords go to the longhand.\n else if (/^[\\d.+-]/.test(p) || p === 'auto' || p === 'from-font')\n result.push({ property: 'text-decoration-thickness', value: p });\n // Remaining tokens are the color (#hex or named).\n else result.push({ property: 'text-decoration-color', value: p });\n }\n if (colorValue) result.push({ property: 'text-decoration-color', value: colorValue });\n if (lines.length > 0) result.unshift({ property: 'text-decoration-line', value: lines.join(' ') });\n return result;\n }\n\n if (property === '-webkit-text-stroke') {\n // -webkit-text-stroke: 1px #1e40af → width + color\n // Split on whitespace at paren-depth 0 so colors with internal spaces\n // (rgb(255, 255, 255), var(--c, ...), color(srgb 1 0 0), …) survive intact.\n const parts = splitTopLevelWhitespace(value.trim());\n const width = parts.find(p => p.endsWith('px') || /^\\d/.test(p)) || '0';\n const color = parts.find(p => !p.endsWith('px') && !/^\\d/.test(p)) || 'currentColor';\n return [\n { property: '-webkit-text-stroke-width', value: width },\n { property: '-webkit-text-stroke-color', value: color },\n ];\n }\n\n if (property === 'flex') {\n // flex: 1 → flex-grow: 1\n const parts = value.trim().split(/\\s+/);\n const grow = parseFloat(parts[0]);\n if (!isNaN(grow)) {\n return [{ property: 'flex-grow', value: String(grow) }];\n }\n return [];\n }\n\n if (property === 'border-collapse' || property === 'border-spacing') {\n // Ignored — table-specific properties we don't handle\n return [];\n }\n\n return [{ property, value }];\n}\n\n/** Normalize the (case-insensitive) currentColor keyword to '', the canonical unset value. */\nfunction normalizeCurrentColor(value: string): string {\n const v = value.trim();\n return v.toLowerCase() === 'currentcolor' ? '' : v;\n}\n\n/**\n * Apply a CSS declaration to a ResolvedStyle, resolving units.\n */\nfunction applyDeclaration(\n style: ResolvedStyle,\n property: string,\n value: string,\n parentFontSize: number,\n containerWidth: number,\n direction: string,\n): void {\n // Resolve the font-size first if that's what we're setting, since\n // em values for other properties depend on the element's own font-size\n const fontSize = style.fontSize || parentFontSize;\n\n switch (property) {\n // Font & text\n case 'font-family': style.fontFamily = value.trim(); break;\n case 'font-size': {\n const v = value.trim();\n if (v.endsWith('em')) {\n style.fontSize = parseFloat(v) * parentFontSize;\n } else if (v.endsWith('%')) {\n style.fontSize = (parseFloat(v) / 100) * parentFontSize;\n } else {\n style.fontSize = parseFloat(v) || parentFontSize;\n }\n break;\n }\n case 'font-weight': style.fontWeight = parseFontWeight(value); break;\n case 'font-style': style.fontStyle = value.trim(); break;\n // Canvas only renders the `small-caps` variant; map anything containing it\n // (incl. the font-variant shorthand) to small-caps, else normal.\n case 'font-variant':\n case 'font-variant-caps':\n style.fontVariantCaps = /\\bsmall-caps\\b/.test(value) ? 'small-caps' : 'normal';\n break;\n case 'color': style.color = value.trim(); break;\n case 'text-align': style.textAlign = value.trim(); break;\n case 'text-align-last': style.textAlignLast = value.trim(); break;\n case 'text-indent':\n style.textIndent = parseValue(value, fontSize, containerWidth); break;\n case 'text-transform': style.textTransform = value.trim(); break;\n case 'text-decoration-line': style.textDecorationLine = value.trim(); break;\n // text-decoration is expanded in expandShorthand, should not reach here\n // but handle just in case\n case 'text-decoration': break;\n case 'text-decoration-style': style.textDecorationStyle = value.trim(); break;\n case 'text-decoration-color': style.textDecorationColor = value.trim(); break;\n case 'text-underline-offset': {\n // px value or null for `auto`; the `_underlineOffsetPct` shadow lets\n // inheritFrom re-resolve a % per child (see the field doc in types.ts).\n // `= undefined` rather than `delete`: same semantics for the only\n // consumer (`!== undefined`), keeps the object's hidden class.\n const v = value.trim();\n if (v === 'auto') {\n (style as any)._underlineOffsetPct = undefined;\n style.textUnderlineOffset = null;\n } else if (isNaN(parseFloat(v))) {\n // Invalid declaration — ignored, like the browser (parseValue would\n // coerce it to 0 and pin the band at the baseline).\n } else if (v.endsWith('%')) {\n const num = parseFloat(v);\n style.textUnderlineOffset = (num / 100) * fontSize;\n (style as any)._underlineOffsetPct = num;\n } else {\n (style as any)._underlineOffsetPct = undefined;\n style.textUnderlineOffset = parseValue(v, fontSize, containerWidth);\n }\n break;\n }\n case 'text-decoration-thickness': {\n // px value or null for `auto`/`from-font` (see the field doc in\n // types.ts). A % resolves against the element's own font size.\n const v = value.trim();\n if (v === 'auto' || v === 'from-font') {\n style.textDecorationThickness = null;\n } else if (isNaN(parseFloat(v))) {\n // Invalid declaration — ignored, like the browser (parseValue would\n // coerce it to 0 and hide the band).\n } else if (v.endsWith('%')) {\n style.textDecorationThickness = (parseFloat(v) / 100) * fontSize;\n } else {\n style.textDecorationThickness = parseValue(v, fontSize, containerWidth);\n }\n break;\n }\n case 'text-shadow': style.textShadow = value.trim(); break;\n case '-webkit-text-stroke-width': style.webkitTextStrokeWidth = parseValue(value, fontSize, containerWidth); break;\n // '' is the canonical currentColor for these two: it must survive\n // inheritance as a keyword and resolve against each element's own\n // color at render time, so it is never eagerly resolved here.\n case '-webkit-text-stroke-color': style.webkitTextStrokeColor = normalizeCurrentColor(value); break;\n // A CSS custom property (not a real -webkit- property) so the browser keeps\n // it in the element's inline cssText — an unknown real property would be\n // dropped before render-tag reads it.\n case '--rt-text-stroke-image': style.webkitTextStrokeImage = value.trim(); break;\n case '-webkit-text-fill-color': style.webkitTextFillColor = normalizeCurrentColor(value); break;\n case 'paint-order': style.paintOrder = value.trim(); break;\n case 'stroke-linejoin': style.strokeLinejoin = value.trim(); break;\n case '-webkit-background-clip':\n case 'background-clip': style.webkitBackgroundClip = value.trim(); break;\n case 'background-image': style.backgroundImage = value.trim(); break;\n case 'letter-spacing':\n style.letterSpacing = value.trim() === 'normal' ? 0 : parseValue(value, fontSize, containerWidth); break;\n case 'word-spacing':\n style.wordSpacing = value.trim() === 'normal' ? 0 : parseValue(value, fontSize, containerWidth); break;\n case 'font-kerning': style.fontKerning = value.trim(); break;\n case 'line-height': {\n const v = value.trim();\n if (v === 'normal') {\n style.lineHeight = 0; // 0 signals \"normal\"\n } else if (v.endsWith('px')) {\n style.lineHeight = parseFloat(v) || 0;\n } else if (v.endsWith('em')) {\n style.lineHeight = parseFloat(v) * fontSize;\n } else if (v.endsWith('%')) {\n // Percentage — computed against the element's own font size and\n // inherited as that computed value (no multiplier for children),\n // same as the em branch. Without this branch \"120%\" used to fall\n // into the unitless path as parseFloat(\"120%\") = 120, producing a\n // 120x line height.\n const num = parseFloat(v);\n if (!isNaN(num)) {\n style.lineHeight = (num / 100) * fontSize;\n }\n } else {\n // Unitless multiplier — compute for this element's font size\n // and mark as unitless so children re-compute\n const num = parseFloat(v);\n if (!isNaN(num)) {\n style.lineHeight = num * fontSize;\n (style as any)._lineHeightMultiplier = num;\n }\n }\n break;\n }\n case '-webkit-line-clamp':\n case 'line-clamp': {\n // Spec accepts `none` / `auto` / positive integer. We map both\n // `none` and `auto` to 0 (no clamp); a non-positive integer also\n // means no clamp. Otherwise store the integer.\n const v = value.trim().toLowerCase();\n if (v === 'none' || v === 'auto') {\n style.lineClamp = 0;\n } else {\n const n = parseInt(v, 10);\n style.lineClamp = Number.isFinite(n) && n > 0 ? n : 0;\n }\n break;\n }\n case 'vertical-align': style.verticalAlign = value.trim(); break;\n case 'white-space': style.whiteSpace = value.trim(); break;\n case 'word-break': style.wordBreak = value.trim(); break;\n case 'overflow-wrap':\n case 'word-wrap': style.overflowWrap = value.trim(); break;\n case 'direction': style.direction = value.trim(); break;\n case 'unicode-bidi': style.unicodeBidi = value.trim(); break;\n\n // Box model\n case 'display': style.display = value.trim(); break;\n case 'width': {\n const v = value.trim();\n if (v === '100%') style.width = containerWidth;\n else if (v !== 'auto') style.width = parseValue(v, fontSize, containerWidth);\n break;\n }\n case 'min-height': style.minHeight = parseValue(value, fontSize, containerWidth); break;\n case 'padding-top': style.paddingTop = parseValue(value, fontSize, containerWidth); break;\n case 'padding-right': style.paddingRight = parseValue(value, fontSize, containerWidth); break;\n case 'padding-bottom': style.paddingBottom = parseValue(value, fontSize, containerWidth); break;\n case 'padding-left': style.paddingLeft = parseValue(value, fontSize, containerWidth); break;\n case 'margin-top': style.marginTop = parseValue(value, fontSize, containerWidth); break;\n case 'margin-right': style.marginRight = parseValue(value, fontSize, containerWidth); break;\n case 'margin-bottom': style.marginBottom = parseValue(value, fontSize, containerWidth); break;\n case 'margin-left': style.marginLeft = parseValue(value, fontSize, containerWidth); break;\n case 'background-color': style.backgroundColor = value.trim(); break;\n case 'background': {\n const v = value.trim();\n if (v.includes('gradient(')) {\n // background: linear-gradient(...) → backgroundImage\n style.backgroundImage = v;\n } else if (v.startsWith('#') || v.startsWith('rgb') || v.startsWith('hsl') ||\n ['transparent', 'none', 'inherit'].includes(v) ||\n /^[a-z]+$/.test(v)) {\n style.backgroundColor = v;\n }\n break;\n }\n\n // Logical properties → physical (based on direction)\n case 'padding-inline-start':\n if (direction === 'rtl') style.paddingRight = parseValue(value, fontSize, containerWidth);\n else style.paddingLeft = parseValue(value, fontSize, containerWidth);\n break;\n case 'padding-inline-end':\n if (direction === 'rtl') style.paddingLeft = parseValue(value, fontSize, containerWidth);\n else style.paddingRight = parseValue(value, fontSize, containerWidth);\n break;\n case 'margin-inline-start':\n if (direction === 'rtl') style.marginRight = parseValue(value, fontSize, containerWidth);\n else style.marginLeft = parseValue(value, fontSize, containerWidth);\n break;\n case 'margin-inline-end':\n if (direction === 'rtl') style.marginLeft = parseValue(value, fontSize, containerWidth);\n else style.marginRight = parseValue(value, fontSize, containerWidth);\n break;\n\n // Border\n case 'border-top-width': style.borderTopWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-top-color': style.borderTopColor = value.trim(); break;\n case 'border-top-style': style.borderTopStyle = value.trim(); break;\n case 'border-right-width': style.borderRightWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-right-color': style.borderRightColor = value.trim(); break;\n case 'border-right-style': style.borderRightStyle = value.trim(); break;\n case 'border-bottom-width': style.borderBottomWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-bottom-color': style.borderBottomColor = value.trim(); break;\n case 'border-bottom-style': style.borderBottomStyle = value.trim(); break;\n case 'border-left-width': style.borderLeftWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-left-color': style.borderLeftColor = value.trim(); break;\n case 'border-left-style': style.borderLeftStyle = value.trim(); break;\n\n // Flex\n case 'flex-direction': style.flexDirection = value.trim(); break;\n case 'gap': style.gap = parseValue(value, fontSize, containerWidth); break;\n case 'flex-grow': style.flexGrow = parseFloat(value) || 0; break;\n\n // List\n case 'list-style-type': style.listStyleType = value.trim(); break;\n\n // Ignored properties (not relevant for our layout)\n case 'position':\n case 'top':\n case 'left':\n case 'right':\n case 'bottom':\n case 'inset-inline-start':\n case 'inset-inline-end':\n case 'content':\n case 'counter-reset':\n case 'counter-increment':\n case 'border-radius':\n case 'border-top-left-radius':\n case 'border-top-right-radius':\n case 'border-bottom-left-radius':\n case 'border-bottom-right-radius':\n case 'cursor':\n case 'opacity':\n case 'overflow':\n case 'box-sizing':\n case 'outline':\n case 'transition':\n case 'transform':\n case 'font-stretch':\n case 'font-display':\n case 'src':\n case 'unicode-range':\n break;\n }\n}\n\n/** Inheritable property names (CSS kebab-case) mapped to ResolvedStyle keys */\nconst INHERITABLE_KEYS: [string, keyof ResolvedStyle][] = [\n ['font-family', 'fontFamily'],\n ['font-size', 'fontSize'],\n ['font-weight', 'fontWeight'],\n ['font-style', 'fontStyle'],\n ['color', 'color'],\n ['text-align', 'textAlign'],\n ['text-align-last', 'textAlignLast'],\n ['text-indent', 'textIndent'],\n ['text-transform', 'textTransform'],\n ['white-space', 'whiteSpace'],\n ['word-break', 'wordBreak'],\n ['overflow-wrap', 'overflowWrap'],\n ['direction', 'direction'],\n ['letter-spacing', 'letterSpacing'],\n ['word-spacing', 'wordSpacing'],\n ['line-height', 'lineHeight'],\n ['text-shadow', 'textShadow'],\n ['font-kerning', 'fontKerning'],\n ['list-style-type', 'listStyleType'],\n ['vertical-align', 'verticalAlign'],\n ['text-underline-offset', 'textUnderlineOffset'],\n ['paint-order', 'paintOrder'],\n ['stroke-linejoin', 'strokeLinejoin'],\n ['-webkit-text-stroke-width', 'webkitTextStrokeWidth'],\n ['-webkit-text-stroke-color', 'webkitTextStrokeColor'],\n ['-webkit-text-fill-color', 'webkitTextFillColor'],\n];\n\n/**\n * Inherit properties from parent style to child style for properties\n * not explicitly set (tracked via setProps).\n */\nfunction inheritFrom(child: ResolvedStyle, parent: ResolvedStyle, setProps: Set<string>): void {\n for (const [cssProp, key] of INHERITABLE_KEYS) {\n if (!setProps.has(cssProp)) {\n if (key === 'lineHeight') {\n // Unitless line-height: re-compute relative to child's font-size\n const multiplier = (parent as any)._lineHeightMultiplier;\n if (multiplier !== undefined) {\n child.lineHeight = multiplier * child.fontSize;\n (child as any)._lineHeightMultiplier = multiplier;\n } else {\n child.lineHeight = parent.lineHeight;\n }\n } else if (key === 'textUnderlineOffset') {\n // Percentage offset: re-resolve against the child's own font size\n // (Chrome-measured), same pattern as the line-height multiplier.\n const pct = (parent as any)._underlineOffsetPct;\n if (pct !== undefined) {\n child.textUnderlineOffset = (pct / 100) * child.fontSize;\n (child as any)._underlineOffsetPct = pct;\n } else {\n child.textUnderlineOffset = parent.textUnderlineOffset;\n }\n } else {\n (child as any)[key] = (parent as any)[key];\n }\n }\n }\n}\n\n// ─── Main resolver ───────────────────────────────────────────────────\n\ninterface MatchedDeclaration {\n property: string;\n value: string;\n specificity: [number, number, number];\n order: number;\n important: boolean;\n}\n\n/** A pre-processed rule entry with parsed selector and pre-expanded declarations */\ninterface ProcessedRule {\n selector: ParsedSelector;\n declarations: { property: string; value: string; important: boolean }[];\n /** Global order for cascade sorting */\n orderBase: number;\n}\n\n/**\n * Build an index of processed rules keyed by rightmost tag name and class names.\n * The '*' key holds rules that match any element (no tag or class constraint).\n */\nfunction buildRuleIndex(rules: CSSRule[]): {\n byTag: Map<string, ProcessedRule[]>;\n byClass: Map<string, ProcessedRule[]>;\n universal: ProcessedRule[];\n} {\n const byTag = new Map<string, ProcessedRule[]>();\n const byClass = new Map<string, ProcessedRule[]>();\n const universal: ProcessedRule[] = [];\n let orderBase = 0;\n\n for (const rule of rules) {\n // Pre-expand declarations once\n const expandedDecls: { property: string; value: string; important: boolean }[] = [];\n for (const decl of rule.declarations) {\n const isImportant = decl.value.includes('!important');\n const cleanValue = isImportant\n ? decl.value.replace(/\\s*!important\\s*/g, '').trim()\n : decl.value;\n const expanded = expandShorthand(decl.property, cleanValue);\n for (const exp of expanded) {\n expandedDecls.push({ property: exp.property, value: exp.value, important: isImportant });\n }\n }\n\n for (const sel of rule.selectors) {\n const parsed = parseSelector(sel);\n if (!parsed) continue;\n\n const entry: ProcessedRule = {\n selector: parsed,\n declarations: expandedDecls,\n orderBase: orderBase++,\n };\n\n const rm = parsed.rightmost;\n if (rm.tag && !parsed.rightmostIsRoot) {\n // Index by tag\n const list = byTag.get(rm.tag);\n if (list) list.push(entry);\n else byTag.set(rm.tag, [entry]);\n }\n if (rm.classes.length > 0) {\n // Index by first class (most selective)\n const cls = rm.classes[0];\n const list = byClass.get(cls);\n if (list) list.push(entry);\n else byClass.set(cls, [entry]);\n }\n if (!rm.tag && rm.classes.length === 0) {\n // Universal selector or html/body root\n universal.push(entry);\n }\n // Also add root-matching selectors to universal\n if (parsed.rightmostIsRoot) {\n universal.push(entry);\n }\n }\n }\n\n return { byTag, byClass, universal };\n}\n\n/** Format an integer using a CSS list-style-type. */\nfunction formatListMarker(n: number, type: string): string {\n switch (type) {\n case 'disc': return '•';\n case 'circle': return '○';\n case 'square': return '■';\n case 'none': return '';\n case 'decimal-leading-zero':\n return `${n < 10 && n >= 0 ? '0' + n : n}.`;\n case 'lower-roman': return `${toRoman(n).toLowerCase()}.`;\n case 'upper-roman': return `${toRoman(n)}.`;\n case 'lower-alpha':\n case 'lower-latin': return `${toAlpha(n).toLowerCase()}.`;\n case 'upper-alpha':\n case 'upper-latin': return `${toAlpha(n)}.`;\n case 'decimal':\n default:\n return `${n}.`;\n }\n}\n\nfunction toRoman(n: number): string {\n if (n < 1 || n > 3999) return `${n}`;\n const map: [number, string][] = [\n [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],\n [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],\n [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'],\n ];\n let out = '';\n for (const [v, s] of map) {\n while (n >= v) { out += s; n -= v; }\n }\n return out;\n}\n\nfunction toAlpha(n: number): string {\n if (n < 1) return `${n}`;\n let out = '';\n while (n > 0) {\n const r = (n - 1) % 26;\n out = String.fromCharCode(65 + r) + out;\n n = Math.floor((n - 1) / 26);\n }\n return out;\n}\n\n/**\n * Detect list marker text for a <li> element based on tree position,\n * honoring list-style-type, <ol start>, <ol reversed>, and <li value>.\n */\nfunction getListMarker(el: Element, listStyleType: string): string | undefined {\n const tag = el.tagName.toLowerCase();\n if (tag !== 'li') return undefined;\n if (listStyleType === 'none') return '';\n\n const parent = el.parentElement;\n const parentTag = parent?.tagName.toLowerCase();\n\n // Bullet markers: independent of position.\n if (listStyleType === 'disc' || listStyleType === 'circle' || listStyleType === 'square') {\n return formatListMarker(0, listStyleType);\n }\n\n // Numbered markers: compute index from siblings + ol attributes + li value.\n if (parentTag === 'ol' || parentTag === 'ul' || !parent) {\n const liItems = parent\n ? Array.from(parent.children).filter(c => c.tagName.toLowerCase() === 'li')\n : [el];\n const startAttr = parent?.getAttribute('start');\n const reversed = parent?.hasAttribute('reversed') ?? false;\n const start = startAttr ? parseInt(startAttr, 10) : (reversed ? liItems.length : 1);\n const step = reversed ? -1 : 1;\n let n = start;\n for (const item of liItems) {\n const valueAttr = item.getAttribute('value');\n if (valueAttr) {\n const v = parseInt(valueAttr, 10);\n if (!Number.isNaN(v)) n = v;\n }\n if (item === el) return formatListMarker(n, listStyleType || 'decimal');\n n += step;\n }\n return formatListMarker(n, listStyleType || 'decimal');\n }\n\n return undefined;\n}\n\n/**\n * Inline-style access without `instanceof HTMLElement` — duck-typed so nodes\n * from non-browser DOMs (linkedom, jsdom) qualify without their constructors\n * being installed as globals.\n */\nfunction inlineStyleOf(el: Element): CSSStyleDeclaration | null {\n const style = (el as HTMLElement).style;\n return style && typeof style.cssText === 'string' ? style : null;\n}\n\n/**\n * Parse inline style attribute into declarations.\n */\nfunction parseInlineStyle(styleAttr: string): CSSDeclaration[] {\n const declarations: CSSDeclaration[] = [];\n for (const decl of styleAttr.split(';')) {\n const colonIdx = decl.indexOf(':');\n if (colonIdx === -1) continue;\n const property = decl.slice(0, colonIdx).trim().toLowerCase();\n const value = decl.slice(colonIdx + 1).trim();\n if (property && value) {\n declarations.push({ property, value });\n }\n }\n return declarations;\n}\n\n/**\n * Resolve styles for a DOM tree without inserting into the document.\n * Parses CSS rules, matches selectors, resolves cascade + inheritance.\n */\nexport function resolveStylesFromCSS(\n fragment: DocumentFragment,\n css: string,\n containerWidth: number,\n): { tree: StyledNode; cleanup: () => void } {\n const { rules, fontFaceRules } = parseCSS(css);\n\n // Inject @font-face rules into the live document so fonts can load.\n // Browser-only side effect: in non-browser environments there is no font\n // loader to trigger, so skip silently (fonts come from the consumer there).\n let fontStyleEl: HTMLStyleElement | null = null;\n if (fontFaceRules.length > 0 && typeof document !== 'undefined' && document.head) {\n fontStyleEl = document.createElement('style');\n fontStyleEl.textContent = fontFaceRules.join('\\n');\n document.head.appendChild(fontStyleEl);\n }\n\n // Build indexed rule lookup\n const ruleIndex = buildRuleIndex(rules);\n\n // Wrap fragment in a container div so resolveElement has a single root\n // Element. Created from the fragment's own document so no ambient DOM is\n // required (the tree is never inserted into the live document).\n const container = fragment.ownerDocument!.createElement('div');\n container.appendChild(fragment);\n\n function buildContext(el: Element, parent: ElementContext | null): ElementContext {\n const classes = new Set<string>();\n const className = el.getAttribute('class');\n if (className) {\n for (const c of className.split(/\\s+/)) {\n if (c) classes.add(c);\n }\n }\n return {\n tagName: el.tagName.toLowerCase(),\n classes,\n parent,\n el,\n };\n }\n\n function resolveElement(\n el: Element,\n parentStyle: ResolvedStyle,\n parentCtx: ElementContext | null,\n ): StyledNode {\n const tag = el.tagName.toLowerCase();\n const ctx = buildContext(el, parentCtx);\n\n // Start with defaults\n const style = defaultStyle();\n\n // Track which properties are explicitly set (tag defaults, CSS rules, inline styles)\n const setProps = new Set<string>();\n\n // --- Step 1: Determine font-size first (needed for em/multiplier resolution) ---\n\n // Collect candidate rules from index (only rules that could match this element)\n const candidates: ProcessedRule[] = [];\n const seen = new Set<ProcessedRule>();\n\n const tagRules = ruleIndex.byTag.get(tag);\n if (tagRules) for (const r of tagRules) { seen.add(r); candidates.push(r); }\n\n for (const cls of ctx.classes) {\n const clsRules = ruleIndex.byClass.get(cls);\n if (clsRules) for (const r of clsRules) {\n if (!seen.has(r)) { seen.add(r); candidates.push(r); }\n }\n }\n\n for (const r of ruleIndex.universal) {\n if (!seen.has(r)) { seen.add(r); candidates.push(r); }\n }\n\n // Match candidates and collect pre-expanded declarations.\n // Rules with `::marker` are routed to a separate list and applied to the\n // <li>'s markerStyle later — they do not affect the element body.\n const matched: MatchedDeclaration[] = [];\n const matchedMarker: MatchedDeclaration[] = [];\n for (const candidate of candidates) {\n if (matchesParsedSelector(candidate.selector, ctx)) {\n const target = candidate.selector.pseudoElement === 'marker' ? matchedMarker : matched;\n for (const decl of candidate.declarations) {\n target.push({\n property: decl.property,\n value: decl.value,\n specificity: candidate.selector.spec,\n order: candidate.orderBase,\n important: decl.important,\n });\n }\n }\n }\n\n // Tag default font-size\n const tagDef = TAG_DEFAULTS[tag];\n let fontSizeSet = false;\n if (tagDef?.fontSize !== undefined) {\n const val = tagDef.fontSize as number;\n if (val < 10) {\n style.fontSize = val * parentStyle.fontSize;\n } else {\n style.fontSize = val;\n }\n fontSizeSet = true;\n setProps.add('font-size');\n }\n\n // Sort by: !important first, then specificity, then source order\n if (matched.length > 1) {\n matched.sort((a, b) => {\n if (a.important !== b.important) return a.important ? 1 : -1;\n const sa = a.specificity, sb = b.specificity;\n if (sa[0] !== sb[0]) return sa[0] - sb[0];\n if (sa[1] !== sb[1]) return sa[1] - sb[1];\n if (sa[2] !== sb[2]) return sa[2] - sb[2];\n return a.order - b.order;\n });\n }\n\n // Apply font-size from CSS rules\n for (const m of matched) {\n if (m.property === 'font-size') {\n applyDeclaration(style, m.property, m.value, parentStyle.fontSize, containerWidth, parentStyle.direction);\n fontSizeSet = true;\n }\n }\n\n // Apply font-size from inline styles\n const elStyle = inlineStyleOf(el);\n if (elStyle && elStyle.cssText) {\n const inlineDecls = parseInlineStyle(elStyle.cssText);\n for (const decl of inlineDecls) {\n if (decl.property === 'font-size') {\n applyDeclaration(style, decl.property, decl.value, parentStyle.fontSize, containerWidth, parentStyle.direction);\n fontSizeSet = true;\n }\n }\n }\n\n // Inherit font-size from parent if not set\n if (!fontSizeSet) {\n style.fontSize = parentStyle.fontSize;\n }\n\n // Now style.fontSize is the element's computed font-size\n const elemFontSize = style.fontSize;\n\n // --- Step 2: Apply all other properties using resolved font-size ---\n\n // Apply non-fontSize tag defaults\n if (tagDef) {\n for (const [key, val] of Object.entries(tagDef)) {\n if (key === 'fontSize') continue; // already handled\n (style as any)[key] = val;\n const cssKey = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());\n setProps.add(cssKey);\n }\n\n // Resolve negative margin values (em multipliers from tag defaults)\n if (style.marginTop < 0) style.marginTop = Math.abs(style.marginTop) * elemFontSize;\n if (style.marginBottom < 0) style.marginBottom = Math.abs(style.marginBottom) * elemFontSize;\n\n // Default padding-inline-start for lists (direction-aware)\n if (tag === 'ul' || tag === 'ol') {\n const dir = parentStyle.direction;\n if (dir === 'rtl') {\n style.paddingRight = 40;\n setProps.add('padding-right');\n } else {\n style.paddingLeft = 40;\n setProps.add('padding-left');\n }\n }\n }\n\n // Determine direction from parent for logical property resolution\n const direction = parentStyle.direction;\n\n // Property aliases: CSS name → canonical name for setProps tracking\n const PROP_ALIASES: Record<string, string> = {\n 'word-wrap': 'overflow-wrap',\n };\n\n // Apply matched CSS declarations (skip font-size, already applied)\n for (const m of matched) {\n if (m.property === 'font-size') continue;\n applyDeclaration(style, m.property, m.value, elemFontSize, containerWidth, direction);\n setProps.add(PROP_ALIASES[m.property] || m.property);\n }\n\n // Apply inline styles (highest specificity, skip font-size)\n const hasInlineWidth = !!elStyle?.width;\n if (elStyle && elStyle.cssText) {\n const inlineDecls = parseInlineStyle(elStyle.cssText);\n for (const decl of inlineDecls) {\n if (decl.property === 'font-size') {\n setProps.add('font-size');\n continue;\n }\n const expanded = expandShorthand(decl.property, decl.value);\n for (const exp of expanded) {\n applyDeclaration(style, exp.property, exp.value, elemFontSize, containerWidth, direction);\n setProps.add(PROP_ALIASES[exp.property] || exp.property);\n }\n }\n }\n\n // Only keep explicit width from inline styles (match DOM resolver behavior)\n if (!hasInlineWidth) {\n style.width = 0;\n }\n\n // Handle `dir` attribute\n const dirAttr = el.getAttribute('dir');\n if (dirAttr) {\n style.direction = dirAttr;\n setProps.add('direction');\n }\n\n // Inherit from parent for properties not explicitly set\n setProps.add('font-size'); // already resolved\n inheritFrom(style, parentStyle, setProps);\n\n // Auto-set currentColor defaults (browser default behavior).\n // Decorations: with no explicit text-decoration-color, Chrome paints the\n // line with -webkit-text-fill-color when that is set (measured: red color +\n // blue fill-color + <u> → blue underline; transparent fill-color → the\n // decoration disappears with the glyphs), falling back to `color`.\n if (!setProps.has('text-decoration-color')) {\n style.textDecorationColor = style.webkitTextFillColor || style.color;\n } else if (style.textDecorationColor === 'currentColor') {\n style.textDecorationColor = style.color;\n }\n for (const side of ['Top', 'Right', 'Bottom', 'Left'] as const) {\n const colorKey = `border${side}Color` as keyof ResolvedStyle;\n const propName = `border-${side.toLowerCase()}-color`;\n if (!setProps.has(propName)) {\n (style as any)[colorKey] = style.color;\n } else if ((style as any)[colorKey] === 'currentColor') {\n (style as any)[colorKey] = style.color;\n }\n }\n\n // Handle text-decoration inheritance (propagates visually, not via normal\n // inheritance). Each decoration keeps the color/style of the element that\n // DECLARED it (Chrome: a parent's red underline stays red across a blue\n // child <s>): ancestor entries ride along in `textDecorations`, own\n // entries are appended after them so they paint on top.\n // `textDecorationLine` stays the union of lines for cheap checks.\n const ownEntries: DecorationEntry[] = [];\n if (style.textDecorationLine && style.textDecorationLine !== 'none') {\n for (const d of style.textDecorationLine.split(/\\s+/)) {\n if (d && d !== 'none') {\n ownEntries.push({\n line: d,\n color: style.textDecorationColor,\n style: style.textDecorationStyle,\n // This element is the decorating box for every descendant the\n // entry rides down to.\n declarer: style,\n });\n }\n }\n }\n style.textDecorations = parentStyle.textDecorations.length\n ? [...parentStyle.textDecorations, ...ownEntries]\n : ownEntries;\n const decoSet = new Set(style.textDecorationLine.split(/\\s+/).filter(d => d && d !== 'none'));\n if (parentStyle.textDecorationLine && parentStyle.textDecorationLine !== 'none') {\n for (const d of parentStyle.textDecorationLine.split(/\\s+/)) {\n if (d && d !== 'none') decoSet.add(d);\n }\n }\n if (decoSet.size > 0) {\n style.textDecorationLine = [...decoSet].join(' ');\n }\n\n // List marker\n const marker = getListMarker(el, style.listStyleType);\n\n // Resolve `::marker` rules into a Partial<ResolvedStyle> override and a\n // hidden flag. We only do this for `<li>` because `::marker` only applies\n // to elements with `display: list-item` (in our model, just `<li>`).\n // The override records ONLY the keys actually written by marker\n // declarations, so the layout consumer can distinguish \"user set padding\n // to 0\" from \"no rule\".\n let markerStyle: Partial<ResolvedStyle> | undefined;\n let markerHidden = false;\n if (tag === 'li' && matchedMarker.length > 0) {\n // Sort by cascade order — same rules as element style.\n if (matchedMarker.length > 1) {\n matchedMarker.sort((a, b) => {\n if (a.important !== b.important) return a.important ? 1 : -1;\n const sa = a.specificity, sb = b.specificity;\n if (sa[0] !== sb[0]) return sa[0] - sb[0];\n if (sa[1] !== sb[1]) return sa[1] - sb[1];\n if (sa[2] !== sb[2]) return sa[2] - sb[2];\n return a.order - b.order;\n });\n }\n\n // Apply to a scratch style cloned from the resolved <li> style, then\n // copy out the keys that changed. Whitelist the physical fields we\n // actually consume in addListMarker — adding more later is a one-line\n // change once the layout side reads them.\n const TRACKED: (keyof ResolvedStyle)[] = [\n 'paddingLeft', 'paddingRight',\n 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle',\n 'color', 'letterSpacing',\n ];\n const scratch = { ...style } as ResolvedStyle;\n const touched = new Set<keyof ResolvedStyle>();\n for (const m of matchedMarker) {\n // `content: none` (and `content: ''`) suppresses the marker entirely,\n // matching DOM `::marker` behavior. `content` isn't part of\n // ResolvedStyle, so we handle it inline.\n if (m.property === 'content') {\n const v = m.value.trim().toLowerCase();\n if (v === 'none' || v === '\"\"' || v === \"''\" || v === 'normal') {\n // 'normal' is the initial value — no override\n markerHidden = (v === 'none' || v === '\"\"' || v === \"''\");\n }\n continue;\n }\n const before = TRACKED.map(k => scratch[k]);\n applyDeclaration(scratch, m.property, m.value, elemFontSize, containerWidth, direction);\n TRACKED.forEach((k, i) => {\n if (scratch[k] !== before[i]) touched.add(k);\n });\n }\n if (touched.size > 0) {\n markerStyle = {};\n for (const k of touched) (markerStyle as any)[k] = scratch[k];\n }\n }\n\n // Walk children\n const children: StyledNode[] = [];\n for (const child of el.childNodes) {\n const childNode = walkNode(child, style, ctx);\n if (childNode) children.push(childNode);\n }\n\n return {\n element: el,\n tagName: tag,\n style,\n children,\n textContent: null,\n listMarker: marker,\n markerStyle,\n markerHidden: markerHidden || undefined,\n };\n }\n\n function walkNode(\n node: Node,\n parentStyle: ResolvedStyle,\n parentCtx: ElementContext | null,\n ): StyledNode | null {\n if (node.nodeType === TEXT_NODE) {\n const text = node.textContent;\n if (!text) return null;\n\n if (text.trim() === '' && !text.includes('\\u00A0')) {\n const ws = parentStyle.whiteSpace;\n const prev = node.previousSibling;\n const next = node.nextSibling;\n const isInlineSibling = (n: Node | null) => {\n if (!n || n.nodeType !== ELEMENT_NODE) return n?.nodeType === TEXT_NODE;\n const tag = (n as Element).tagName.toLowerCase();\n const def = TAG_DEFAULTS[tag];\n const d = def?.display || 'block';\n return d === 'inline' || d === 'inline-block';\n };\n\n if (prev && next && !isInlineSibling(prev) && !isInlineSibling(next)) {\n if (ws === 'pre' || ws === 'pre-wrap' || ws === 'pre-line') {\n // Keep\n } else {\n return null;\n }\n }\n\n if (ws !== 'pre' && ws !== 'pre-wrap' && ws !== 'pre-line') {\n if (text.includes('\\n')) return null;\n }\n }\n\n // Clone parent style for text node (text nodes don't match CSS rules)\n const style = { ...parentStyle };\n\n // CSS Text 3 §4.1.1: in `normal` and `nowrap`, a source newline is\n // collapsed to a single space (no forced break). Only `pre`,\n // `pre-wrap`, `pre-line`, and `break-spaces` preserve newlines.\n // <br>-derived text nodes are created separately below with `\\n`\n // and are not touched here, so they keep forcing breaks.\n const ws = parentStyle.whiteSpace;\n let normalizedText = text;\n if (ws !== 'pre' && ws !== 'pre-wrap' && ws !== 'pre-line' && ws !== 'break-spaces') {\n normalizedText = text.replace(/[\\n\\r]/g, ' ');\n }\n\n return {\n element: null,\n tagName: '#text',\n style,\n children: [],\n textContent: normalizedText,\n };\n }\n\n if (node.nodeType !== ELEMENT_NODE) return null;\n\n const el = node as Element;\n const tag = el.tagName.toLowerCase();\n if (tag === 'style' || tag === 'script') return null;\n\n // <br> → text node with newline\n if (tag === 'br') {\n return {\n element: null,\n tagName: '#text',\n style: { ...parentStyle },\n children: [],\n textContent: '\\n',\n };\n }\n\n return resolveElement(el, parentStyle, parentCtx);\n }\n\n const rootStyle = defaultStyle();\n const tree = resolveElement(container, rootStyle, null);\n\n const cleanup = () => {\n if (fontStyleEl) fontStyleEl.remove();\n };\n\n return { tree, cleanup };\n}\n","import type { StyledNode, LayoutNode, LayoutBox, LayoutText, ResolvedStyle, LayoutLine, DecorationEntry } from './types.js';\n\n// Module-level flag controlling DOM measurement usage.\n// Set by buildLayoutTree() based on the useDomMeasurements option.\nlet _useDomMeasurements = true;\nlet _debug: ((entry: import('./types.ts').DebugEntry) => void) | undefined;\n\n// Lines emitted during layout. Reset at the start of buildLayoutTree();\n// layoutInlineContent appends one entry per committed line.\nlet _lines: LayoutLine[] = [];\n\n// ─── measureText width cache ──────────────────────────────────────────\n// Caches ctx.measureText(text).width keyed by \"font\\0text\".\n// Cleared at the start of each buildLayoutTree() call.\nconst _measureCache = new Map<string, number>();\n\nfunction cachedMeasureWidth(ctx: CanvasRenderingContext2D, text: string): number {\n // ctx.font and ctx.letterSpacing must already be set by caller.\n // letterSpacing is part of the key because it changes measured width.\n const key = ctx.font + '\\0' + (ctx.letterSpacing || '') + '\\0' + text;\n const cached = _measureCache.get(key);\n if (cached !== undefined) return cached;\n const w = ctx.measureText(text).width;\n _measureCache.set(key, w);\n return w;\n}\n\n\n/**\n * Check if a line has mixed fonts (different fontFamily/fontSize/fontWeight/fontStyle).\n */\nfunction hasMixedFonts(words: Word[]): boolean {\n let font = '';\n for (const w of words) {\n if (!w.text || w.isSpace) continue;\n const f = buildCanvasFont(w.style);\n if (font && f !== font) return true;\n font = f;\n }\n return false;\n}\n\n// ─── Canvas font helpers ───────────────────────────────────────────────\n\n/**\n * Set canvas font and kerning from resolved style.\n */\nexport function applyFont(ctx: CanvasRenderingContext2D, style: ResolvedStyle): void {\n ctx.font = buildCanvasFont(style);\n ctx.fontKerning = style.fontKerning === 'none' ? 'none' : 'normal';\n}\n\n/** Format a letter-spacing value (px) as a canvas `ctx.letterSpacing` string. */\nfunction formatLetterSpacing(value: number): string {\n // Negative letter-spacing is valid and narrows text — Chrome applies it per\n // character (trailing included). Clamping it to 0 measured text wider than\n // the browser renders it, causing earlier/extra line wraps. Guard against\n // non-finite values (undefined/NaN), which would produce an invalid\n // \"undefinedpx\"/\"NaNpx\" string that canvas silently ignores.\n return Number.isFinite(value) && value !== 0 ? `${value}px` : '0px';\n}\n\n/**\n * Build a canvas font string from resolved style. Results are cached.\n */\nconst _fontStringCache = new Map<string, string>();\nexport function buildCanvasFont(style: ResolvedStyle): string {\n const key = `${style.fontStyle}|${style.fontVariantCaps}|${style.fontWeight}|${style.fontSize}|${style.fontFamily}`;\n const cached = _fontStringCache.get(key);\n if (cached) return cached;\n const parts: string[] = [];\n // CSS font shorthand order: style, variant, weight, size, family.\n if (style.fontStyle !== 'normal') parts.push(style.fontStyle);\n if (style.fontVariantCaps === 'small-caps') parts.push('small-caps');\n if (style.fontWeight !== 400) parts.push(String(style.fontWeight));\n parts.push(`${style.fontSize}px`);\n parts.push(style.fontFamily);\n const result = parts.join(' ');\n _fontStringCache.set(key, result);\n return result;\n}\n\n/**\n * Cache for DOM-measured line heights.\n * Key: \"font|lineHeight|probeType\" → actual pixel height from the browser.\n */\nconst _lineHeightCache = new Map<string, number>();\n\n// Probe elements: a <div> for general use, and a <ul><li> for unordered list items.\n// Firefox renders <ul><li> with bullet markers (disc/circle/square) 1.5px taller\n// than other elements for the same line-height, due to the ::marker pseudo-element.\n// <ol><li> items do NOT have this extra height.\nlet _blockProbe: HTMLDivElement | null = null;\nlet _ulProbeContainer: HTMLUListElement | null = null;\nlet _ulProbeLi: HTMLLIElement | null = null;\n\nconst BULLET_MARKERS = new Set(['disc', 'circle', 'square']);\n\n/**\n * Measure the actual line height using a hidden DOM element.\n * Uses an actual <li> inside a <ul> when listStyleType is a bullet marker\n * (disc/circle/square) to capture Firefox's ::marker line box contribution.\n * Results are cached per font+lineHeight+probeType combination.\n */\nfunction measureDomLineHeight(font: string, lineHeight: string, useBulletProbe = false): number {\n const key = `${font}|${lineHeight}|${useBulletProbe ? 'ul-li' : 'block'}`;\n const cached = _lineHeightCache.get(key);\n if (cached !== undefined) return cached;\n\n if (typeof document === 'undefined' || !document.body) {\n throw new Error(\n \"render-tag: accuracy 'balanced' requires a browser DOM for line-height probes; use the default 'performance' mode in non-browser environments.\"\n );\n }\n\n let probe: HTMLElement;\n if (useBulletProbe) {\n if (!_ulProbeContainer) {\n _ulProbeContainer = document.createElement('ul');\n _ulProbeContainer.style.cssText =\n 'position:absolute;top:-9999px;left:-9999px;visibility:hidden;padding:0;margin:0;border:0;list-style:disc;';\n _ulProbeLi = document.createElement('li');\n _ulProbeLi.style.cssText = 'white-space:nowrap;padding:0;margin:0;border:0;';\n _ulProbeLi.textContent = 'Mg';\n _ulProbeContainer.appendChild(_ulProbeLi);\n document.body.appendChild(_ulProbeContainer);\n }\n probe = _ulProbeLi!;\n } else {\n if (!_blockProbe) {\n _blockProbe = document.createElement('div');\n _blockProbe.style.cssText =\n 'position:absolute;top:-9999px;left:-9999px;visibility:hidden;white-space:nowrap;padding:0;margin:0;border:0;';\n _blockProbe.textContent = 'Mg';\n document.body.appendChild(_blockProbe);\n }\n probe = _blockProbe;\n }\n\n probe.style.font = font;\n probe.style.lineHeight = lineHeight;\n const height = probe.getBoundingClientRect().height;\n\n _lineHeightCache.set(key, height);\n return height;\n}\n\n/**\n * Get the effective line height for a style.\n * Uses DOM measurement for accuracy across browsers (Firefox vs Chrome).\n * Falls back to canvas metrics for \"normal\" line-height.\n */\nfunction getLineHeight(ctx: CanvasRenderingContext2D, style: ResolvedStyle, useBulletProbe = false): number {\n if (style.lineHeight > 0) {\n if (_useDomMeasurements) {\n const font = buildCanvasFont(style);\n return measureDomLineHeight(font, `${style.lineHeight}px`, useBulletProbe);\n }\n // Canvas-only: use the CSS line-height value directly\n return style.lineHeight;\n }\n\n if (_useDomMeasurements) {\n const font = buildCanvasFont(style);\n return measureDomLineHeight(font, 'normal', useBulletProbe);\n }\n\n // Canvas-only fallback for \"normal\" line-height: use font bounding box\n // fontBoundingBoxAscent + fontBoundingBoxDescent already represents the\n // full line box height, no multiplier needed.\n const { ascent, descent } = getFontMetrics(ctx, style);\n return ascent + descent;\n}\n\n/**\n * Which engine's line rules to follow. Only the UA string can say, because\n * `accuracy: 'performance'` promises not to touch the DOM.\n *\n * Blink is the DEFAULT, and the other two are what we detect: a server-side\n * render (no navigator, or jsdom) targets headless Chrome, so anything we\n * cannot positively identify has to round the way Chrome does.\n *\n * - Gecko is the one engine that still sends a real `Gecko/<date>` product\n * token; Blink and WebKit carry only the \"like Gecko\" comment, no slash.\n * - Safari is WebKit that says neither `Chrome/` nor `jsdom/`. jsdom borrows\n * WebKit's UA and would otherwise be mistaken for it.\n * - `Chrome/` is matched with NO word boundary, because headless Chrome sends\n * `HeadlessChrome/`.\n */\nconst UA = typeof navigator === 'undefined' ? '' : navigator.userAgent;\nconst IS_GECKO = /\\bGecko\\/\\d/.test(UA);\nconst IS_SAFARI =\n /AppleWebKit/.test(UA) && !/Chrome\\/\\d/.test(UA) && !/\\bjsdom\\//.test(UA);\nconst IS_BLINK = !IS_GECKO && !IS_SAFARI;\n\n/**\n * True where the engine floors a line's baseline onto a whole CSS pixel.\n *\n * Blink alone does (`FontHeight::AddLeading`). Gecko and WebKit both lay the\n * exact half-leading out — measured over the whole 530-case corpus, giving\n * Safari the Blink branch cost 214 wins against 223 losses (avg 7.50% ->\n * 9.21%) where the exact value wins 48 against 3 (7.50% -> 6.52%).\n *\n * Public API: the parity suites expect per engine, and every renderer that has\n * to place a baseline beside a render-tag canvas (@polotno/svg-export) must\n * round the same way this does.\n */\nexport const FLOORS_LINE_BASELINE = IS_BLINK;\n\n/**\n * `super` and `sub` are engine constants, not CSS. Blink and WebKit share\n * theirs (`fontSize/3 + 1`, `fontSize/5 + 1`); Gecko raises by 0.34em and\n * lowers by 0.20em. This is a separate question from the baseline rounding\n * above — Safari rounds like nobody and shifts like Blink.\n */\nconst BLINK_SUPER_SUB = !IS_GECKO;\n\n/**\n * Baseline offset from the top of a line box, the way the engine places it.\n *\n * The CSS half-leading is `(lineHeight - (ascent + descent)) / 2`, and the\n * baseline sits that far below the line top, plus the ascent. Blink FLOORS that\n * sum to a whole CSS pixel (`FontHeight::AddLeading`), so its DOM text stands up\n * to 1px HIGHER than the exact value. Gecko lays the exact value out in app\n * units. WebKit floors on 80 of 90 measured size × line-height combinations and\n * has no exact rule we can state, so it takes the Blink branch — the one that\n * fits it best, not one it matches everywhere.\n *\n * So the rounding is the engine's, not a style choice: each browser's canvas\n * lands on the baseline that browser's own DOM would use, which is what keeps a\n * canvas render and a contenteditable overlay of the same text on one line.\n */\nfunction lineBaselineOffset(lineHeight: number, ascent: number, descent: number): number {\n const exact = (lineHeight - (ascent + descent)) / 2 + ascent;\n return FLOORS_LINE_BASELINE ? Math.floor(exact) : exact;\n}\n\n/**\n * The vertical space an inline-block's margin box adds around its content, over\n * and above the font's own leading. Written once because the wrap pass grows\n * the line by the same six values.\n */\nfunction inlineBlockExtra(bs: ResolvedStyle): { top: number; bottom: number } {\n return {\n top: bs.marginTop + bs.borderTopWidth + bs.paddingTop,\n bottom: bs.paddingBottom + bs.borderBottomWidth + bs.marginBottom,\n };\n}\n\n/**\n * One box's half of a line: how far it reaches above its own baseline and how\n * far below, over its OWN line-height. This is the inline box CSS 2.1 §10.8\n * talks about — the font's content area plus its half-leading — not the bare\n * font metrics. `vertical-align: text-top` and `text-bottom` align THIS box's\n * edges, and the line box is the union of these over everything on the line.\n */\nfunction leadedBox(\n ctx: CanvasRenderingContext2D,\n style: ResolvedStyle,\n useBulletProbe = false,\n): { ascent: number; descent: number } {\n const { ascent, descent } = getFontMetrics(ctx, style);\n const lineHeight = getLineHeight(ctx, style, useBulletProbe);\n const boxAscent = lineBaselineOffset(lineHeight, ascent, descent);\n return { ascent: boxAscent, descent: lineHeight - boxAscent };\n}\n\nfunction applyTextTransform(text: string, transform: string): string {\n\n switch (transform) {\n case 'uppercase': return text.toUpperCase();\n case 'lowercase': return text.toLowerCase();\n // Capitalize the first letter of each word. A mid-word apostrophe is NOT a\n // word boundary (UAX#29), so \"o'clock\" → \"O'clock\", not \"O'Clock\".\n case 'capitalize': return text.replace(/(^|[\\s\\p{P}])(\\p{L})/gu, (m, p, c) =>\n p === \"'\" || p === '’' ? m : p + c.toUpperCase());\n default: return text;\n }\n}\n\nfunction isInline(node: StyledNode): boolean {\n if (node.tagName === '#text') return true;\n const d = node.style.display;\n return d === 'inline' || d === 'inline-block';\n}\n\nfunction hasOnlyInlineChildren(node: StyledNode): boolean {\n return node.children.length > 0 && node.children.every(isInline);\n}\n\nexport function isTransparent(color: string): boolean {\n return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';\n}\n\n/**\n * Get font ascent and descent metrics. Results are cached per font string.\n */\nconst _fontMetricsCache = new Map<string, { ascent: number; descent: number }>();\nexport function getFontMetrics(ctx: CanvasRenderingContext2D, style: ResolvedStyle): { ascent: number; descent: number } {\n const font = buildCanvasFont(style);\n const cached = _fontMetricsCache.get(font);\n if (cached) return cached;\n ctx.font = font;\n const m = ctx.measureText('M');\n const ascent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;\n const descent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;\n const result = { ascent, descent };\n _fontMetricsCache.set(font, result);\n return result;\n}\n\n/**\n * Baseline shift (canvas pixels, positive = downward) for a vertical-align\n * value, applied on top of the line baseline. Returns 0 for 'baseline' and for\n * the line-box-relative keywords 'top'/'bottom' — those need a second layout\n * pass (the box position depends on the final line box it helps size), so they\n * fall back to baseline rather than being approximated wrongly.\n *\n * - super/sub the engine's own rule, measured off the DOM across\n * 8-56px × sans-serif/serif/monospace and fitting every\n * point to within 0.06px (LayoutUnit's 1/64). Neither\n * engine reads the font's metrics — the family does not\n * move the number.\n * - text-top/-bottom the box's LEADED edge against the parent's CONTENT-area\n * edge (bare ascent/descent, no leading). Taking the box's\n * bare metrics instead costs 25px on a line holding both.\n * - middle box midpoint at parent baseline + half the x-height\n * - <length>/<%> raise (positive value) by the length / % of line-height\n */\nfunction verticalAlignShift(\n va: string,\n ctx: CanvasRenderingContext2D, style: ResolvedStyle, parentStyle: ResolvedStyle,\n useBulletProbe: boolean,\n): number {\n switch (va) {\n case 'super':\n return BLINK_SUPER_SUB\n ? -(parentStyle.fontSize / 3 + 1) : -parentStyle.fontSize * 0.34;\n case 'sub':\n return BLINK_SUPER_SUB\n ? parentStyle.fontSize / 5 + 1 : parentStyle.fontSize * 0.2;\n // Against the PARENT's content area (CSS 2.1 §10.8.1) — its bare\n // ascent/descent, no leading. Measured against Chrome, taking the line's\n // tallest box instead of the real parent put this 14px out.\n case 'text-top':\n return leadedBox(ctx, style, useBulletProbe).ascent - getFontMetrics(ctx, parentStyle).ascent;\n case 'text-bottom':\n return getFontMetrics(ctx, parentStyle).descent - leadedBox(ctx, style, useBulletProbe).descent;\n case 'middle': {\n const { ascent, descent } = getFontMetrics(ctx, style);\n return -(parentStyle.fontSize * 0.25) - (descent - ascent) / 2;\n }\n default: {\n // baseline / top / bottom / '' all parseFloat to NaN → 0, which is what\n // an unshifted run wants — the line-box pass calls this for every word.\n const n = parseFloat(va);\n if (!Number.isFinite(n)) return 0;\n // A percentage resolves against the ELEMENT's own line-height (CSS 2.1\n // §10.8.1), not the line's. Measured against Chrome: the line's put the\n // box 10px out on a line whose tallest run was not this one.\n return va.endsWith('%')\n ? -(n / 100) * getLineHeight(ctx, style, useBulletProbe)\n : -n;\n }\n }\n}\n\n/** True when a vertical-align value moves content off the baseline. */\nexport function isShiftedVAlign(va: string): boolean {\n return va !== 'baseline' && va !== 'top' && va !== 'bottom' && va !== '';\n}\n\n/**\n * Two entries put the band in the same place, at the same thickness — the\n * geometry half only, so each caller keeps comparing color its own way (raw\n * here, canonicalized in the path renderer, where `red` and `#ff0000` must\n * still share one dash phase).\n *\n * Identity settles the normal case: entries ride down the tree by reference,\n * so every run under one declarer holds the same object. Two SEPARATE\n * declarers still count as equal when they would draw the same band, which\n * keeps a shaping group whole across siblings that declare the same thing.\n */\nexport function sameDecorationBand(a: DecorationEntry, b: DecorationEntry): boolean {\n if (a === b) return true;\n const da = a.declarer, db = b.declarer;\n return (\n da.fontSize === db.fontSize &&\n da.fontFamily === db.fontFamily &&\n da.fontWeight === db.fontWeight &&\n da.fontStyle === db.fontStyle &&\n da.fontVariantCaps === db.fontVariantCaps &&\n // The declarer's own vertical-align decides which baseline an underline\n // hangs off, so two declarers that differ there draw two bands.\n da.verticalAlign === db.verticalAlign &&\n // Explicit offset/thickness are band geometry too — two declarers that\n // differ there must not merge into one band.\n da.textUnderlineOffset === db.textUnderlineOffset &&\n da.textDecorationThickness === db.textDecorationThickness\n );\n}\n\n/** Same decoration set: entries must match pairwise, so runs whose decorations\n * would paint differently don't merge and take the first one's band. */\nfunction sameDecorations(a: ResolvedStyle, b: ResolvedStyle): boolean {\n const da = a.textDecorations, db = b.textDecorations;\n if (da === db) return true;\n if (!da || !db || da.length !== db.length) return false;\n for (let i = 0; i < da.length; i++) {\n if (\n da[i].line !== db[i].line ||\n da[i].color !== db[i].color ||\n da[i].style !== db[i].style ||\n !sameDecorationBand(da[i], db[i])\n ) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Check if two styles have the same text rendering properties.\n */\nfunction sameTextStyle(a: ResolvedStyle, b: ResolvedStyle): boolean {\n return a.fontFamily === b.fontFamily &&\n a.fontSize === b.fontSize &&\n a.fontWeight === b.fontWeight &&\n a.fontStyle === b.fontStyle &&\n a.color === b.color &&\n a.textDecorationLine === b.textDecorationLine &&\n sameDecorations(a, b) &&\n a.backgroundColor === b.backgroundColor;\n}\n\nfunction hasVisibleBoxStyles(style: ResolvedStyle): boolean {\n if (!isTransparent(style.backgroundColor)) return true;\n if (style.borderTopWidth > 0 && style.borderTopStyle !== 'none') return true;\n if (style.borderRightWidth > 0 && style.borderRightStyle !== 'none') return true;\n if (style.borderBottomWidth > 0 && style.borderBottomStyle !== 'none') return true;\n if (style.borderLeftWidth > 0 && style.borderLeftStyle !== 'none') return true;\n return false;\n}\n\n/** True for an element declaring `background-clip:text` with a visible\n * background (gradient image or solid color) — the fill/decorations of every\n * glyph it covers must sample that background instead of painting it as a box. */\nexport function hasTextClip(style: ResolvedStyle): boolean {\n return style.webkitBackgroundClip === 'text' &&\n ((!!style.backgroundImage && style.backgroundImage !== 'none') ||\n !isTransparent(style.backgroundColor));\n}\n\n// ─── Inline text run types ─────────────────────────────────────────────\n\ninterface TextRun {\n text: string;\n style: ResolvedStyle;\n /**\n * The style of the PARENT of the element this run's style came from — what\n * `vertical-align` measures its shift against (CSS 2.1 §10.8.1). Not the\n * tallest run on the line, which is what a line-level maximum would give:\n * a 40px sibling put a sup 8px out of place.\n */\n parentStyle?: ResolvedStyle;\n /** If this run came from an inline element with visible box styles */\n boxStyle?: ResolvedStyle;\n /** Marks the start of an inline box */\n boxOpen?: ResolvedStyle;\n /** Marks the end of an inline box */\n boxClose?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring background-clip:text + background */\n clipStyle?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring --rt-text-stroke-image */\n strokeImageStyle?: ResolvedStyle;\n}\n\ninterface Word {\n text: string;\n width: number;\n style: ResolvedStyle;\n /** See `TextRun.parentStyle`. */\n parentStyle?: ResolvedStyle;\n isSpace: boolean;\n /** Tab character — width computed dynamically based on position */\n isTab?: boolean;\n /** Word came from soft-hyphen split — show '-' if this word ends a line */\n isSoftHyphenBreak?: boolean;\n /**\n * No soft-wrap opportunity before this word: it abuts the previous word with\n * no whitespace (e.g. adjacent inline spans `<span>a</span><span>b</span>`),\n * so the browser treats them as one unbreakable unit at that boundary.\n */\n noBreakBefore?: boolean;\n boxStyle?: ResolvedStyle;\n /** Marks the start of an inline box (adds left padding/border) */\n boxOpen?: ResolvedStyle;\n /** Marks the end of an inline box (adds right padding/border) */\n boxClose?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring background-clip:text + background */\n clipStyle?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring --rt-text-stroke-image */\n strokeImageStyle?: ResolvedStyle;\n}\n\ninterface PositionedLine {\n words: Word[];\n totalWidth: number;\n lineHeight: number;\n /** True if this line ends at a forced break (\\n or <br>). Such a line is\n * treated as a \"last line\" for text-align — never justified. */\n endedByHardBreak?: boolean;\n}\n\n/** True for atomic inline-block words (boxOpen && boxClose && text together). */\nfunction isAtomicInlineBlock(w: Word): boolean {\n return !!(w.boxOpen && w.boxClose && w.text);\n}\n\n/**\n * Truncate a PositionedLine's trailing words and append \"…\" so the line\n * fits within maxWidth. Used by `-webkit-line-clamp` to mark the visible\n * cut-off on the Nth line.\n *\n * Trim strategy:\n * 1. Pick the style of the last NON-empty, NON-atomic-inline-block word\n * — so the ellipsis font matches the surrounding text, not the button\n * or pill it was sitting next to.\n * 2. Drop trailing isSpace words (genuine spaces only — box markers carry\n * padding/border that we must keep).\n * 3. Back-trim: pop trailing non-space words until ellipsis fits. If we\n * end up with a single text word that STILL doesn't fit, pop it too —\n * the ellipsis stands alone rather than overflowing the container.\n * Box-open markers earlier on the line stay; they preserve inline-box\n * padding/border that the emit loop needs.\n * 4. Inherit boxStyle from the trailing context so inline `<span>`\n * backgrounds/borders extend across the ellipsis.\n */\nfunction applyEllipsisToLine(\n ctx: CanvasRenderingContext2D,\n line: PositionedLine,\n maxWidth: number,\n): void {\n // 1. Find the last word whose style should drive the ellipsis.\n // Skip empty-text markers AND atomic inline-blocks (their style is\n // the inline-block element's, not the surrounding text).\n let styleIdx = line.words.length - 1;\n while (\n styleIdx >= 0 &&\n (line.words[styleIdx].text === '' || isAtomicInlineBlock(line.words[styleIdx]))\n ) styleIdx--;\n if (styleIdx < 0) return;\n const lastStyle = line.words[styleIdx].style;\n const boxStyle = line.words[styleIdx].boxStyle;\n // The ellipsis takes the trimmed run's style, so it has to take the parent\n // that style's vertical-align measures against too.\n const parentStyle = line.words[styleIdx].parentStyle;\n applyFont(ctx, lastStyle);\n // ALWAYS assign (don't gate on truthy) — otherwise a previous segment's\n // non-zero letter-spacing leaks into the ellipsis measurement.\n ctx.letterSpacing = `${lastStyle.letterSpacing || 0}px` as any;\n const ellipsisWidth = cachedMeasureWidth(ctx, '…');\n\n // Helper: pop trailing isSpace words. Box markers (text === '' with\n // boxOpen/boxClose) are NOT popped — they carry inline-box padding the\n // emit loop relies on.\n const popTrailingSpaces = () => {\n while (\n line.words.length > 0 &&\n line.words[line.words.length - 1].isSpace\n ) {\n const r = line.words.pop()!;\n line.totalWidth -= r.width;\n }\n };\n\n // 2. Strip purely trailing whitespace.\n popTrailingSpaces();\n\n // 3. Back-trim non-space text words until the ellipsis fits.\n // Atomic inline-blocks are non-space too; they pop along with words.\n const isTrimmableText = (w: Word) =>\n !w.isSpace && w.text !== '' && !w.boxOpen && !w.boxClose;\n while (\n line.totalWidth + ellipsisWidth > maxWidth &&\n line.words.length > 0\n ) {\n const last = line.words[line.words.length - 1];\n if (!isTrimmableText(last) && !isAtomicInlineBlock(last)) break;\n line.totalWidth -= last.width;\n line.words.pop();\n popTrailingSpaces();\n }\n\n // 4. Append the ellipsis. Inherit boxStyle so inline-span backgrounds /\n // borders extend over the ellipsis.\n const ellipsisWord: Word = {\n text: '…',\n width: ellipsisWidth,\n style: lastStyle,\n parentStyle,\n isSpace: false,\n boxStyle,\n };\n line.words.push(ellipsisWord);\n line.totalWidth += ellipsisWidth;\n}\n\n// ─── Inline layout ─────────────────────────────────────────────────────\n\n/**\n * Collect text runs from inline children, preserving style and tracking\n * inline elements with visible backgrounds. Emits open/close markers\n * for inline boxes so padding/border can be applied.\n */\nfunction collectTextRuns(node: StyledNode): TextRun[] {\n const runs: TextRun[] = [];\n\n function walk(\n n: StyledNode,\n boxStyle?: ResolvedStyle,\n clipStyle?: ResolvedStyle,\n strokeImageStyle?: ResolvedStyle,\n parentStyle?: ResolvedStyle,\n ) {\n if (n.tagName === '#text' && n.textContent) {\n // A #text node carries its parent ELEMENT's style, so the element that\n // owns any vertical-align here is that parent — and what the shift\n // measures against is ITS parent, which is the `parentStyle` handed to\n // this element's walk.\n runs.push({\n text: n.textContent, style: n.style, parentStyle, boxStyle, clipStyle, strokeImageStyle,\n });\n return;\n }\n const isInlineBlock = n.style.display === 'inline-block';\n // Inline-block always needs box treatment (padding/margin affect layout)\n const isBox = isInlineBlock || (isInline(n) && hasVisibleBoxStyles(n.style));\n const newBoxStyle = isBox ? n.style : boxStyle;\n // Track the nearest inline element declaring a background-clip:text\n // background or a --rt-text-stroke-image, so those paints reach descendant\n // runs that don't carry the (non-inheriting) properties themselves.\n const newClipStyle = isInline(n) && hasTextClip(n.style) ? n.style : clipStyle;\n const newStrokeImageStyle =\n isInline(n) && n.style.webkitTextStrokeImage && n.style.webkitTextStrokeImage !== 'none'\n ? n.style : strokeImageStyle;\n const hasHorizSpacing = isBox && (n.style.paddingLeft > 0 || n.style.paddingRight > 0 ||\n n.style.borderLeftWidth > 0 || n.style.borderRightWidth > 0);\n\n if (isInlineBlock) {\n // Inline-block is fully atomic — the entire element (margins + padding + text)\n // wraps as one unit. We emit a single \"atomic\" TextRun with a special marker\n // so the tokenizer creates one non-splittable word with the full box width.\n const allText = n.element?.textContent || '';\n runs.push({\n text: allText,\n style: n.style,\n parentStyle,\n boxStyle: newBoxStyle,\n clipStyle: newClipStyle,\n strokeImageStyle: newStrokeImageStyle,\n // Store the full box info for atomic inline-block handling\n boxOpen: n.style, // signals this is a boxed element\n boxClose: n.style,\n });\n return;\n }\n\n // unicode-bidi: bidi-override (e.g. <bdo dir=\"rtl\">) forces visual order.\n // For an RTL override, reverse both the characters of each descendant run\n // and the order of the runs, so the subtree renders right-to-left.\n const ub = n.style.unicodeBidi;\n const overrideRtl = (ub === 'bidi-override' || ub === 'isolate-override') &&\n n.style.direction === 'rtl';\n const overrideStart = runs.length;\n\n if (hasHorizSpacing) {\n runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxOpen: n.style });\n }\n\n for (const child of n.children) {\n walk(\n child, isBox ? newBoxStyle : boxStyle, newClipStyle, newStrokeImageStyle,\n // An element child measures against this element; a text child's\n // vertical-align belongs to this element, so it measures against what\n // this element measures against.\n child.tagName === '#text' ? parentStyle : n.style,\n );\n }\n\n if (hasHorizSpacing) {\n runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxClose: n.style });\n }\n\n if (overrideRtl && runs.length > overrideStart) {\n const seg = runs.splice(overrideStart);\n for (const r of seg) {\n if (r.text) {\n r.text = [...r.text].reverse().join('');\n // The glyphs are now in visual (reversed) order, so render them\n // left-to-right; otherwise renderText would right-anchor x and the\n // LTR emission (which set x as the left edge) would misposition them.\n r.style = { ...r.style, direction: 'ltr' };\n }\n }\n seg.reverse();\n runs.push(...seg);\n }\n }\n\n // The block itself is the parent every top-level run measures against.\n for (const child of node.children) {\n walk(child, undefined, undefined, undefined, node.style);\n }\n return runs;\n}\n\n/**\n * Check if text needs Intl.Segmenter for word breaking (Thai, Khmer, Lao, Myanmar).\n * These scripts don't use spaces between words.\n */\nfunction needsSegmenter(text: string): boolean {\n for (let i = 0; i < text.length; i++) {\n const code = text.codePointAt(i)!;\n if (\n (code >= 0x0E00 && code <= 0x0E7F) || // Thai\n (code >= 0x0E80 && code <= 0x0EFF) || // Lao\n (code >= 0x1000 && code <= 0x109F) || // Myanmar\n (code >= 0x1780 && code <= 0x17FF) // Khmer\n ) return true;\n if (code > 0xFFFF) i++; // skip surrogate pair\n }\n return false;\n}\n\nlet _segmenter: Intl.Segmenter | undefined;\nfunction getSegmenter(): Intl.Segmenter | null {\n if (_segmenter) return _segmenter;\n if (typeof Intl !== 'undefined' && Intl.Segmenter) {\n _segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });\n return _segmenter;\n }\n return null;\n}\n\n/**\n * Tokenize a single string into words based on whitespace mode.\n */\nfunction tokenizeString(ctx: CanvasRenderingContext2D, text: string, run: TextRun, allWords: Word[], cumState?: { cumText: string; cumWidth: number }): void {\n // Split on zero-width spaces and soft hyphens (break opportunities).\n // Pass cumulative state through so pieces are measured as one text run\n // (preserving kerning accuracy across break points).\n if (text.includes('\\u200B') || text.includes('\\u00AD')) {\n const parts = text.split(/(\\u200B|\\u00AD)/);\n // Share cumulative state across all sub-parts for accurate measurement\n const sharedState = cumState ?? { cumText: '', cumWidth: 0 };\n let nextIsSoftHyphen = false;\n for (const part of parts) {\n if (part === '\\u00AD') {\n nextIsSoftHyphen = true;\n continue;\n }\n if (part === '\\u200B' || part === '') {\n nextIsSoftHyphen = false;\n continue;\n }\n const prevLen = allWords.length;\n tokenizeString(ctx, part, run, allWords, sharedState);\n if (nextIsSoftHyphen && prevLen > 0) {\n allWords[prevLen - 1].isSoftHyphenBreak = true;\n }\n nextIsSoftHyphen = false;\n }\n if (nextIsSoftHyphen && allWords.length > 0) {\n allWords[allWords.length - 1].isSoftHyphenBreak = true;\n }\n return;\n }\n\n // `pre-line` preserves newlines (handled by the \\n pre-split in\n // tokenizeRuns) but collapses spaces and tabs — so it goes through the\n // non-preserving branch below, same as `normal`.\n const isPreserve = run.style.whiteSpace === 'pre' ||\n run.style.whiteSpace === 'pre-wrap' ||\n run.style.whiteSpace === 'break-spaces';\n\n if (isPreserve) {\n // Split on spaces and tabs, keeping delimiters\n const words = text.split(/( +|\\t)/);\n const tabStopInterval = cachedMeasureWidth(ctx, ' ') * 8; // CSS default: 8 spaces\n for (const w of words) {\n if (w === '') continue;\n if (w === '\\t') {\n // Tab width depends on current position — mark it for dynamic calculation\n allWords.push({\n text: '\\t',\n width: tabStopInterval, // placeholder — recalculated in flowWordsIntoLines\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: true,\n isTab: true,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n continue;\n }\n const isSpace = /^ +$/.test(w);\n allWords.push({\n text: w,\n width: cachedMeasureWidth(ctx, w),\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n }\n } else {\n // Split on whitespace but NOT on non-breaking spaces (\\u00A0).\n // Then add a break opportunity AFTER \"?\" inside an otherwise-unbreakable\n // token (the URL query delimiter): Chrome wraps \"\\u2026/q3?\" | \"lang=ar&\\u2026\"\n // even with overflow-wrap:normal. It does NOT break at \"/\", \"&\", \"=\", \".\"\n // or \":\" (verified against the browser), so only \"?\" is split here. The\n // \"?\" stays with the preceding fragment; a trailing \"?\" (no follower) is\n // left intact. Fragments measure cumulatively so kerning stays accurate.\n const words = text\n .split(/([ \\t\\n\\r\\f\\v]+)/)\n .flatMap((w) =>\n /^[ \\t\\n\\r\\f\\v]+$/.test(w) ? [w] : w.split(/(?<=\\?)(?=.)/),\n );\n\n // Use cumulative measurement to avoid rounding error accumulation\n // within a single text run. When cumState is provided (from \\u200B/\\u00AD\n // split), continue from the previous cumulative position to preserve\n // kerning accuracy across break points.\n let cumText = cumState?.cumText ?? '';\n let cumWidth = cumState?.cumWidth ?? 0;\n\n for (const w of words) {\n if (w === '') continue;\n const isSpace = /^[ \\t\\n\\r\\f\\v]+$/.test(w);\n\n if (isSpace) {\n const prevCum = cumWidth;\n cumText += ' ';\n cumWidth = ctx.measureText(cumText).width;\n const spaceWidth = cumWidth - prevCum + (run.style.wordSpacing || 0);\n allWords.push({\n text: ' ',\n width: spaceWidth,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: true,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n continue;\n }\n\n // Use Intl.Segmenter for scripts without spaces (Thai, Khmer, etc.)\n if (needsSegmenter(w)) {\n const segmenter = getSegmenter();\n if (segmenter) {\n for (const seg of segmenter.segment(w)) {\n const s = seg.segment;\n const prevCum = cumWidth;\n cumText += s;\n cumWidth = ctx.measureText(cumText).width;\n allWords.push({\n text: s,\n width: cumWidth - prevCum,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: false,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n }\n continue;\n }\n }\n\n const prevCum = cumWidth;\n cumText += w;\n cumWidth = ctx.measureText(cumText).width;\n let width = cumWidth - prevCum;\n const directWidth = cachedMeasureWidth(ctx, w);\n if (_debug) {\n _debug({\n type: 'measure-word',\n message: `\"${w}\" delta=${width.toFixed(2)} direct=${directWidth.toFixed(2)} diff=${(width - directWidth).toFixed(2)} cumText=\"${cumText}\"`,\n data: { text: w, deltaWidth: width, directWidth, cumWidth, prevCum, font: run.style.fontFamily, fontSize: run.style.fontSize },\n });\n }\n allWords.push({\n text: w,\n width,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: false,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n }\n\n // Propagate cumulative state back to caller (for \\u200B/\\u00AD splits)\n if (cumState) {\n cumState.cumText = cumText;\n cumState.cumWidth = cumWidth;\n }\n }\n}\n\n/**\n * Tokenize text runs into words for line wrapping.\n */\nfunction tokenizeRuns(ctx: CanvasRenderingContext2D, runs: TextRun[]): Word[] {\n const allWords: Word[] = [];\n\n for (const run of runs) {\n // Handle inline-block margins (empty text, no boxOpen/boxClose)\n if (run.text === '' && !run.boxOpen && !run.boxClose) {\n const margin = run.style.display === 'inline-block'\n ? (run.style.marginLeft || run.style.marginRight || 0)\n : 0;\n if (margin > 0) {\n allWords.push({ text: '', width: margin, style: run.style, isSpace: false, boxStyle: run.boxStyle });\n }\n continue;\n }\n\n // Atomic inline-block: entire element (margin + padding + text) is one word\n // Must check before boxOpen/boxClose handlers since atomic has both set.\n if (run.boxOpen && run.boxClose && run.text) {\n applyFont(ctx, run.style);\n ctx.letterSpacing = formatLetterSpacing(run.style.letterSpacing);\n const text = applyTextTransform(run.text, run.style.textTransform);\n const s = run.style;\n const textWidth = cachedMeasureWidth(ctx, text);\n const totalWidth = s.marginLeft + s.borderLeftWidth + s.paddingLeft +\n textWidth + s.paddingRight + s.borderRightWidth + s.marginRight;\n allWords.push({\n text,\n width: totalWidth,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: false,\n boxStyle: run.boxStyle,\n boxOpen: run.boxOpen,\n boxClose: run.boxClose,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n continue;\n }\n\n // Handle inline box open/close markers (padding)\n if (run.boxOpen) {\n const pad = run.boxOpen.paddingLeft + run.boxOpen.borderLeftWidth;\n if (pad > 0) {\n allWords.push({ text: '', width: pad, style: run.style, isSpace: false, boxStyle: run.boxStyle, boxOpen: run.boxOpen });\n }\n continue;\n }\n if (run.boxClose) {\n const pad = run.boxClose.paddingRight + run.boxClose.borderRightWidth;\n if (pad > 0) {\n allWords.push({ text: '', width: pad, style: run.style, isSpace: false, boxStyle: run.boxStyle, boxClose: run.boxClose });\n }\n continue;\n }\n\n applyFont(ctx, run.style);\n ctx.letterSpacing = formatLetterSpacing(run.style.letterSpacing);\n const text = applyTextTransform(run.text, run.style.textTransform);\n\n // Mark the first word produced from `startLen` as having no soft-wrap\n // opportunity before it when it directly abuts real text from a previous\n // run (adjacent inline elements with no whitespace between them). The\n // preceding word must be actual text — not a space, newline, empty\n // box-padding marker, or box edge — so a whitespace/padding boundary still\n // allows a break.\n const markGlue = (startLen: number) => {\n const first = allWords[startLen];\n if (!first || first.isSpace || !first.text || first.text === '\\n') return;\n const prev = allWords[startLen - 1];\n if (\n !prev || prev.isSpace || !prev.text.trim() ||\n prev.boxOpen || prev.boxClose\n ) return;\n // CJK, emoji and segmenter-driven scripts (Thai/Khmer/…) have break\n // opportunities between characters regardless of element boundaries, so\n // an element edge between them is NOT a no-break point. Only glue when\n // both sides are ordinary (Latin-like) text with no intrinsic break.\n // Take the boundary characters as GRAPHEME clusters — indexing by code\n // unit reads past the end of a surrogate pair, and indexing by code point\n // splits VS16 emoji (❤️ = U+2764 U+FE0F) so the cluster reads as non-emoji.\n const firstChar = graphemes(first.text)[0];\n const prevClusters = graphemes(prev.text);\n const prevChar = prevClusters[prevClusters.length - 1];\n if (\n isCJK(firstChar) || isCJK(prevChar) ||\n isEmojiCluster(firstChar) || isEmojiCluster(prevChar) ||\n needsSegmenter(first.text) || needsSegmenter(prev.text)\n ) return;\n first.noBreakBefore = true;\n };\n\n // Handle explicit newlines (from <br> or pre-wrap) — always force line break\n if (text.includes('\\n')) {\n const parts = text.split('\\n');\n for (let i = 0; i < parts.length; i++) {\n if (i > 0) {\n allWords.push({ text: '\\n', width: 0, style: run.style, isSpace: false, boxStyle: run.boxStyle });\n }\n if (parts[i]) {\n const startLen = allWords.length;\n tokenizeString(ctx, parts[i], run, allWords);\n markGlue(startLen);\n }\n }\n } else {\n const startLen = allWords.length;\n tokenizeString(ctx, text, run, allWords);\n markGlue(startLen);\n }\n }\n\n return allWords;\n}\n\n/**\n * Check if a character is CJK (Chinese/Japanese/Korean) — these wrap at character level.\n */\nfunction isCJK(char: string): boolean {\n const code = char.codePointAt(0) || 0;\n return (\n (code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified\n (code >= 0x3400 && code <= 0x4DBF) || // CJK Extension A\n (code >= 0x3000 && code <= 0x303F) || // CJK Symbols\n (code >= 0x3040 && code <= 0x309F) || // Hiragana\n (code >= 0x30A0 && code <= 0x30FF) || // Katakana\n (code >= 0xAC00 && code <= 0xD7AF) || // Hangul\n (code >= 0xFF00 && code <= 0xFFEF) || // Fullwidth\n (code >= 0x20000 && code <= 0x2A6DF) // CJK Extension B\n );\n}\n\nlet _graphemeSegmenter: Intl.Segmenter | undefined;\nfunction getGraphemeSegmenter(): Intl.Segmenter | null {\n if (_graphemeSegmenter) return _graphemeSegmenter;\n if (typeof Intl !== 'undefined' && Intl.Segmenter) {\n _graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n return _graphemeSegmenter;\n }\n return null;\n}\n\n/**\n * Split into grapheme clusters — falls back to code points when\n * Intl.Segmenter is unavailable.\n */\nfunction graphemes(text: string): string[] {\n const seg = getGraphemeSegmenter();\n return seg ? [...seg.segment(text)].map((s) => s.segment) : [...text];\n}\n\nconst EMOJI_PICTOGRAPHIC = /\\p{Extended_Pictographic}/u;\n/**\n * Is this grapheme cluster an emoji that creates a line-break opportunity?\n * Restricted to emoji-presentation clusters (emoji planes, regional-indicator\n * flags, and ZWJ/VS16 sequences) so plain text symbols like ©/®/™ — which are\n * Extended_Pictographic but render as text and do NOT break — are excluded.\n */\nfunction isEmojiCluster(s: string): boolean {\n for (const ch of s) {\n const cp = ch.codePointAt(0)!;\n if (cp >= 0x1f000) return true; // emoji planes (incl. regional indicators)\n }\n if (s.includes('\\u200D') || s.includes('\\uFE0F')) {\n return EMOJI_PICTOGRAPHIC.test(s); // ZWJ sequence or VS16 emoji presentation\n }\n return false;\n}\n\n/**\n * Break a word into character-level pieces if it contains CJK/emoji or if\n * overflow-wrap: break-word is set and the word is too wide.\n */\nfunction breakWordIfNeeded(\n ctx: CanvasRenderingContext2D,\n word: Word,\n contentWidth: number,\n currentLineWidth: number,\n): Word[] {\n // Check if word has CJK characters — always break at character level\n const hasCJK = [...word.text].some(isCJK);\n\n // Emoji form their own break opportunities (a run of emoji wraps between\n // clusters). Only meaningful when a grapheme segmenter is available so ZWJ\n // sequences / skin-tone / flag pairs stay intact.\n const hasEmoji = EMOJI_PICTOGRAPHIC.test(word.text) && !!getGraphemeSegmenter();\n\n // Check if word needs break-word splitting — when it won't fit on a fresh line\n const needsBreak = word.width > contentWidth &&\n (word.style.overflowWrap === 'break-word' || word.style.wordBreak === 'break-all');\n\n if (!hasCJK && !hasEmoji && !needsBreak) return [word];\n\n // overflow-wrap:break-word is a LAST RESORT — the browser first uses any\n // normal break opportunity inside the word (a hyphen) before breaking\n // mid-character. So split a hyphenated word at its hyphens first and only\n // char-break the segments that are themselves still too wide. (word-break:\n // break-all genuinely allows breaking between any two characters, so it\n // skips this and falls through to the char loop below.)\n if (needsBreak && word.style.wordBreak !== 'break-all' &&\n word.style.overflowWrap === 'break-word') {\n const segTexts = word.text.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/).filter((s) => s.length);\n if (segTexts.length > 1) {\n ctx.font = buildCanvasFont(word.style);\n ctx.letterSpacing = formatLetterSpacing(word.style.letterSpacing);\n const out: Word[] = [];\n for (const segText of segTexts) {\n const segWidth = cachedMeasureWidth(ctx, segText);\n if (segWidth <= contentWidth) {\n out.push({ ...word, text: segText, width: segWidth });\n } else {\n // Segment still overflows — char-break just this segment.\n out.push(...breakWordIfNeeded(ctx, { ...word, text: segText, width: segWidth }, contentWidth, 0));\n }\n }\n return out;\n }\n }\n\n // Split into characters using cumulative measurement for accuracy.\n // Measuring each char individually ignores kerning — the sum of individual\n // widths diverges from the true string width over many characters.\n ctx.font = buildCanvasFont(word.style);\n // Re-assert letter-spacing: tokenizeRuns may have left ctx at a later run's\n // value, but break points must use THIS word's letter-spacing.\n ctx.letterSpacing = formatLetterSpacing(word.style.letterSpacing);\n // When the word contains emoji, iterate by GRAPHEME cluster so multi-codepoint\n // emoji (ZWJ families, skin tones, flags) are never split mid-cluster.\n const chars = hasEmoji ? graphemes(word.text) : [...word.text];\n const pieces: Word[] = [];\n\n let current = '';\n let currentWidth = 0;\n\n for (const char of chars) {\n // Emoji clusters each get their own word — a break opportunity between\n // adjacent emoji, matching the browser line breaker.\n if (hasEmoji && isEmojiCluster(char)) {\n if (current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n current = '';\n currentWidth = 0;\n }\n pieces.push({ ...word, text: char, width: cachedMeasureWidth(ctx, char) });\n continue;\n }\n\n // CJK chars always get their own word for wrapping\n if (isCJK(char)) {\n if (current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n current = '';\n currentWidth = 0;\n }\n const charWidth = cachedMeasureWidth(ctx, char);\n pieces.push({ ...word, text: char, width: charWidth });\n continue;\n }\n\n // Use cumulative measurement: measure the growing string, not individual chars\n const candidateText = current + char;\n const candidateWidth = cachedMeasureWidth(ctx, candidateText);\n\n // For break-word: break when adding this char would exceed container\n if (needsBreak && candidateWidth > contentWidth && current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n current = char;\n currentWidth = cachedMeasureWidth(ctx, char);\n continue;\n }\n\n current = candidateText;\n currentWidth = candidateWidth;\n }\n\n if (current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n }\n\n return pieces;\n}\n\n/** Punctuation that cannot start a line — stays with the preceding word. */\nconst TRAILING_PUNCT = /^[,.\\;:!?\\)\\]\\}'\"»›]+$/;\n\n/**\n * Flow words into lines that fit within contentWidth.\n * Handles: word wrapping, nowrap, break-word, CJK character wrapping.\n */\nfunction flowWordsIntoLines(\n ctx: CanvasRenderingContext2D,\n words: Word[],\n contentWidth: number,\n whiteSpace: string,\n useBulletProbe = false,\n textIndent = 0,\n tabMetrics?: { interval: number; halfSpace: number },\n strutLineHeight = 0,\n): PositionedLine[] {\n const lines: PositionedLine[] = [];\n // Every line box starts at the block's own \"strut\" height (its font +\n // line-height), so a line whose only content is a SMALLER inline font is\n // still at least the block's line-height tall — matching CSS. See callers.\n const newLine = (): PositionedLine => ({\n words: [],\n totalWidth: 0,\n lineHeight: strutLineHeight,\n });\n let currentLine: PositionedLine = newLine();\n const noWrap = whiteSpace === 'nowrap' || whiteSpace === 'pre';\n // text-indent reduces the first line's width budget; subsequent lines use full width.\n const effWidth = () => contentWidth - (lines.length === 0 ? textIndent : 0);\n\n const isPreWrap = whiteSpace === 'pre-wrap' || whiteSpace === 'pre' || whiteSpace === 'pre-line';\n // `pre`, `pre-wrap`, and `break-spaces` preserve author whitespace\n // (leading and trailing); the others collapse it.\n const preservesWhitespace =\n whiteSpace === 'pre' || whiteSpace === 'pre-wrap' || whiteSpace === 'break-spaces';\n\n function pushLine(isSoftWrap = false) {\n const hadWords = currentLine.words.length > 0;\n // Trim trailing spaces. `break-spaces` preserves them even at soft wraps;\n // `pre`/`pre-wrap` preserve them at hard breaks and end-of-content but not\n // at soft wraps (per CSS Text 3 §4.1.1).\n const preserveTrailing = whiteSpace === 'break-spaces'\n || (preservesWhitespace && !isSoftWrap);\n if (!preserveTrailing) {\n while (currentLine.words.length > 0 && currentLine.words[currentLine.words.length - 1].isSpace) {\n currentLine.totalWidth -= currentLine.words[currentLine.words.length - 1].width;\n currentLine.words.pop();\n }\n }\n // Soft hyphen: if this is a soft wrap and the last word has a soft-hyphen\n // break, append a visible '-' since the word is being broken here.\n if (isSoftWrap && currentLine.words.length > 0) {\n const lastWord = currentLine.words[currentLine.words.length - 1];\n if (lastWord.isSoftHyphenBreak) {\n applyFont(ctx, lastWord.style);\n const hyphenWidth = cachedMeasureWidth(ctx, '-');\n currentLine.words.push({\n text: '-',\n width: hyphenWidth,\n style: lastWord.style,\n parentStyle: lastWord.parentStyle,\n isSpace: false,\n // The visible hyphen continues the broken word, so it inherits the\n // word's clip/stroke-image declarer (else it paints transparent).\n clipStyle: lastWord.clipStyle,\n strokeImageStyle: lastWord.strokeImageStyle,\n });\n currentLine.totalWidth += hyphenWidth;\n }\n }\n // In pre-wrap mode, space-only lines still need height (they are content)\n if (currentLine.words.length > 0 || (hadWords && isPreWrap)) {\n if (_debug) {\n const text = currentLine.words.map(w => w.text).join('');\n _debug({\n type: 'line-commit',\n message: `Line ${lines.length}: \"${text}\" width=${currentLine.totalWidth.toFixed(2)} / ${contentWidth}`,\n data: { lineIndex: lines.length, text, totalWidth: currentLine.totalWidth, contentWidth },\n });\n }\n lines.push(currentLine);\n }\n currentLine = newLine();\n }\n\n let afterHardBreak = true; // start of content is like after a hard break\n\n for (let wordIndex = 0; wordIndex < words.length; wordIndex++) {\n const word = words[wordIndex];\n let wordLineHeight = getLineHeight(ctx, word.style, useBulletProbe);\n // Inline-block elements expand line height with their vertical padding+margin\n if (word.boxStyle && word.boxStyle.display === 'inline-block') {\n // Clamped at 0: negative margins shrink the margin box, but the original\n // `Math.max(h, h + extra)` never let them shrink the LINE, and nothing\n // here is measuring a case that says they should.\n const extra = inlineBlockExtra(word.boxStyle);\n wordLineHeight += Math.max(0, extra.top + extra.bottom);\n }\n\n if (word.text === '\\n') {\n if (currentLine.words.length === 0) {\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n currentLine.endedByHardBreak = true;\n lines.push(currentLine);\n currentLine = newLine();\n } else {\n currentLine.endedByHardBreak = true;\n pushLine();\n }\n afterHardBreak = true;\n continue;\n }\n\n // No wrapping mode — everything on one line\n if (noWrap) {\n currentLine.words.push(word);\n currentLine.totalWidth += word.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n continue;\n }\n\n // Breaking a word that is split across a run boundary. A single word split\n // across adjacent inline runs (e.g. <span>E</span>xperience, a font-size\n // change mid-word, or <span>wel</span>l-being) is several Words glued by\n // `noBreakBefore`. Per-word break logic can't see the whole word, so its\n // internal break opportunities — hyphens, and break-word char points — are\n // lost and the unit overflows the edge. Detect the maximal glued chain\n // starting here and break it across the run boundaries like the browser.\n if (!word.isSpace && word.text && !word.noBreakBefore && !word.boxOpen && !word.boxClose) {\n let end = wordIndex;\n while (end + 1 < words.length) {\n const nx = words[end + 1];\n if (!nx.text || nx.isSpace || nx.boxOpen || nx.boxClose || !nx.noBreakBefore) break;\n end++;\n }\n if (end > wordIndex) {\n const breakWord = word.style.overflowWrap === 'break-word' || word.style.wordBreak === 'break-all';\n let combined = 0;\n for (let j = wordIndex; j <= end; j++) combined += words[j].width;\n // Flatten the chain into styled characters (per-run style retained).\n // Carry the run's clip/stroke-image declarer too, else a break-word\n // split drops it and a gradient/stroke fragment paints nothing (the\n // inherited transparent fill has no clip box to reveal).\n // `parentStyle` rides along for the same reason: dropping it made a\n // split `vertical-align` run measure its shift against the block\n // instead of its real parent, 8px out on a narrow break-word line.\n type Cell = {\n ch: string;\n style: ResolvedStyle;\n parentStyle?: ResolvedStyle;\n clipStyle?: ResolvedStyle;\n strokeImageStyle?: ResolvedStyle;\n };\n const cells: Cell[] = [];\n for (let j = wordIndex; j <= end; j++)\n for (const ch of [...words[j].text])\n cells.push({\n ch,\n style: words[j].style,\n parentStyle: words[j].parentStyle,\n clipStyle: words[j].clipStyle,\n strokeImageStyle: words[j].strokeImageStyle,\n });\n const combinedText = cells.map((c) => c.ch).join('');\n // Hyphen break opportunities (same rule as the single-word hyphen path).\n const segTexts = combinedText.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/).filter((s) => s.length);\n const hyphenMode = segTexts.length > 1;\n const fitsLine = currentLine.totalWidth + combined <= effWidth();\n // A hyphen is an ordinary break opportunity — intervene whenever the\n // unit doesn't fit the remaining space. break-word is last-resort —\n // only when the unit can't fit a full line at all (otherwise the normal\n // flow + glued-tail fit check correctly wraps it whole to a fresh line).\n const enter = !fitsLine && (hyphenMode || (breakWord && combined > effWidth()));\n if (enter) {\n // Atomic units for breaking: hyphen segments, else the whole chain.\n const segs: Cell[][] = [];\n let ci = 0;\n for (const st of segTexts) {\n const len = [...st].length;\n segs.push(cells.slice(ci, ci + len));\n ci += len;\n }\n // Place a segment's cells onto the current line, splitting same-style\n // runs into pieces. When `chars` is set, wrap at the line edge between\n // characters (break-word); otherwise place atomically (it may overflow\n // its own line, e.g. a hyphen prefix wider than the container).\n const placeCells = (cs: Cell[], chars: boolean) => {\n let i = 0;\n while (i < cs.length) {\n const st = cs[i].style;\n // clip/stroke declarer is 1:1 with the style run (same source\n // word), so capturing it at the run start covers every push below.\n const clipStyle = cs[i].clipStyle;\n const strokeImageStyle = cs[i].strokeImageStyle;\n const parentStyle = cs[i].parentStyle;\n applyFont(ctx, st);\n ctx.letterSpacing = formatLetterSpacing(st.letterSpacing);\n const lh = getLineHeight(ctx, st, useBulletProbe);\n const run: { ch: string; style: ResolvedStyle }[] = [];\n let cur = '';\n let curW = 0;\n while (i < cs.length && cs[i].style === st) {\n const ch = cs[i].ch;\n const candW = cachedMeasureWidth(ctx, cur + ch);\n if (chars && currentLine.totalWidth + candW > effWidth() &&\n (currentLine.words.length > 0 || cur)) {\n if (cur) {\n currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });\n currentLine.totalWidth += curW;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);\n }\n pushLine(true);\n afterHardBreak = false;\n cur = ch;\n curW = cachedMeasureWidth(ctx, ch);\n } else {\n cur += ch;\n curW = candW;\n }\n i++;\n }\n if (cur) {\n currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });\n currentLine.totalWidth += curW;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);\n afterHardBreak = false;\n }\n }\n };\n const measureSeg = (cs: Cell[]) => {\n let w = 0;\n let i = 0;\n while (i < cs.length) {\n const st = cs[i].style;\n let txt = '';\n while (i < cs.length && cs[i].style === st) { txt += cs[i].ch; i++; }\n applyFont(ctx, st);\n ctx.letterSpacing = formatLetterSpacing(st.letterSpacing);\n w += cachedMeasureWidth(ctx, txt);\n }\n return w;\n };\n // Pure break-word (no hyphen) is last-resort: move the whole word to a\n // fresh line first (using the preceding space), then break it there.\n if (!hyphenMode && currentLine.words.length > 0) {\n pushLine(true);\n afterHardBreak = false;\n }\n for (const seg of segs) {\n const segW = measureSeg(seg);\n if (currentLine.words.length > 0 && currentLine.totalWidth + segW > effWidth()) {\n pushLine(true);\n afterHardBreak = false;\n }\n // Char-break a segment only when break-word and it can't fit a line.\n placeCells(seg, breakWord && segW > effWidth());\n }\n wordIndex = end;\n continue;\n }\n }\n }\n\n // Break long words / CJK characters if needed\n const pieces = (!word.isSpace && word.text.length > 1)\n ? breakWordIfNeeded(ctx, word, effWidth(), currentLine.totalWidth)\n : [word];\n\n // Glued tail: content immediately after this word that cannot start a new\n // line — trailing punctuation (\",.)]}…\"), an inline span's right\n // padding/border (empty boxClose markers), and a word continuation that\n // abuts this word across a run boundary with no soft-wrap opportunity\n // (noBreakBefore — e.g. one word split across two inline spans with\n // different font sizes). The browser includes all of it when deciding\n // whether this word fits, so the unit wraps together: if \"Music Experie\"\n // doesn't leave room for the glued \"nce\", the whole word wraps as one.\n // Stops at whitespace or the next breakable word.\n let gluedTailWidth = 0;\n for (let j = wordIndex + 1; j < words.length; j++) {\n const nw = words[j];\n if (nw.isSpace || nw.text === '\\n') break;\n const isPunct = !!nw.text && TRAILING_PUNCT.test(nw.text);\n const isCloseMarker = !nw.text && !!nw.boxClose;\n const isGluedCont = !!nw.text && !!nw.noBreakBefore;\n if (isPunct || isCloseMarker || isGluedCont) { gluedTailWidth += nw.width; continue; }\n break;\n }\n\n for (const piece of pieces) {\n const isLastPiece = piece === pieces[pieces.length - 1];\n // Only the last piece of the word carries the glued tail.\n const tail = isLastPiece ? gluedTailWidth : 0;\n // Trailing punctuation (e.g. comma after </span>) should not wrap\n // independently — browsers keep it with the preceding word.\n const isTrailingPunct = !piece.isSpace && piece.text.length > 0 &&\n TRAILING_PUNCT.test(piece.text) &&\n currentLine.words.length > 0 &&\n !currentLine.words[currentLine.words.length - 1].isSpace;\n\n // A word that abuts the previous run with no whitespace has no soft-wrap\n // opportunity before it — keep it with the preceding word like trailing\n // punctuation. Only the FIRST piece carries the flag; a break-word split\n // inside the word may still wrap mid-word.\n const isGlued = piece === pieces[0] && piece.noBreakBefore &&\n currentLine.words.length > 0 &&\n !currentLine.words[currentLine.words.length - 1].isSpace;\n\n // Leading inline padding/border (an empty boxOpen marker) must not be\n // stranded at the end of a line — it belongs with the span's following\n // content (CSS applies padding-left at the box's start). Include the next\n // content word's width in this marker's fit test so the two wrap together\n // and the left padding lands on the new line with the content.\n let headExtra = 0;\n if (!piece.text && piece.boxOpen) {\n const next = words[wordIndex + 1];\n if (next && !next.isSpace && next.text) {\n // Only the next word's first BREAKABLE unit must stay with the leading\n // padding — the whole word for unbreakable Latin, but just the first\n // character for CJK / break-word (which wrap per character). Using the\n // whole word here would over-wrap a long CJK run that follows padding.\n const np = next.text.length > 1\n ? breakWordIfNeeded(ctx, next, effWidth(), 0)\n : [next];\n headExtra = np[0].width;\n }\n }\n\n // A soft-hyphen break point draws a visible '-' when the line breaks\n // right after this piece. Chrome only allows a break there if the prefix\n // PLUS the hyphen fits, so reserve the hyphen advance in the overflow\n // test — otherwise we pack one extra segment and the appended hyphen\n // overflows the line (breaking one segment later than the browser).\n let shReserve = 0;\n if (piece.isSoftHyphenBreak) {\n applyFont(ctx, piece.style);\n ctx.letterSpacing = formatLetterSpacing(piece.style.letterSpacing);\n shReserve = cachedMeasureWidth(ctx, '-');\n }\n\n // Would this piece overflow?\n if (!piece.isSpace && !isTrailingPunct && !isGlued && currentLine.words.length > 0 &&\n currentLine.totalWidth + piece.width + shReserve + tail + headExtra > effWidth()) {\n const overflow = currentLine.totalWidth + piece.width + shReserve + tail + headExtra - effWidth();\n\n // For borderline cases (overflow < 1px), word-by-word delta\n // accumulation may introduce rounding errors. Re-measure the\n // full candidate line as a single string for accuracy.\n // Only works for single-font lines — mixed fonts can't be\n // measured as one string.\n let reallyOverflows = true;\n if (overflow < 1 && !hasMixedFonts([...currentLine.words, piece])) {\n applyFont(ctx, piece.style);\n const fullText = currentLine.words.map(w => w.text).join('') + piece.text +\n (piece.isSoftHyphenBreak ? '-' : '');\n // Empty-text words carry non-glyph advance (inline padding/border\n // markers, inline-block margins) that measureText(fullText) misses —\n // add them back so padded inline spans aren't under-measured.\n let markerWidth = 0;\n for (const w of currentLine.words) if (!w.text) markerWidth += w.width;\n if (!piece.text) markerWidth += piece.width;\n const fullWidth = cachedMeasureWidth(ctx, fullText) + markerWidth + tail + headExtra;\n // Allow only a hair of sub-pixel overflow. measureText matches the\n // browser's rendered width to ~0.01px, so a larger slack would keep\n // lines the browser actually wraps (packing one extra word per\n // borderline line and drifting the whole document's breaks).\n if (fullWidth <= effWidth() + 0.02) {\n reallyOverflows = false;\n }\n }\n\n // Hyphen break on current line: before wrapping the whole word,\n // try fitting a hyphen prefix on the current line. Browsers prefer\n // keeping content on the current line by splitting at hyphens.\n if (reallyOverflows && piece.text.includes('-')) {\n const parts = piece.text.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/);\n if (parts.length > 1) {\n applyFont(ctx, piece.style);\n let fitted = '';\n let fittedWidth = 0;\n let partIdx = 0;\n const available = effWidth() - currentLine.totalWidth;\n for (; partIdx < parts.length; partIdx++) {\n const candidate = fitted + parts[partIdx];\n const candidateWidth = cachedMeasureWidth(ctx, candidate);\n if (candidateWidth > available) break;\n fitted = candidate;\n fittedWidth = candidateWidth;\n }\n if (partIdx > 0 && partIdx < parts.length) {\n currentLine.words.push({ ...piece, text: fitted, width: fittedWidth });\n currentLine.totalWidth += fittedWidth;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n pushLine(true);\n afterHardBreak = false;\n const remainder = parts.slice(partIdx).join('');\n const remainderWidth = cachedMeasureWidth(ctx, remainder);\n currentLine.words.push({ ...piece, text: remainder, width: remainderWidth });\n currentLine.totalWidth += remainderWidth;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n continue;\n }\n }\n }\n\n if (reallyOverflows) {\n if (_debug) {\n const lineText = currentLine.words.map(w => w.text).join('');\n _debug({\n type: 'line-wrap',\n message: `\"${piece.text}\" overflow=${overflow.toFixed(2)} wrap=true lineWidth=${currentLine.totalWidth.toFixed(2)} pieceWidth=${piece.width.toFixed(2)} contentWidth=${contentWidth} line=\"${lineText}\"`,\n data: { text: piece.text, overflow, lineWidth: currentLine.totalWidth, pieceWidth: piece.width, contentWidth, lineText },\n });\n }\n pushLine(true);\n afterHardBreak = false;\n }\n }\n\n // Skip leading spaces at the start of a line. Preserving modes\n // (pre/pre-wrap/break-spaces) keep them after hard breaks; collapsing\n // modes (normal/nowrap/pre-line) drop them in all cases.\n if (piece.isSpace && currentLine.words.length === 0\n && (!afterHardBreak || !preservesWhitespace)) continue;\n\n // Tab: advance to the next tab stop (stops measured from the content\n // edge). Chrome rule: when the next stop is closer than half a space\n // width, skip to the following stop (Blink Font::TabWidth).\n let pieceWidth = piece.width;\n if (piece.isTab) {\n const interval = tabMetrics?.interval || piece.width;\n const halfSpace = tabMetrics?.halfSpace ?? 0;\n const currentPos = (lines.length === 0 ? textIndent : 0) + currentLine.totalWidth;\n let advance = interval - (currentPos % interval);\n if (advance < halfSpace) advance += interval;\n pieceWidth = advance;\n piece.width = pieceWidth;\n }\n\n // Hyphen break on a fresh line when word still too wide.\n if (currentLine.words.length === 0 && pieceWidth > effWidth() &&\n !piece.isSpace && piece.text.includes('-')) {\n const subParts = piece.text.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/);\n if (subParts.length > 1) {\n applyFont(ctx, piece.style);\n // Inject sub-parts as individual pieces — they'll flow through\n // the normal overflow/wrap logic on subsequent iterations.\n const newPieces: Word[] = subParts.filter(p => p).map(p => ({\n ...piece,\n text: p,\n width: cachedMeasureWidth(ctx, p),\n }));\n // Replace current piece with the sub-parts by splicing into the pieces array\n // Since we're iterating `pieces`, we push remaining sub-parts after the first\n // onto the current line normally, letting the overflow check handle wrapping.\n let first = true;\n for (const sp of newPieces) {\n if (first) {\n first = false;\n // First sub-part: add to current line (it fits since it's smaller)\n currentLine.words.push(sp);\n currentLine.totalWidth += sp.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n } else if (currentLine.totalWidth + sp.width > effWidth()) {\n // Overflow: wrap to next line\n pushLine(true);\n afterHardBreak = false;\n currentLine.words.push(sp);\n currentLine.totalWidth += sp.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n } else {\n currentLine.words.push(sp);\n currentLine.totalWidth += sp.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n }\n }\n continue;\n }\n }\n\n currentLine.words.push(piece);\n currentLine.totalWidth += pieceWidth;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n if (!piece.isSpace) afterHardBreak = false;\n }\n }\n pushLine();\n return lines;\n}\n\n/**\n * Shared line budget for `-webkit-line-clamp` on a block container whose\n * text lives in block descendants (Chrome legacy `-webkit-box` semantics:\n * line boxes are counted across ALL descendants; the Nth line gets an\n * ellipsis and everything after it is dropped). Created in layoutBlock at\n * the clamped element and threaded through descendant layout calls.\n *\n * Known limitation: when the budget runs out exactly at a paragraph\n * boundary (Nth line is a paragraph's last line), following content is\n * dropped but the already-emitted Nth line gets no ellipsis — its layout\n * nodes were positioned before we learned more content follows.\n */\ninterface LineClampState {\n /** Line boxes still allowed before the cut. */\n remaining: number;\n /** Truncation point reached — all subsequent content is dropped. */\n exhausted: boolean;\n}\n\n/**\n * Layout inline content: text wrapping + positioning using pure canvas measurement.\n * Returns layout nodes and the total height consumed.\n */\nfunction layoutInlineContent(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n x: number,\n y: number,\n contentWidth: number,\n useBulletProbe = false,\n clamp?: LineClampState,\n): { nodes: LayoutNode[]; height: number } {\n const results: LayoutNode[] = [];\n // Text nodes covered by an inline element declaring background-clip:text\n // (clipRuns) or --rt-text-stroke-image (strokeImageRuns), mapped to that\n // declaring element's style. A post-pass turns each per-line run of\n // same-declarer nodes into a fragment-spanning paint box.\n const clipRuns = new Map<LayoutText, ResolvedStyle>();\n const strokeImageRuns = new Map<LayoutText, ResolvedStyle>();\n if (clamp && (clamp.exhausted || clamp.remaining <= 0)) {\n // An ancestor's clamp already used its line budget — drop this content.\n clamp.exhausted = true;\n return { nodes: results, height: 0 };\n }\n const runs = collectTextRuns(node);\n if (runs.length === 0) return { nodes: results, height: 0 };\n\n const words = tokenizeRuns(ctx, runs);\n const textIndent = node.style.textIndent || 0;\n // Tab stops follow the BLOCK's style, not the inline run the tab sits in:\n // Chrome sizes the interval as tab-size(8) × the block font's space advance\n // plus letter- and word-spacing (css-text-3 §tab-size) — verified against\n // the DOM: a tab inside a bold span still uses the regular-weight space.\n applyFont(ctx, node.style);\n const prevLetterSpacing = ctx.letterSpacing;\n ctx.letterSpacing = '0px';\n const blockSpaceWidth = cachedMeasureWidth(ctx, ' ');\n ctx.letterSpacing = prevLetterSpacing;\n const tabMetrics = {\n interval: (blockSpaceWidth + (node.style.letterSpacing || 0) + (node.style.wordSpacing || 0)) * 8,\n halfSpace: blockSpaceWidth / 2,\n };\n // The block's own font + line-height set the strut: the minimum height of\n // every line box, even a line holding only smaller inline content.\n const strutLineHeight = getLineHeight(ctx, node.style, useBulletProbe);\n const lines = flowWordsIntoLines(ctx, words, contentWidth, node.style.whiteSpace, useBulletProbe, textIndent, tabMetrics, strutLineHeight);\n\n // `-webkit-line-clamp` / `line-clamp`: truncate to N lines and append a\n // CSS-style ellipsis (\"…\") to the Nth line, back-trimming trailing words\n // until the ellipsis fits within contentWidth. The budget comes from an\n // ancestor's shared clamp state when one is active (clamp on a block\n // container with block children), else from this element's own style.\n const clampN = clamp ? clamp.remaining : node.style.lineClamp;\n if (clampN > 0 && lines.length > clampN) {\n lines.length = clampN;\n const lastLine = lines[clampN - 1];\n // First line has reduced width because of text-indent; a cut on this\n // element's first line (effective budget of 1) hits it.\n const lineMaxForEllipsis = contentWidth - (clampN === 1 ? textIndent : 0);\n applyEllipsisToLine(ctx, lastLine, lineMaxForEllipsis);\n // Tag the truncated line so per-line alignment (text-align vs\n // text-align-last) still picks the right branch.\n lastLine.endedByHardBreak = true;\n if (clamp) {\n clamp.remaining = 0;\n clamp.exhausted = true;\n }\n } else if (clamp) {\n clamp.remaining -= lines.length;\n }\n\n const isRTL = node.style.direction === 'rtl';\n const resolveDir = (a: string) => {\n if (a === 'start') return isRTL ? 'right' : 'left';\n if (a === 'end') return isRTL ? 'left' : 'right';\n return a;\n };\n let textAlign = resolveDir(node.style.textAlign);\n // text-align-last: 'auto' inherits from text-align except when text-align is\n // 'justify', then defaults to 'start' (CSS Text 3 §7.2).\n let textAlignLast = node.style.textAlignLast || 'auto';\n if (textAlignLast === 'auto') {\n textAlignLast = node.style.textAlign === 'justify' ? (isRTL ? 'right' : 'left') : textAlign;\n } else {\n textAlignLast = resolveDir(textAlignLast);\n }\n\n // The block strut also participates in the line's baseline, not just its\n // height: inline content aligns to the block-font baseline, so a line whose\n // only content is a SMALLER inline font sits on the strut baseline (lower in\n // the box), not centered in it. Seed each line's ascent/descent with the\n // block font's metrics so the baseline lands where the DOM puts it.\n // The block is the parent of any run with no inline ancestor, and the emit\n // loop shadows `node` with the LayoutText it builds.\n const blockStyle = node.style;\n\n let curY = y;\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx];\n if (line.words.length === 0) {\n curY += line.lineHeight;\n continue;\n }\n\n const isLastLine = lineIdx === lines.length - 1;\n const isFirstLine = lineIdx === 0;\n\n // Per-line alignment: lines ending at a forced break or the last line\n // use text-align-last; all others use text-align (CSS Text 3 §7.1, §7.2).\n const useLast = isLastLine || line.endedByHardBreak;\n const align = useLast ? textAlignLast : textAlign;\n\n // text-indent narrows the first line's available width.\n const indent = isFirstLine ? textIndent : 0;\n const lineMaxWidth = contentWidth - indent;\n\n // Justify: expand spaces to fill the line.\n let justifyExtraPerSpace = 0;\n if (align === 'justify' && line.totalWidth < lineMaxWidth) {\n const spaceCount = line.words.filter(w => w.isSpace).length;\n if (spaceCount > 0) {\n justifyExtraPerSpace = (lineMaxWidth - line.totalWidth) / spaceCount;\n }\n }\n\n // text-align (with first-line indent baked into curX).\n // When the line overflows its container, browsers fall back to start\n // alignment (per CSS Text 3 §7.1) instead of pushing the line outside\n // the box. Common trigger: wide letter-spacing on text that doesn't\n // wrap at letter boundaries (no break-word/break-all), where centering\n // would put glyphs at negative x. Sub-pixel tolerance avoids switching\n // to start for rounding noise on lines that visually fit.\n // Start edge differs by direction. LTR lines start at the left (x+indent).\n // RTL lines are anchored at the right, inset from the content's right edge\n // by text-indent — and lineMaxWidth already subtracts indent, so the RTL\n // right edge is x+lineMaxWidth. `align` here is physically resolved\n // (start/end → left/right via resolveDir), so RTL with align==='left'\n // (explicit left, or end) correctly falls through to left alignment.\n const overflows = line.totalWidth > lineMaxWidth + 0.5;\n let curX = x + indent;\n if (overflows) {\n // Overflow fallback: pin to the start edge (CSS Text 3 §7.1).\n curX = isRTL ? x + lineMaxWidth - line.totalWidth : x + indent;\n } else if (align === 'center') {\n curX = x + indent + (lineMaxWidth - line.totalWidth) / 2;\n } else if (align === 'right') {\n curX = (isRTL ? x + lineMaxWidth : x + indent + lineMaxWidth) - line.totalWidth;\n } else if (align === 'justify' && isRTL) {\n // RTL justify: anchor the right edge at the inset start; spaces expand left.\n curX = x + lineMaxWidth - line.totalWidth;\n }\n // Snapshot the line's left edge before LTR emission advances curX.\n const lineLeftX = curX;\n\n // Inline background boxes and text are emitted after baseline computation\n // (below) so that emitInlineBox can use line-level metrics for alignment.\n\n // The line box is the union of every box on it — strut, run, shifted run,\n // inline-block — each carrying its own leading over its own line-height:\n // lineAscent = max(ascent - shift), lineDescent = max(descent + shift).\n // One font, one line-height and no shift collapse that back to the plain\n // half-leading every single-style line already had.\n const strutBox = leadedBox(ctx, node.style, useBulletProbe);\n let lineAscent = strutBox.ascent;\n let lineDescent = strutBox.descent;\n for (const word of line.words) {\n if (word.text === '') continue;\n // A wrapper element with no text of its own — `<span lh:3><span>x</span>`\n // — never becomes a Word, but it is still a box on the line and still\n // brings its own line-height. Its run children carry it as `parentStyle`,\n // so take it from there, AT ITS OWN SHIFT: added unshifted, a wrapper\n // that carries a vertical-align and direct text enters the union twice\n // at two different places, and the line spans both (measured 60px where\n // the DOM has 40). With the shift it is idempotent — a wrapper with\n // direct text contributes the identical box through its own run.\n if (word.parentStyle) {\n const parentBox = leadedBox(ctx, word.parentStyle, useBulletProbe);\n const parentShift = verticalAlignShift(\n word.parentStyle.verticalAlign, ctx, word.parentStyle, blockStyle, useBulletProbe);\n if (parentBox.ascent - parentShift > lineAscent) {\n lineAscent = parentBox.ascent - parentShift;\n }\n if (parentBox.descent + parentShift > lineDescent) {\n lineDescent = parentBox.descent + parentShift;\n }\n }\n const box = leadedBox(ctx, word.style, useBulletProbe);\n // An inline-block joins the line as an ATOMIC box: its own content\n // baseline with its margin box stacked around it. It takes the extra\n // space, but no shift — the emit pass puts its content on the line\n // baseline and does not honour vertical-align on it, so shifting the box\n // here would grow the line one way while the paint went the other.\n const atomic = word.boxStyle?.display === 'inline-block' ? word.boxStyle : null;\n if (atomic) {\n const extra = inlineBlockExtra(atomic);\n box.ascent += extra.top;\n box.descent += extra.bottom;\n }\n // A shift moves the box, not the line's baseline: positive is downward,\n // so it lifts the box's demand on the ascent side and adds to the descent\n // side. The parent-size fallback is the one the emit pass uses, so a line\n // whose only content is shifted still sizes around it.\n const shift = atomic ? 0 : verticalAlignShift(\n word.style.verticalAlign, ctx, word.style,\n word.parentStyle ?? blockStyle, useBulletProbe);\n if (box.ascent - shift > lineAscent) lineAscent = box.ascent - shift;\n if (box.descent + shift > lineDescent) lineDescent = box.descent + shift;\n }\n const lineBoxHeight = lineAscent + lineDescent;\n const lineBaselineY = curY + lineAscent;\n\n // Emit inline background box using line-level baseline for vertical alignment.\n // Uses the line's ascent/descent (not the box's own font) so box aligns with text.\n const emitInlineBox = (style: ResolvedStyle, bx: number, bw: number) => {\n // The box's OWN font decides its height, not the line's largest. An\n // inline-block's content box is its LINE-HEIGHT, though, not the bare\n // font metrics — measured against Chrome, bare metrics put it at\n // y=6 h=29 where the DOM has y=4 h=33.2.\n const { ascent: boxAscent, descent: boxDescent } =\n style.display === 'inline-block'\n ? leadedBox(ctx, style, useBulletProbe)\n : getFontMetrics(ctx, style);\n const padTop = style.paddingTop + style.borderTopWidth;\n const padBottom = style.paddingBottom + style.borderBottomWidth;\n const boxHeight = boxAscent + boxDescent + padTop + padBottom;\n // Every inline box hangs off the line's baseline, an inline-block too:\n // its content is emitted on that baseline, so a box pinned to the line\n // TOP instead detached from its own glyphs as soon as something taller\n // shared the line — measured, a background at y 4..33 around text whose\n // baseline was 46.\n const boxY = lineBaselineY - boxAscent - padTop;\n results.push({\n type: 'box', style, x: bx, y: boxY, width: bw, height: boxHeight,\n tagName: 'span', children: [],\n });\n };\n\n // LTR: emit inline background boxes (Pass 1) before text.\n if (!isRTL) {\n let scanX = curX;\n let boxStartX = scanX;\n let currentBoxStyle: ResolvedStyle | undefined;\n let boxHasText = false;\n\n for (const word of line.words) {\n if (word.boxOpen && word.boxClose && word.text) {\n if (currentBoxStyle) {\n if (boxHasText) emitInlineBox(currentBoxStyle, boxStartX, scanX - boxStartX);\n currentBoxStyle = undefined;\n boxHasText = false;\n }\n const s = word.style;\n const textWidth = word.width - s.marginLeft - s.borderLeftWidth - s.paddingLeft\n - s.paddingRight - s.borderRightWidth - s.marginRight;\n const boxX = scanX + s.marginLeft;\n const boxW = s.borderLeftWidth + s.paddingLeft + textWidth + s.paddingRight + s.borderRightWidth;\n emitInlineBox(s, boxX, boxW);\n boxHasText = false;\n scanX += word.width;\n continue;\n }\n\n if (word.boxStyle !== currentBoxStyle) {\n if (currentBoxStyle && boxHasText) {\n emitInlineBox(currentBoxStyle, boxStartX, scanX - boxStartX);\n }\n currentBoxStyle = word.boxStyle;\n boxStartX = scanX;\n boxHasText = false;\n }\n if (word.text && !word.isSpace) boxHasText = true;\n scanX += word.width + (word.isSpace ? justifyExtraPerSpace : 0);\n }\n if (currentBoxStyle && boxHasText) {\n emitInlineBox(currentBoxStyle, boxStartX, scanX - boxStartX);\n }\n }\n\n // Emit text nodes.\n const textWords = line.words.filter(w => w.text !== '');\n const allSameStyle = textWords.length > 0 && textWords.every(w =>\n sameTextStyle(w.style, textWords[0].style)\n );\n\n if (isRTL) {\n // RTL: build groups, compute positions, emit boxes then text.\n // Groups join consecutive same-style words for proper glyph shaping.\n // Padding markers between groups create spacing.\n interface StyledGroup {\n text: string; style: ResolvedStyle; width: number;\n boxStyle?: ResolvedStyle; clipStyle?: ResolvedStyle;\n strokeImageStyle?: ResolvedStyle; x: number;\n padBefore: number; // padding before this group (from boxOpen/boxClose markers)\n }\n const groups: StyledGroup[] = [];\n let currentGroup: StyledGroup | null = null;\n let pendingPad = 0;\n\n for (const word of line.words) {\n if (word.text === '') {\n // Padding marker — accumulate for the next group boundary\n if (currentGroup) { groups.push(currentGroup); currentGroup = null; }\n pendingPad += word.width;\n continue;\n }\n if (word.isSpace && justifyExtraPerSpace > 0) {\n // Justify only: break the shaping group at the space and fold the\n // expansion into the inter-group advance so the line fills the width.\n // (Arabic does not join across spaces, so this is shaping-safe.)\n // When not justifying, spaces stay merged into the group text below\n // so the canvas BiDi engine can reorder embedded LTR runs/numbers.\n if (currentGroup) { groups.push(currentGroup); currentGroup = null; }\n pendingPad += word.width + justifyExtraPerSpace;\n continue;\n }\n if (currentGroup && sameTextStyle(currentGroup.style, word.style)) {\n currentGroup.text += word.text;\n currentGroup.width += word.width;\n } else {\n if (currentGroup) groups.push(currentGroup);\n currentGroup = { text: word.text, style: word.style, width: word.width, boxStyle: word.boxStyle, clipStyle: word.clipStyle, strokeImageStyle: word.strokeImageStyle, x: 0, padBefore: pendingPad };\n pendingPad = 0;\n }\n }\n if (currentGroup) groups.push(currentGroup);\n\n // Compute positions right-to-left: group-level measureText for accuracy,\n // with padding markers creating spacing between groups.\n let rtlX = curX + line.totalWidth;\n for (const group of groups) {\n rtlX -= group.padBefore; // spacing from padding markers\n applyFont(ctx, group.style);\n const measuredWidth = cachedMeasureWidth(ctx, group.text);\n rtlX -= measuredWidth;\n group.x = rtlX;\n group.width = measuredWidth;\n }\n\n // Emit inline boxes first (behind text).\n // Include padding/border from boxStyle in box dimensions.\n for (const group of groups) {\n if (group.boxStyle && hasVisibleBoxStyles(group.boxStyle)) {\n const bs = group.boxStyle;\n const padLeft = bs.paddingLeft + bs.borderLeftWidth;\n const padRight = bs.paddingRight + bs.borderRightWidth;\n emitInlineBox(bs, group.x - padLeft, group.width + padLeft + padRight);\n }\n }\n\n // Emit text groups\n for (const group of groups) {\n const node: LayoutText = {\n type: 'text',\n text: group.text,\n x: group.x + group.width, // x = right edge for RTL textAlign\n y: lineBaselineY,\n width: group.width,\n style: { ...group.style, direction: 'rtl' },\n };\n results.push(node);\n if (group.clipStyle) clipRuns.set(node, group.clipStyle);\n if (group.strokeImageStyle) strokeImageRuns.set(node, group.strokeImageStyle);\n }\n } else {\n // LTR with mixed BiDi scripts: emit the entire line as one fillText call\n // so the canvas engine handles BiDi reordering (Arabic/Hebrew in LTR).\n // Only do this when the line contains RTL characters — pure LTR lines\n // are more accurate with word-by-word positioning.\n const lineText = line.words.map(w => w.text).join('');\n const hasBidiMix = allSameStyle && /[\\u0590-\\u08FF\\uFB50-\\uFDFF\\uFE70-\\uFEFF]/.test(lineText) &&\n !line.words.some(w => w.boxOpen || w.boxClose ||\n w.style.verticalAlign === 'super' || w.style.verticalAlign === 'sub');\n if (hasBidiMix) {\n applyFont(ctx, textWords[0].style);\n const measuredWidth = cachedMeasureWidth(ctx, lineText);\n // This line belongs to an LTR block (we're in the !isRTL branch), so it\n // must be painted with an LTR base direction even when its first word is\n // RTL (an RTL span that wrapped onto this line). Without forcing LTR the\n // node inherits the first word's direction:'rtl' and the paint path\n // right-aligns the whole line at the left edge (x=curX), drawing it\n // off-screen. The canvas BiDi engine still reorders the embedded\n // Arabic/Hebrew runs within the LTR line.\n const node: LayoutText = {\n type: 'text',\n text: lineText,\n x: curX,\n y: lineBaselineY,\n width: measuredWidth,\n style: { ...textWords[0].style, direction: 'ltr' },\n };\n results.push(node);\n if (textWords[0].clipStyle) clipRuns.set(node, textWords[0].clipStyle);\n if (textWords[0].strokeImageStyle) strokeImageRuns.set(node, textWords[0].strokeImageStyle);\n } else {\n // Mixed styles: word by word\n for (const word of line.words) {\n if (word.text === '') {\n curX += word.width;\n continue;\n }\n\n // Atomic inline-block: position text inside the box (after margin + padding)\n if (word.boxOpen && word.boxClose) {\n const s = word.style;\n const textX = curX + s.marginLeft + s.borderLeftWidth + s.paddingLeft;\n const node: LayoutText = {\n type: 'text',\n text: word.text,\n x: textX,\n y: lineBaselineY,\n width: cachedMeasureWidth(ctx, word.text),\n style: word.style,\n };\n results.push(node);\n if (word.clipStyle) clipRuns.set(node, word.clipStyle);\n if (word.strokeImageStyle) strokeImageRuns.set(node, word.strokeImageStyle);\n curX += word.width;\n continue;\n }\n\n // Adjust baseline for vertical-align\n let baselineY = lineBaselineY;\n const va = word.style.verticalAlign;\n if (isShiftedVAlign(va)) {\n baselineY += verticalAlignShift(\n va, ctx, word.style, word.parentStyle ?? blockStyle, useBulletProbe);\n }\n const effectiveWidth = word.width + (word.isSpace ? justifyExtraPerSpace : 0);\n\n const node: LayoutText = {\n type: 'text',\n text: word.text,\n x: curX,\n y: baselineY,\n width: effectiveWidth,\n style: word.style,\n // Only when vertical-align moved this run off the line — an\n // underline from an unshifted declarer still hangs off the line.\n ...(baselineY !== lineBaselineY ? { lineBaselineY } : {}),\n };\n results.push(node);\n if (word.clipStyle) clipRuns.set(node, word.clipStyle);\n if (word.strokeImageStyle) strokeImageRuns.set(node, word.strokeImageStyle);\n\n curX += effectiveWidth;\n }\n }\n }\n\n // Emit a public LayoutLine record for this committed line.\n // bounds.width: justified lines fill lineMaxWidth (spaces expanded);\n // others use the measured words width.\n const lineWidth =\n align === 'justify' && justifyExtraPerSpace > 0\n ? lineMaxWidth\n : line.totalWidth;\n _lines.push({\n y: Math.round(lineBaselineY),\n text: line.words.map(w => w.text).join(''),\n bounds: {\n x: lineLeftX,\n // The line box starts at curY: shifted content grew the box through\n // `lineAscent`/`lineDescent` above, so nothing on the line reaches\n // outside it.\n y: curY,\n width: lineWidth,\n height: lineBoxHeight,\n },\n });\n\n curY += lineBoxHeight;\n }\n\n assignInlineFragmentBoxes(ctx, results, clipRuns, (node, s, box) => {\n node.clip = {\n image: s.backgroundImage && s.backgroundImage !== 'none' ? s.backgroundImage : undefined,\n color: !isTransparent(s.backgroundColor) ? s.backgroundColor : undefined,\n ...box,\n };\n });\n assignInlineFragmentBoxes(ctx, results, strokeImageRuns, (node, s, box) => {\n node.strokeImage = { image: s.webkitTextStrokeImage, ...box };\n });\n\n return { nodes: results, height: curY - y };\n}\n\n/**\n * Give each text run covered by an inline paint declarer (background-clip:text\n * background, --rt-text-stroke-image) a paint box spanning the declaring\n * element's fragment on its line.\n *\n * Browsers paint the declaring element's background over its inline fragment\n * (the run of glyphs it covers on one line) and clip it to the text; with\n * `background-size:100% 100%` the gradient fills that fragment box. Consecutive\n * text nodes sharing the same declaring element (same style object) on the\n * same baseline form one fragment; a wrap to the next line starts a new one\n * (box-decoration-break:clone semantics — Chrome's default `slice` continues\n * the gradient across line fragments; accepted approximation), and unlike a\n * per-run gradient it never restarts per word.\n */\nfunction assignInlineFragmentBoxes(\n ctx: CanvasRenderingContext2D,\n results: LayoutNode[],\n runs: Map<LayoutText, ResolvedStyle>,\n assign: (\n node: LayoutText,\n declarer: ResolvedStyle,\n box: { x: number; y: number; width: number; height: number },\n ) => void,\n): void {\n if (runs.size === 0) return;\n const edges = (n: LayoutText) =>\n n.style.direction === 'rtl'\n ? { left: n.x - n.width, right: n.x } // RTL x is the right edge\n : { left: n.x, right: n.x + n.width };\n for (let i = 0; i < results.length;) {\n const first = results[i];\n const declarer = first.type === 'text' ? runs.get(first) : undefined;\n if (!declarer) { i++; continue; }\n let j = i;\n let left = Infinity, right = -Infinity;\n while (j < results.length) {\n const n = results[j];\n if (n.type !== 'text' || runs.get(n) !== declarer || n.y !== first.y) break;\n const e = edges(n);\n if (e.left < left) left = e.left;\n if (e.right > right) right = e.right;\n j++;\n }\n const { ascent, descent } = getFontMetrics(ctx, declarer);\n const box = {\n x: left,\n y: first.y - ascent,\n width: right - left,\n height: ascent + descent,\n };\n for (let k = i; k < j; k++) assign(results[k] as LayoutText, declarer, box);\n i = j;\n }\n}\n\n// ─── Block layout ──────────────────────────────────────────────────────\n\n/**\n * Collapse margins between two adjacent block elements.\n * Returns the effective spacing (max of the two margins, not sum).\n */\nfunction collapseMargins(prevMarginBottom: number, nextMarginTop: number): number {\n // Both positive: take the larger\n if (prevMarginBottom >= 0 && nextMarginTop >= 0) {\n return Math.max(prevMarginBottom, nextMarginTop);\n }\n // Both negative: take the more negative\n if (prevMarginBottom < 0 && nextMarginTop < 0) {\n return Math.min(prevMarginBottom, nextMarginTop);\n }\n // One positive, one negative: sum them\n return prevMarginBottom + nextMarginTop;\n}\n\n/**\n * Check if a node is a block-level display.\n */\nfunction isBlock(node: StyledNode): boolean {\n const d = node.style.display;\n return d === 'block' || d === 'list-item' || d === 'flex' || d === 'table' ||\n d === 'table-row' || d === 'table-cell' || d === 'table-row-group' ||\n d === 'table-header-group' || d === 'table-footer-group';\n}\n\n/**\n * Layout a block-level element and all its children.\n * Returns the LayoutBox and total height consumed (including margins).\n */\nfunction layoutBlock(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n x: number,\n y: number,\n availableWidth: number,\n clamp?: LineClampState,\n): { box: LayoutBox; height: number; marginBottomOut: number } {\n const style = node.style;\n\n // `-webkit-line-clamp` on a block container: start a shared line budget\n // here and thread it through descendant layout so the count spans block\n // children (Chrome legacy -webkit-box semantics). An ancestor's active\n // clamp wins over a nested one.\n if (!clamp && style.lineClamp > 0) {\n clamp = { remaining: style.lineClamp, exhausted: false };\n }\n\n // Box model\n const marginLeft = style.marginLeft;\n const marginRight = style.marginRight;\n const borderLeft = style.borderLeftWidth;\n const borderRight = style.borderRightWidth;\n const borderTop = style.borderTopWidth;\n const borderBottom = style.borderBottomWidth;\n const padLeft = style.paddingLeft;\n const padRight = style.paddingRight;\n const padTop = style.paddingTop;\n const padBottom = style.paddingBottom;\n\n const boxX = x + marginLeft;\n // If element has explicit width, use it; otherwise fill available width\n const boxWidth = (style.width > 0)\n ? style.width\n : availableWidth - marginLeft - marginRight;\n const contentX = boxX + borderLeft + padLeft;\n const contentWidth = Math.max(0, boxWidth - borderLeft - borderRight - padLeft - padRight);\n\n const boxY = y;\n const contentStartY = boxY + borderTop + padTop;\n\n const box: LayoutBox = {\n type: 'box',\n style,\n x: boxX,\n y: boxY,\n width: boxWidth,\n height: 0, // computed below\n tagName: node.tagName,\n children: [],\n listMarker: node.listMarker,\n };\n\n // Flex layout\n if (style.display === 'flex') {\n const result = layoutFlex(ctx, node, contentX, contentStartY, contentWidth);\n box.children = result.children;\n box.height = borderTop + padTop + result.height + padBottom + borderBottom;\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n }\n\n // Table layout\n if (style.display === 'table') {\n const result = layoutTable(ctx, node, contentX, contentStartY, contentWidth);\n box.children = result.children;\n box.height = borderTop + padTop + result.height + padBottom + borderBottom;\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n }\n\n // Empty block elements: zero content height (CSS spec — no line boxes created).\n // Only min-height or padding/border contribute to height.\n if (node.children.length === 0) {\n box.height = borderTop + padTop + padBottom + borderBottom;\n if (style.minHeight > 0) box.height = Math.max(box.height, style.minHeight);\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n }\n\n // Layout children\n if (hasOnlyInlineChildren(node)) {\n // Inline formatting context\n const bulletProbe = node.tagName === 'li' && BULLET_MARKERS.has(style.listStyleType);\n const { nodes, height } = layoutInlineContent(ctx, node, contentX, contentStartY, contentWidth, bulletProbe, clamp);\n box.children = nodes;\n box.height = borderTop + padTop + height + padBottom + borderBottom;\n } else {\n // Block formatting context — stack children vertically\n let curY = contentStartY;\n let prevMarginBottom = 0;\n let hasContent = false; // tracks whether we've placed any content\n // Margin collapsing through parent: only for list elements.\n const allowCollapseThrough =\n node.tagName === 'li' || node.tagName === 'ul' || node.tagName === 'ol' ||\n node.tagName === 'dd' || node.tagName === 'dt';\n\n for (let ci = 0; ci < node.children.length; ci++) {\n const child = node.children[ci];\n\n // Line-clamp budget exhausted — everything below the cut is dropped,\n // including the margin trailing the cut line.\n if (clamp && (clamp.exhausted || clamp.remaining <= 0)) {\n clamp.exhausted = true;\n prevMarginBottom = 0;\n break;\n }\n\n if (child.tagName === '#text' || isInline(child)) {\n // Collect ALL consecutive inline/text children into one group\n const inlineChildren: StyledNode[] = [child];\n while (ci + 1 < node.children.length) {\n const next = node.children[ci + 1];\n if (next.tagName === '#text' || isInline(next)) {\n inlineChildren.push(next);\n ci++;\n } else {\n break;\n }\n }\n\n // Apply pending margin before inline content\n if (prevMarginBottom > 0) {\n curY += prevMarginBottom;\n prevMarginBottom = 0;\n }\n\n const inlineGroup: StyledNode = {\n element: null,\n tagName: 'div',\n style: { ...node.style, display: 'block', marginTop: 0, marginBottom: 0, paddingTop: 0, paddingBottom: 0, borderTopWidth: 0, borderBottomWidth: 0 },\n children: inlineChildren,\n textContent: null,\n };\n const bulletProbe2 = node.tagName === 'li' && BULLET_MARKERS.has(style.listStyleType);\n const { nodes, height } = layoutInlineContent(ctx, inlineGroup, contentX, curY, contentWidth, bulletProbe2, clamp);\n box.children.push(...nodes);\n curY += height;\n prevMarginBottom = 0;\n hasContent = true;\n continue;\n }\n\n // Block child — collapse margins\n const childMarginTop = child.style.marginTop;\n\n // First child margin-top collapses through parent if parent has no top border/padding\n // Only for elements that don't establish a new BFC (not root, not flex, not overflow)\n // First child margin-top collapses through parent if parent has no\n // top padding/border and doesn't establish a new BFC.\n if (!hasContent && padTop === 0 && borderTop === 0 && allowCollapseThrough) {\n // Skip — margin collapses with parent's margin\n } else {\n const collapsed = collapseMargins(prevMarginBottom, childMarginTop);\n curY += collapsed;\n }\n\n const { box: childBox, height: childTotalHeight, marginBottomOut } = layoutBlock(\n ctx, child, contentX, curY, contentWidth, clamp,\n );\n box.children.push(childBox);\n curY += childTotalHeight;\n // A child truncated by line-clamp clips its trailing margin too.\n prevMarginBottom = clamp?.exhausted ? 0 : marginBottomOut;\n hasContent = true;\n }\n\n // Last child's margin-bottom collapses through parent if no bottom border/padding.\n // Root container does NOT collapse last-child margin (it defines the content height).\n let marginBottomOut = style.marginBottom;\n const canCollapseThrough = padBottom === 0 && borderBottom === 0 && allowCollapseThrough;\n if (canCollapseThrough && prevMarginBottom > 0) {\n // Last child's margin passes through to become parent's effective margin-bottom\n marginBottomOut = Math.max(style.marginBottom, prevMarginBottom);\n }\n\n // Include last child's margin-bottom in parent height when it can't collapse through\n let contentEnd = curY - contentStartY;\n if (!canCollapseThrough && prevMarginBottom > 0) {\n contentEnd += prevMarginBottom;\n }\n box.height = borderTop + padTop + contentEnd + padBottom + borderBottom;\n if (style.minHeight > 0) box.height = Math.max(box.height, style.minHeight);\n return { box, height: box.height, marginBottomOut };\n }\n\n if (style.minHeight > 0) box.height = Math.max(box.height, style.minHeight);\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n}\n\n// ─── Table layout ──────────────────────────────────────────────────────\n\nfunction layoutTable(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n contentX: number,\n contentY: number,\n contentWidth: number,\n): { children: LayoutNode[]; height: number } {\n const children: LayoutNode[] = [];\n\n // Collect rows from thead, tbody, tfoot, or direct tr children\n const rows: StyledNode[] = [];\n for (const child of node.children) {\n if (child.tagName === 'tr') {\n rows.push(child);\n } else if (['thead', 'tbody', 'tfoot'].includes(child.tagName)) {\n for (const grandchild of child.children) {\n if (grandchild.tagName === 'tr') rows.push(grandchild);\n }\n }\n }\n\n if (rows.length === 0) return { children, height: 0 };\n\n // Determine column count from first row\n const colCount = Math.max(...rows.map(r => r.children.filter(c => c.tagName === 'td' || c.tagName === 'th').length));\n if (colCount === 0) return { children, height: 0 };\n\n // Equal column widths (simple approach)\n const colWidth = contentWidth / colCount;\n\n let curY = contentY;\n\n for (const row of rows) {\n const cells = row.children.filter(c => c.tagName === 'td' || c.tagName === 'th');\n let maxCellHeight = 0;\n const cellBoxes: LayoutBox[] = [];\n\n for (let i = 0; i < cells.length; i++) {\n const cell = cells[i];\n const cellX = contentX + i * colWidth;\n\n const { box: cellBox, height: cellHeight } = layoutBlock(ctx, cell, cellX, curY, colWidth);\n cellBoxes.push(cellBox);\n maxCellHeight = Math.max(maxCellHeight, cellHeight);\n }\n\n // Normalize cell heights to the tallest cell in the row\n for (const cellBox of cellBoxes) {\n cellBox.height = maxCellHeight;\n children.push(cellBox);\n }\n\n curY += maxCellHeight;\n }\n\n return { children, height: curY - contentY };\n}\n\n// ─── Flex layout ───────────────────────────────────────────────────────\n\nfunction layoutFlex(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n contentX: number,\n contentY: number,\n contentWidth: number,\n): { children: LayoutNode[]; height: number } {\n const style = node.style;\n const gap = style.gap;\n const children: LayoutNode[] = [];\n\n const flexChildren = node.children.filter(c => c.tagName !== '#text' || c.textContent?.trim());\n if (flexChildren.length === 0) return { children, height: 0 };\n\n if (style.flexDirection === 'row' || style.flexDirection === '') {\n // Row layout\n const totalGaps = gap * (flexChildren.length - 1);\n const totalGrow = flexChildren.reduce((s, c) => s + (c.style.flexGrow || 0), 0);\n const flexBasis = (contentWidth - totalGaps) / (totalGrow || flexChildren.length);\n\n let curX = contentX;\n let maxHeight = 0;\n\n for (const child of flexChildren) {\n if (child.tagName === '#text') continue;\n const grow = child.style.flexGrow || (totalGrow === 0 ? 1 : 0);\n const childWidth = flexBasis * grow;\n\n const { box, height } = layoutBlock(ctx, child, curX, contentY, childWidth);\n children.push(box);\n maxHeight = Math.max(maxHeight, height);\n curX += childWidth + gap;\n }\n\n return { children, height: maxHeight };\n }\n\n // Column layout (fallback)\n let curY = contentY;\n for (const child of flexChildren) {\n if (child.tagName === '#text') continue;\n const { box, height } = layoutBlock(ctx, child, contentX, curY, contentWidth);\n children.push(box);\n curY += height + gap;\n }\n return { children, height: curY - contentY };\n}\n\n// ─── List marker layout ────────────────────────────────────────────────\n\n/**\n * Add list marker to a layout box if applicable.\n */\nfunction addListMarker(\n ctx: CanvasRenderingContext2D,\n box: LayoutBox,\n node: StyledNode,\n): void {\n if (!node.listMarker) return;\n // `::marker { content: none }` suppresses the marker entirely —\n // canonical CSS behavior, matches the DOM reference.\n if (node.markerHidden) return;\n\n const style = node.style;\n // Marker style = li style with explicit `::marker` overrides applied on top.\n // `markerStyle` holds only keys explicitly set by `::marker` rules, so a\n // missing key falls back to the li style. A present key (incl. 0) wins.\n const ms = node.markerStyle;\n const markerStyleObj: ResolvedStyle = ms ? { ...style, ...ms } : style;\n\n ctx.font = buildCanvasFont(markerStyleObj);\n // ascent + descent is the li's line-height by construction, so one call\n // gives both the marker's baseline and the box it reports.\n const strut = leadedBox(ctx, style);\n const baselineY = box.y + style.borderTopWidth + style.paddingTop + strut.ascent;\n\n const markerWidth = cachedMeasureWidth(ctx, node.listMarker);\n const isRTL = style.direction === 'rtl';\n const isBullet = BULLET_MARKERS.has(style.listStyleType);\n // Gap between marker and content, matching Chrome (measured empirically):\n // - bullets: Chrome paints a symbol (diameter ascent/3) whose ink ends\n // 7px + ascent/3 before the content edge, centered ascent/3 above the\n // baseline. We keep the glyph but position its ink to land there.\n // - text markers (\"1.\"): Chrome's marker text carries a \". \" suffix, so\n // the gap is one space advance and the baseline is the line baseline.\n // `::marker { padding-inline-end: <length> }` overrides the gap — we honor\n // the direction-resolved physical padding (paddingRight in LTR, paddingLeft\n // in RTL) when explicitly set on the marker.\n const explicitGap = isRTL ? ms?.paddingLeft : ms?.paddingRight;\n\n let markerX: number;\n let markerY = baselineY;\n let markerDirection = 'ltr';\n // Style/width the marker glyph is actually DRAWN with. Numbers draw at the\n // li font (unchanged); bullets scale up (see below), so keep these separate.\n let markerDrawStyle: ResolvedStyle = markerStyleObj;\n let markerDrawWidth = markerWidth;\n const contentStartX = box.x + style.borderLeftWidth + style.paddingLeft;\n const boxRightEdge = box.x + box.width;\n if (isBullet) {\n const { ascent } = getFontMetrics(ctx, markerStyleObj);\n const m = ctx.measureText(node.listMarker);\n // Blink's marker unit: the disc DIAMETER, the variable part of the gap, and\n // the vertical centering all key off this one value (a 2/3·ascent marker\n // box with a half-filling disc → ascent/3). Named once so tuning one keeps\n // the trio in sync.\n const markerUnit = ascent / 3;\n const gap = explicitGap !== undefined ? explicitGap : 7 + markerUnit;\n // Chrome paints bullet symbols (disc/circle/square) as a SYNTHETIC shape of\n // that diameter, NOT the font's smaller '•'/'○'/'■' glyph (Roboto's '•' ink\n // is ~0.22em vs Chrome's ~0.31em disc). Match it by scaling the glyph so its\n // ink height equals markerUnit. Keeping the marker a text node means fill /\n // stroke / shadow / gradient still apply exactly as before.\n const inkH = (m.actualBoundingBoxAscent ?? 0) + (m.actualBoundingBoxDescent ?? 0);\n const scale = inkH > 0 ? markerUnit / inkH : 1;\n const inkRight = (m.actualBoundingBoxRight ?? markerWidth) * scale;\n const inkLeft = (m.actualBoundingBoxLeft ?? 0) * scale;\n const glyphInkCenter =\n (((m.actualBoundingBoxAscent ?? 0) - (m.actualBoundingBoxDescent ?? 0)) / 2) * scale;\n if (isRTL) {\n // actualBoundingBoxLeft is positive when ink extends left of origin\n markerX = boxRightEdge + gap + inkLeft;\n } else {\n markerX = contentStartX - gap - inkRight;\n }\n markerY = baselineY - markerUnit + glyphInkCenter;\n markerDrawStyle = { ...markerStyleObj, fontSize: markerStyleObj.fontSize * scale };\n markerDrawWidth = markerWidth * scale;\n } else {\n const gap = explicitGap !== undefined\n ? explicitGap\n : cachedMeasureWidth(ctx, ' ');\n if (isRTL) {\n // RTL: marker in the parent's right padding area (outside the li box).\n // Numbered markers (\"1.\") need RTL direction to display as \".1\".\n // With textAlign='right', x is the right edge — so add markerWidth.\n const isNumbered = /\\d/.test(node.listMarker);\n if (isNumbered) {\n markerDirection = 'rtl';\n markerX = boxRightEdge + gap + markerWidth;\n } else {\n markerX = boxRightEdge + gap;\n }\n } else {\n // LTR: marker in the parent's left padding area (outside the li box).\n markerX = contentStartX - markerWidth - gap;\n }\n }\n\n box.children.unshift({\n type: 'text',\n text: node.listMarker,\n x: markerX,\n y: markerY,\n width: markerDrawWidth,\n style: { ...markerDrawStyle, textDecorationLine: 'none', textDecorations: [], fontWeight: ms?.fontWeight ?? 400, fontStyle: ms?.fontStyle ?? 'normal', direction: markerDirection },\n });\n\n // Also publish the marker through the LayoutLine stream so result.lines\n // sees the bullet/number alongside the item text. Markers are added AFTER\n // inline content is laid out, so they don't go through layoutInlineContent.\n // The buildLayoutTree sort+merge step picks up the marker by its baseline.\n // RTL numbered markers store their right edge in markerX (textAlign trick).\n // bounds.width is the (scaled) glyph ADVANCE, not its ink extent — same\n // convention as numbered markers; the ink right edge itself is pinned to\n // contentStart - gap above.\n const markerLeftX = markerDirection === 'rtl' ? markerX - markerDrawWidth : markerX;\n _lines.push({\n y: Math.round(baselineY),\n text: node.listMarker,\n bounds: {\n x: markerLeftX,\n y: box.y + style.borderTopWidth + style.paddingTop,\n width: markerDrawWidth,\n height: strut.ascent + strut.descent,\n },\n });\n}\n\n// ─── Main entry ────────────────────────────────────────────────────────\n\n/**\n * Build the layout tree from the styled tree using pure canvas measurement.\n * No DOM measurements used — all positions computed from CSS values + canvas.measureText.\n */\nexport function buildLayoutTree(\n ctx: CanvasRenderingContext2D,\n styledTree: StyledNode,\n containerWidth: number,\n useDomMeasurements = true,\n debug?: (entry: import('./types.ts').DebugEntry) => void,\n): { root: LayoutBox; height: number; lines: LayoutLine[] } {\n _useDomMeasurements = useDomMeasurements;\n _debug = debug;\n\n // Clear caches — fonts may have loaded since last call\n _lineHeightCache.clear();\n _fontMetricsCache.clear();\n _fontStringCache.clear();\n _measureCache.clear();\n _lines = [];\n\n // The styledTree root is our container div — layout its children as a block flow\n const { box, height } = layoutBlock(ctx, styledTree, 0, 0, containerWidth);\n\n // Add list markers post-layout\n addListMarkersRecursive(ctx, box, styledTree);\n\n // Sort by baseline y, then by left edge so cross-cell content merges in\n // reading order (LTR). List markers sit at smaller x than their content\n // and so come first, producing \"• Item\" rather than \"Item •\".\n const sorted = _lines.slice().sort((a, b) =>\n (a.y - b.y) || (a.bounds.x - b.bounds.x)\n );\n const lines: LayoutLine[] = [];\n for (const candidate of sorted) {\n const last = lines[lines.length - 1];\n // Tolerance keys off the candidate's line height (matches the legacy\n // extractLines behavior). Using max(last, candidate) is symmetric but\n // grows after each merge as last.bounds.height becomes the union — that\n // leaks across rows in tight multi-column layouts.\n const tolerance = candidate.bounds.height * 0.5;\n if (last && Math.abs(candidate.y - last.y) < tolerance) {\n // Cross-cell merge: insert a space separator so the text stays\n // readable when N cells of a table row collapse into one LayoutLine.\n // Skip if either side already has a boundary space.\n const needsSep = last.text.length > 0 && candidate.text.length > 0 &&\n !/\\s$/.test(last.text) && !/^\\s/.test(candidate.text);\n last.text += (needsSep ? ' ' : '') + candidate.text;\n // Carry baseline forward so the next comparison uses the running\n // edge of the group, not the stale first element's baseline.\n last.y = Math.max(last.y, candidate.y);\n const x1 = Math.min(last.bounds.x, candidate.bounds.x);\n const y1 = Math.min(last.bounds.y, candidate.bounds.y);\n const x2 = Math.max(last.bounds.x + last.bounds.width, candidate.bounds.x + candidate.bounds.width);\n const y2 = Math.max(last.bounds.y + last.bounds.height, candidate.bounds.y + candidate.bounds.height);\n last.bounds = { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };\n } else {\n lines.push({ y: candidate.y, text: candidate.text, bounds: { ...candidate.bounds } });\n }\n }\n return { root: box, height, lines };\n}\n\nfunction addListMarkersRecursive(\n ctx: CanvasRenderingContext2D,\n box: LayoutBox,\n node: StyledNode,\n): void {\n addListMarker(ctx, box, node);\n\n // Match children — box.children may have extra text/inline nodes,\n // so we correlate by walking both in parallel\n let boxChildIdx = 0;\n for (const styledChild of node.children) {\n if (styledChild.tagName === '#text' || isInline(styledChild)) {\n continue;\n }\n // Find the matching LayoutBox\n while (boxChildIdx < box.children.length) {\n const layoutChild = box.children[boxChildIdx];\n if (layoutChild.type === 'box' && layoutChild.tagName === styledChild.tagName) {\n addListMarkersRecursive(ctx, layoutChild, styledChild);\n boxChildIdx++;\n break;\n }\n boxChildIdx++;\n }\n }\n}\n","import type { DecorationEntry, LayoutNode, LayoutBox, LayoutText, ResolvedStyle } from './types.js';\nimport { buildCanvasFont, isTransparent, getFontMetrics, hasTextClip, isShiftedVAlign } from './layout.js';\nimport { paintOrderHasStrokeFirst } from './css-resolver.js';\n\n/**\n * Parse a CSS text-shadow string into individual shadow values.\n * Format: \"2px 2px 4px rgba(0,0,0,0.3), ...\"\n */\nexport function parseTextShadows(shadow: string): Array<{\n offsetX: number;\n offsetY: number;\n blur: number;\n color: string;\n}> {\n if (!shadow || shadow === 'none') return [];\n\n const shadows: Array<{ offsetX: number; offsetY: number; blur: number; color: string }> = [];\n\n // Split by comma but not within parentheses\n const parts = shadow.split(/,(?![^(]*\\))/);\n\n for (const part of parts) {\n const trimmed = part.trim();\n // Extract color (rgb/rgba or named) and numbers\n const colorMatch = trimmed.match(/(rgb[a]?\\([^)]+\\)|#[0-9a-fA-F]+|\\b[a-z]+\\b)(?:\\s|$)/i);\n const numMatches = trimmed.match(/-?[\\d.]+px/g);\n\n if (numMatches && numMatches.length >= 2) {\n const nums = numMatches.map(n => parseFloat(n));\n shadows.push({\n offsetX: nums[0],\n offsetY: nums[1],\n blur: nums[2] || 0,\n color: colorMatch ? colorMatch[1] : 'rgba(0,0,0,1)',\n });\n }\n }\n\n return shadows;\n}\n\n/**\n * Check if a border is visible.\n */\nfunction hasBorder(style: ResolvedStyle, side: 'Top' | 'Right' | 'Bottom' | 'Left'): boolean {\n const width = style[`border${side}Width` as keyof ResolvedStyle] as number;\n const borderStyle = style[`border${side}Style` as keyof ResolvedStyle] as string;\n return width > 0 && borderStyle !== 'none';\n}\n\n/**\n * Draw a decoration line with the given style (solid, dotted, dashed, double, wavy).\n */\nexport function drawDecorationLine(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n width: number,\n lineWidth: number,\n decoStyle: string,\n color: string | CanvasGradient,\n): void {\n // Chrome paints decorations as crisp integer-pixel bands. Snap the stroke\n // center so the band edges land on the pixel grid.\n y = Math.round(y - lineWidth / 2) + lineWidth / 2;\n ctx.save();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n\n if (decoStyle === 'double') {\n const gap = Math.max(lineWidth, 2);\n ctx.lineWidth = Math.max(0.5, lineWidth * 0.5);\n ctx.beginPath();\n ctx.moveTo(x, y - gap / 2);\n ctx.lineTo(x + width, y - gap / 2);\n ctx.moveTo(x, y + gap / 2);\n ctx.lineTo(x + width, y + gap / 2);\n ctx.stroke();\n } else if (decoStyle === 'wavy') {\n const amplitude = Math.max(1.5, lineWidth);\n const wavelength = amplitude * 4;\n ctx.beginPath();\n ctx.moveTo(x, y);\n for (let cx = x; cx < x + width; cx += wavelength) {\n ctx.quadraticCurveTo(cx + wavelength / 4, y - amplitude, cx + wavelength / 2, y);\n ctx.quadraticCurveTo(cx + wavelength * 3 / 4, y + amplitude, cx + wavelength, y);\n }\n ctx.stroke();\n } else {\n // solid, dotted, dashed\n if (decoStyle === 'dotted') ctx.setLineDash([lineWidth, lineWidth * 2]);\n else if (decoStyle === 'dashed') ctx.setLineDash([lineWidth * 3, lineWidth * 2]);\n ctx.beginPath();\n ctx.moveTo(x, y);\n ctx.lineTo(x + width, y);\n ctx.stroke();\n }\n\n ctx.setLineDash([]);\n ctx.restore();\n}\n\n/**\n * Parse a CSS linear-gradient into canvas CanvasGradient.\n */\nexport function parseLinearGradient(\n ctx: CanvasRenderingContext2D,\n bgImage: string,\n x: number,\n width: number,\n y: number,\n height: number,\n): CanvasGradient | null {\n // Extract content inside linear-gradient(...) handling nested parens\n const startIdx = bgImage.indexOf('linear-gradient(');\n if (startIdx === -1) return null;\n let depth = 0;\n let endIdx = -1;\n for (let i = startIdx + 16; i < bgImage.length; i++) {\n if (bgImage[i] === '(') depth++;\n else if (bgImage[i] === ')') {\n if (depth === 0) { endIdx = i; break; }\n depth--;\n }\n }\n if (endIdx === -1) return null;\n const innerContent = bgImage.slice(startIdx + 16, endIdx);\n\n // Split by commas not inside parentheses\n const parts: string[] = [];\n depth = 0;\n let start = 0;\n const inner = innerContent;\n for (let i = 0; i < inner.length; i++) {\n if (inner[i] === '(') depth++;\n else if (inner[i] === ')') depth--;\n else if (inner[i] === ',' && depth === 0) {\n parts.push(inner.slice(start, i).trim());\n start = i + 1;\n }\n }\n parts.push(inner.slice(start).trim());\n // Parse angle/direction\n let angle = 180; // default top to bottom\n let colorStartIdx = 0;\n const firstPart = parts[0];\n if (firstPart.endsWith('deg')) {\n angle = parseFloat(firstPart);\n colorStartIdx = 1;\n } else if (firstPart === 'to right') {\n angle = 90; colorStartIdx = 1;\n } else if (firstPart === 'to left') {\n angle = 270; colorStartIdx = 1;\n } else if (firstPart === 'to bottom') {\n angle = 180; colorStartIdx = 1;\n } else if (firstPart === 'to top') {\n angle = 0; colorStartIdx = 1;\n }\n\n const rad = (angle - 90) * Math.PI / 180;\n const cx = x + width / 2;\n const cy = y + height / 2;\n const len = Math.abs(width * Math.cos(rad)) + Math.abs(height * Math.sin(rad));\n const dx = Math.cos(rad) * len / 2;\n const dy = Math.sin(rad) * len / 2;\n\n const gradient = ctx.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy);\n\n const colors = parts.slice(colorStartIdx);\n for (let i = 0; i < colors.length; i++) {\n const entry = colors[i].trim();\n // Match color followed by optional percentage: \"rgb(220, 38, 38) 0%\"\n // The percentage is always at the very end after the last space outside parens\n let color = entry;\n let stop = i / Math.max(1, colors.length - 1);\n const percentMatch = entry.match(/\\s+([\\d.]+%)\\s*$/);\n if (percentMatch) {\n stop = parseFloat(percentMatch[1]) / 100;\n color = entry.slice(0, entry.length - percentMatch[0].length).trim();\n }\n try {\n gradient.addColorStop(stop, color);\n } catch {\n // Invalid color, skip\n }\n }\n\n return gradient;\n}\n\n/** The solid fill color for text: -webkit-text-fill-color if set, else color. */\nexport function textFillColor(style: ResolvedStyle): string {\n return style.webkitTextFillColor && style.webkitTextFillColor !== 'transparent'\n ? style.webkitTextFillColor : style.color;\n}\n\n/**\n * Text decoration thickness for `auto`. Chromium paints an integer-pixel\n * band of max(1, floor(fontSize / 10)) regardless of font (measured across\n * 6 fonts × 16-64px against the DOM raster).\n */\nexport function decorationThickness(fontSize: number): number {\n return Math.max(1, Math.floor(fontSize / 10));\n}\n\n/**\n * The band width for one decoration entry: the declarer's explicit\n * text-decoration-thickness when set (Chrome draws round(T) rows; a declared\n * 0 hides the band — callers skip on 0), else the auto thickness from the\n * declarer's font size. Shared by both renderers.\n */\nexport function bandWidthFor(deco: DecorationEntry): number {\n const t = deco.declarer.textDecorationThickness;\n if (t === null) return decorationThickness(deco.declarer.fontSize);\n return t <= 0 ? 0 : Math.max(1, Math.round(t));\n}\n\n/**\n * Band-center delta below the baseline for EXPLICIT underline geometry, or\n * null for auto (each renderer keeps its own auto formula). Chrome-measured:\n * an explicit offset puts the band TOP at baseline + offset; auto offset\n * with an explicit thickness T puts it at baseline + ceil(T/2) — measured\n * exactly for T ∈ {1, 3, 4, 5, 8, 10}.\n */\nexport function explicitUnderlineDelta(\n deco: DecorationEntry,\n lineWidth: number,\n): number | null {\n const offset = deco.declarer.textUnderlineOffset;\n if (offset !== null) return offset + lineWidth / 2;\n if (deco.declarer.textDecorationThickness !== null)\n return Math.ceil(lineWidth / 2) + lineWidth / 2;\n return null;\n}\n\n/** Apply the canvas stroke settings for -webkit-text-stroke. A gradient stroke\n * (webkitTextStrokeImage, pre-resolved to a CanvasGradient) wins over the solid\n * stroke color, mirroring how a background-clip:text gradient wins over `color`\n * for the fill. */\nexport function applyTextStroke(\n ctx: CanvasRenderingContext2D,\n style: ResolvedStyle,\n strokeGradient?: CanvasGradient | null,\n): void {\n ctx.strokeStyle = strokeGradient || style.webkitTextStrokeColor || style.color;\n ctx.lineWidth = style.webkitTextStrokeWidth;\n const join = style.strokeLinejoin;\n ctx.lineJoin = join === 'miter' || join === 'bevel' ? join : 'round';\n}\n\n/**\n * Render a single text node to canvas.\n * @param gradientFill — pre-computed gradient for background-clip:text spanning full element\n * @param strokeGradient — pre-computed gradient for -webkit-text-stroke-image spanning full element\n */\nfunction renderText(\n ctx: CanvasRenderingContext2D,\n node: LayoutText,\n gradientFill?: CanvasGradient | string | null,\n strokeGradient?: CanvasGradient | null,\n): void {\n const { style } = node;\n\n ctx.save();\n ctx.font = buildCanvasFont(style);\n ctx.textBaseline = 'alphabetic';\n ctx.fontKerning = style.fontKerning === 'none' ? 'none' : 'normal';\n if (Number.isFinite(style.letterSpacing) && style.letterSpacing !== 0) {\n ctx.letterSpacing = `${style.letterSpacing}px`;\n }\n if (style.wordSpacing) {\n (ctx as any).wordSpacing = `${style.wordSpacing}px`;\n }\n if (style.direction === 'rtl') {\n ctx.direction = 'rtl';\n ctx.textAlign = 'right';\n }\n\n const hasOwnClip = hasTextClip(style);\n const isStrokedText = style.webkitTextStrokeWidth > 0;\n const isFillTransparent = style.webkitTextFillColor === 'transparent' ||\n style.color === 'transparent';\n\n // The background-clip:text paint from a declaring INLINE ancestor (e.g.\n // <span>/<s>) whose non-inheriting background this run's own style doesn't\n // carry: a gradient and/or solid color. Layout resolves the geometry — a box\n // spanning the declaring element's fragment on this line (see\n // assignInlineFragmentBoxes) — and this paint wins over any ancestor block\n // `gradientFill`, because Chrome clips the NEAREST declaring element's\n // background to the glyphs.\n const inlineClipPaint: CanvasGradient | string | null = node.clip\n ? (node.clip.image\n ? parseLinearGradient(\n ctx, node.clip.image,\n node.clip.x, node.clip.width,\n node.clip.y, node.clip.height,\n )\n : null) ?? node.clip.color ?? null\n : null;\n\n // An ancestor's clip paint only shows when this run's own fill is\n // transparent — an opaque own color paints over the clipped background and\n // wins.\n const isGradientText = hasOwnClip ||\n ((gradientFill != null || inlineClipPaint != null) && isFillTransparent);\n\n // What actually fills this run's glyphs (and any clipped decoration band):\n // the nearest inline declarer's paint if present, else the ancestor block's.\n const effectiveGradient = inlineClipPaint ?? gradientFill ?? null;\n\n // Same for the stroke gradient: an inline --rt-text-stroke-image declarer's\n // fragment gradient wins over an ancestor block's threaded one.\n const inlineStrokeGradient = node.strokeImage\n ? parseLinearGradient(\n ctx, node.strokeImage.image,\n node.strokeImage.x, node.strokeImage.width,\n node.strokeImage.y, node.strokeImage.height,\n )\n : null;\n const effectiveStrokeGradient = inlineStrokeGradient ?? strokeGradient ?? null;\n\n // Text shadow (drawn behind the text). Cast the shadow from the shape that\n // is actually painted: the fill when it's visible, and/or the stroke. This\n // matters for stroked text with a transparent fill (color:transparent +\n // -webkit-text-stroke), where CSS casts the shadow from the stroke outline\n // rather than the invisible fill.\n const shadows = parseTextShadows(style.textShadow);\n if (shadows.length > 0) {\n const hasVisibleFill = isGradientText || !isFillTransparent;\n for (const shadow of shadows) {\n ctx.save();\n ctx.shadowOffsetX = shadow.offsetX;\n ctx.shadowOffsetY = shadow.offsetY;\n ctx.shadowBlur = shadow.blur;\n ctx.shadowColor = shadow.color;\n if (hasVisibleFill) {\n ctx.fillStyle = isGradientText && effectiveGradient ? effectiveGradient : textFillColor(style);\n ctx.fillText(node.text, node.x, node.y);\n }\n if (isStrokedText) {\n applyTextStroke(ctx, style, effectiveStrokeGradient);\n ctx.strokeText(node.text, node.x, node.y);\n }\n ctx.restore();\n }\n }\n\n const drawFill = () => {\n if (isGradientText) {\n ctx.save();\n ctx.fillStyle = effectiveGradient || style.color;\n ctx.fillText(node.text, node.x, node.y);\n ctx.restore();\n } else if (!isFillTransparent) {\n // Normal text fill. A transparent fill paints NOTHING, stroked or not —\n // Chrome hides the glyphs entirely for `-webkit-text-fill-color:\n // transparent` (or `color: transparent`) even without a stroke.\n ctx.fillStyle = textFillColor(style);\n ctx.fillText(node.text, node.x, node.y);\n }\n };\n\n const drawStroke = () => {\n if (!isStrokedText) return;\n ctx.save();\n applyTextStroke(ctx, style, effectiveStrokeGradient);\n ctx.strokeText(node.text, node.x, node.y);\n ctx.restore();\n };\n\n if (paintOrderHasStrokeFirst(style.paintOrder)) {\n drawStroke();\n drawFill();\n } else {\n drawFill();\n drawStroke();\n }\n\n // Text decorations — use font metrics for accurate positioning.\n // Each entry paints with its ORIGIN element's color/style (ancestors first,\n // so a child's own decoration lands on top), matching Chrome's non-inherited\n // decoration propagation.\n //\n // Geometry splits, measured against Chrome for `30px ABC + 80px Tale` under\n // one declaration (see tests/decorating-box-geometry.test.ts):\n // - THICKNESS is the decorating box's for all three lines — the band over\n // the 80px child stays 3px, the 30px declarer's.\n // - The UNDERLINE also takes its position from the decorating box: one flat\n // band at rows 225-227 across both runs. It hangs off the alphabetic\n // baseline, which every fragment on the line shares.\n // - The OVERLINE and the LINE-THROUGH do NOT: Chrome steps them per\n // fragment (193-195 vs 148-150, and 213-215 vs 168-170), because each\n // hangs off the crossed fragment's own ascent, not a shared line.\n //\n // `vertical-align` splits the same way: an underline declared ABOVE a\n // `super` child stays flat across it (measured: one band, x 0-228), while\n // the overline and the strike step up with the child. So the underline\n // hangs off the DECLARER's baseline — the line's own, unless the declarer\n // is the shifted element itself, which then carries the band up with it.\n const textWidth = node.width;\n // For RTL text, node.x is the right edge (textAlign='right').\n // Decoration lines need the left edge as start position.\n const decoX = style.direction === 'rtl' ? node.x - textWidth : node.x;\n\n if (style.textDecorations.length > 0) {\n for (const deco of style.textDecorations) {\n const decoWidth = bandWidthFor(deco);\n if (decoWidth <= 0) continue; // declared text-decoration-thickness: 0\n // A transparent decoration inside a background-clip:text element shows\n // the clipped background through the band (Chrome includes decorations\n // in the clip region), so paint it with the gradient — REGARDLESS of\n // this run's own glyph fill: a solid-colored span inside a gradient\n // element still gets the gradient band across it. Transparent with no\n // gradient ancestor paints nothing.\n let color: string | CanvasGradient = deco.color;\n if (isTransparent(deco.color)) {\n if (effectiveGradient) {\n color = effectiveGradient;\n } else {\n continue;\n }\n }\n const decoStyle = deco.style || 'solid';\n\n // Chrome strokes decorations with -webkit-text-stroke, same as glyphs\n // (measured: red text + 3px blue stroke + underline adds only blue\n // pixels — the stroke swallows the thin band). Approximate the outline\n // with a thicker stroke-colored underlay; the decoration paint on top\n // keeps whatever the stroke leaves visible (decoWidth - strokeWidth).\n const strokeW = style.webkitTextStrokeWidth > 0 ? style.webkitTextStrokeWidth : 0;\n const strokeColor: string | CanvasGradient =\n effectiveStrokeGradient || style.webkitTextStrokeColor || style.color;\n const paintBand = (y: number) => {\n // A gradient stroke (CanvasGradient) is never transparent; a solid\n // stroke color still gets the transparent check below.\n const strokeIsTransparent =\n typeof strokeColor === 'string' && isTransparent(strokeColor);\n if (strokeW > 0 && !strokeIsTransparent) {\n drawDecorationLine(ctx, decoX, y, textWidth, decoWidth + strokeW, decoStyle, strokeColor);\n const inner = decoWidth - strokeW;\n if (inner > 0) {\n drawDecorationLine(ctx, decoX, y, textWidth, inner, decoStyle, color);\n }\n } else {\n drawDecorationLine(ctx, decoX, y, textWidth, decoWidth, decoStyle, color);\n }\n };\n\n if (deco.line === 'underline') {\n // The line's own baseline when this run was moved off it by\n // vertical-align and the DECLARER stayed behind; `node.lineBaselineY`\n // is set only on a shifted run.\n const baseline =\n node.lineBaselineY !== undefined && !isShiftedVAlign(deco.declarer.verticalAlign)\n ? node.lineBaselineY\n : node.y;\n const explicitDelta = explicitUnderlineDelta(deco, decoWidth);\n if (explicitDelta !== null) {\n paintBand(baseline + explicitDelta);\n } else {\n // Chrome centers the underline ~0.105em below the baseline for every\n // font tested (measured against the DOM raster sweep). The -0.2px is a\n // rounding tiebreak: at fractional baselines (line-height 1.6/1.8/2.0)\n // Chrome resolves the pixel row downward less often than plain\n // rounding; empirically this cuts row-off-by-one cases 39 → 12 across\n // the sweep without disturbing integer baselines.\n paintBand(baseline + deco.declarer.fontSize * 0.105 - 0.2);\n }\n } else if (deco.line === 'line-through') {\n // Chrome positions the strike from the font's OS/2 strikeout metric,\n // which canvas can't read. 0.33em above the baseline is the closest\n // single-formula fit (tuned against the DOM raster sweep; ±1px for\n // most fonts, ±2px worst case).\n paintBand(node.y - style.fontSize * 0.33);\n } else if (deco.line === 'overline') {\n // Chrome hangs the overline band above the ascent line: its bottom\n // edge sits on the floored ascent pixel row, growing upward. The\n // ascent is the crossed run's, not the declarer's.\n const { ascent: decoAscent } = getFontMetrics(ctx, style);\n const overlineY = Math.floor(node.y - decoAscent) - decoWidth / 2;\n paintBand(overlineY);\n }\n }\n }\n\n ctx.restore();\n}\n\n/**\n * Render a layout box and its children to canvas.\n */\nfunction renderBox(\n ctx: CanvasRenderingContext2D,\n box: LayoutBox,\n gradientFill: CanvasGradient | string | null = null,\n strokeGradient: CanvasGradient | null = null,\n): void {\n const { style } = box;\n\n // Background. With background-clip:text the background is NOT painted as a\n // box — it's clipped to descendant glyphs (threaded below as the text fill).\n if (!isTransparent(style.backgroundColor) && style.webkitBackgroundClip !== 'text') {\n ctx.fillStyle = style.backgroundColor;\n ctx.fillRect(box.x, box.y, box.width, box.height);\n }\n\n // Borders\n const borders: [side: 'Top' | 'Right' | 'Bottom' | 'Left', x1: number, y1: number, x2: number, y2: number][] = [\n ['Top', box.x, box.y + style.borderTopWidth / 2, box.x + box.width, box.y + style.borderTopWidth / 2],\n ['Right', box.x + box.width - style.borderRightWidth / 2, box.y, box.x + box.width - style.borderRightWidth / 2, box.y + box.height],\n ['Bottom', box.x, box.y + box.height - style.borderBottomWidth / 2, box.x + box.width, box.y + box.height - style.borderBottomWidth / 2],\n ['Left', box.x + style.borderLeftWidth / 2, box.y, box.x + style.borderLeftWidth / 2, box.y + box.height],\n ];\n for (const [side, x1, y1, x2, y2] of borders) {\n if (!hasBorder(style, side)) continue;\n ctx.strokeStyle = style[`border${side}Color` as keyof ResolvedStyle] as string;\n ctx.lineWidth = style[`border${side}Width` as keyof ResolvedStyle] as number;\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.lineTo(x2, y2);\n ctx.stroke();\n }\n\n // Pre-compute the paint for background-clip: text elements — a gradient\n // (background-image) or a solid color (background-color). It spans the\n // declaring box and threads through descendant boxes (browsers clip the\n // ancestor's background to ALL descendant glyphs, so text inside block\n // children like <p>/<li> keeps it — the background properties themselves\n // don't inherit); a box declaring its own clipping background overrides it.\n // (Inline declarers are resolved in layout via node.clip, not here.)\n if (hasTextClip(style)) {\n const grad = style.backgroundImage && style.backgroundImage !== 'none'\n ? parseLinearGradient(ctx, style.backgroundImage, box.x, box.width, box.y, box.height)\n : null;\n const solid = !isTransparent(style.backgroundColor) ? style.backgroundColor : null;\n // An unparseable image with no solid color keeps the ancestor's paint.\n gradientFill = grad ?? solid ?? gradientFill;\n }\n\n // Pre-compute the stroke gradient the same way: it spans the declaring box\n // and threads through descendants (a box declaring its own overrides it).\n // -webkit-text-stroke-image isn't inherited as a value; the computed gradient\n // is threaded down instead — exactly like the background-clip:text fill.\n if (style.webkitTextStrokeImage && style.webkitTextStrokeImage !== 'none') {\n strokeGradient = parseLinearGradient(ctx, style.webkitTextStrokeImage, box.x, box.width, box.y, box.height);\n }\n\n // Children\n for (const child of box.children) {\n renderNode(ctx, child, gradientFill, strokeGradient);\n }\n}\n\n/**\n * Render any layout node.\n */\nexport function renderNode(\n ctx: CanvasRenderingContext2D,\n node: LayoutNode,\n gradientFill?: CanvasGradient | string | null,\n strokeGradient?: CanvasGradient | null,\n): void {\n if (node.type === 'text') {\n renderText(ctx, node, gradientFill, strokeGradient);\n } else {\n renderBox(ctx, node, gradientFill, strokeGradient);\n }\n}\n","import type {\n RenderConfig, RenderResult,\n LayoutConfig, LayoutResult, DrawConfig,\n LayoutLine, AnyCanvas, AnyContext,\n} from './types.js';\nimport { parseHTML } from './parse.js';\nimport { resolveStylesFromCSS } from './css-resolver.js';\nimport { buildLayoutTree } from './layout.js';\nimport { renderNode } from './render.js';\n\nexport type { RenderConfig, RenderResult, LayoutConfig, LayoutResult, DrawConfig, LayoutLine };\nexport { setDOMParser, type DOMParserLike } from './dom.js';\nexport { FLOORS_LINE_BASELINE } from './layout.js';\nimport { createFallbackMeasureCtx } from './dom.js';\n\n// Default measurement context, created lazily and reused across layout()\n// calls — safe because font/letterSpacing state is set before every\n// measurement anyway. Browser-first source keeps measurement identical to\n// previous releases.\nlet defaultMeasureCtx: CanvasRenderingContext2D | null = null;\n\n// ─── layout() ────────────────────────────────────────────────────────\n\n/**\n * Compute layout for an HTML string without rendering.\n * Returns a reusable LayoutResult that can be drawn onto multiple targets via drawLayout().\n */\nexport function layout(config: LayoutConfig): LayoutResult {\n const {\n html,\n width,\n height,\n accuracy = 'performance',\n debug,\n } = config;\n\n if (!width || width <= 0 || Number.isNaN(width)) {\n throw new TypeError(`layout: width must be a positive number, got ${width}`);\n }\n\n const useDomMeasurements = accuracy === 'balanced';\n\n const { fragment, css } = parseHTML(html);\n const { tree, cleanup } = resolveStylesFromCSS(fragment, css, width);\n\n // Caller-provided ctx is mutated (font, fontKerning) and intentionally NOT\n // save/restored — save/restore is not free on all contexts (e.g. PDF\n // proxies emit stream operators for it).\n const measureCtx =\n (config.ctx as CanvasRenderingContext2D | undefined) ??\n (defaultMeasureCtx ??= createFallbackMeasureCtx(true));\n measureCtx.fontKerning = 'normal';\n\n const { root, height: contentHeight, lines } = buildLayoutTree(measureCtx, tree, width, useDomMeasurements, debug);\n const finalHeight = height || contentHeight;\n\n cleanup();\n\n return { layoutRoot: root, height: finalHeight, lines };\n}\n\n// ─── drawLayout() ────────────────────────────────────────────────────\n\n/**\n * Draw a pre-computed layout onto a canvas or context.\n * Use with layout() to render the same content onto multiple targets.\n */\nexport function drawLayout(config: DrawConfig): { canvas: AnyCanvas } {\n const {\n layout: layoutResult,\n width,\n pixelRatio = globalThis.devicePixelRatio ?? 1,\n } = config;\n\n if (config.ctx && config.canvas) {\n throw new TypeError('drawLayout: ctx and canvas are mutually exclusive — provide one or neither');\n }\n\n const finalHeight = layoutResult.height;\n let canvas: AnyCanvas;\n let renderCtx: AnyContext;\n\n if (config.ctx) {\n renderCtx = config.ctx;\n canvas = config.ctx.canvas;\n } else {\n if (!config.canvas && typeof document === 'undefined') {\n throw new Error(\n 'render-tag: drawLayout cannot create a canvas in a non-browser environment — pass ctx or canvas.'\n );\n }\n canvas = config.canvas ?? document.createElement('canvas');\n canvas.width = Math.ceil(width * pixelRatio);\n canvas.height = Math.ceil(finalHeight * pixelRatio);\n if ('style' in canvas) {\n (canvas as HTMLCanvasElement).style.width = `${width}px`;\n (canvas as HTMLCanvasElement).style.height = `${finalHeight}px`;\n }\n renderCtx = canvas.getContext('2d')! as AnyContext;\n renderCtx.scale(pixelRatio, pixelRatio);\n }\n\n renderNode(renderCtx as CanvasRenderingContext2D, layoutResult.layoutRoot);\n\n return { canvas };\n}\n\n// ─── render() ────────────────────────────────────────────────────────\n\n/**\n * Render an HTML string onto a canvas using pure 2D canvas API.\n * Convenience function combining layout() + drawLayout().\n * Fonts must already be loaded before calling this function.\n */\nexport function render(config: RenderConfig): RenderResult {\n if (config.ctx && config.canvas) {\n throw new TypeError('render: ctx and canvas are mutually exclusive — provide one or neither');\n }\n\n // The output ctx doubles as the measurement ctx (same font resolution for\n // measuring and drawing — required in non-browser environments).\n const layoutResult = layout({\n html: config.html,\n width: config.width,\n height: config.height,\n accuracy: config.accuracy,\n debug: config.debug,\n ctx: config.ctx,\n });\n\n const { canvas } = drawLayout({\n layout: layoutResult,\n width: config.width,\n ctx: config.ctx,\n canvas: config.canvas,\n pixelRatio: config.pixelRatio,\n });\n\n return {\n canvas,\n height: layoutResult.height,\n layoutRoot: layoutResult.layoutRoot,\n lines: layoutResult.lines,\n };\n}\n\n"],"mappings":"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"}
1
+ {"version":3,"file":"render-tag.umd.js","names":[],"sources":["../src/dom.ts","../src/parse.ts","../src/css-resolver.ts","../src/layout.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["/**\n * DOM parser resolution — the only place render-tag looks for a DOM.\n *\n * Resolution order:\n * 1. A parser injected via setDOMParser() (explicit always wins).\n * 2. The ambient global DOMParser (browsers, jsdom/happy-dom environments).\n * 3. Throw with guidance — render-tag has zero dependencies, so in Node the\n * consumer must inject a parser (e.g. linkedom's or jsdom's DOMParser).\n */\n\nexport interface DOMParserLike {\n /**\n * Must behave like the standard DOMParser for 'text/html' input. The return\n * type is intentionally loose so non-browser DOM libraries (linkedom,\n * jsdom) type-check without casts — their Document types are structurally\n * different from the TS lib's.\n */\n parseFromString(markup: string, type: string): unknown;\n}\n\nlet explicitParser: DOMParserLike | null = null;\nlet ambientParser: DOMParserLike | null = null;\n\n/**\n * Inject the DOM parser render-tag uses to parse HTML input.\n * Required in non-browser environments (Node.js). Pass null to reset.\n *\n * import { DOMParser } from 'linkedom';\n * setDOMParser(new DOMParser());\n */\nexport function setDOMParser(parser: DOMParserLike | null): void {\n explicitParser = parser;\n // Reset the ambient cache too, so tests/harnesses that tear down a DOM\n // polyfill (jsdom etc.) don't keep measuring against a stale realm.\n if (parser === null) ambientParser = null;\n}\n\n/**\n * Create a measurement 2D context from whatever canvas source the environment\n * offers. `preferDocument` preserves each entry point's historical source\n * (block layout: document canvas; path: OffscreenCanvas) so existing pixel\n * baselines don't move.\n */\nexport function createFallbackMeasureCtx(preferDocument: boolean): CanvasRenderingContext2D {\n const hasDocument = typeof document !== 'undefined';\n const hasOffscreen = typeof OffscreenCanvas !== 'undefined';\n if (hasDocument && (preferDocument || !hasOffscreen)) {\n return document.createElement('canvas').getContext('2d')! as CanvasRenderingContext2D;\n }\n if (hasOffscreen) {\n return new OffscreenCanvas(1, 1).getContext('2d')! as unknown as CanvasRenderingContext2D;\n }\n throw new Error(\n 'render-tag: no canvas available for text measurement. ' +\n 'In a non-browser environment, pass config.ctx (a 2D context).'\n );\n}\n\n/** @internal */\nexport function resolveDOMParser(): DOMParserLike {\n if (explicitParser) return explicitParser;\n if (typeof DOMParser !== 'undefined') {\n // DOMParser instances are stateless — cache one.\n if (!ambientParser) ambientParser = new DOMParser();\n return ambientParser;\n }\n throw new Error(\n 'render-tag: no DOM parser available. In a non-browser environment, ' +\n 'inject one via setDOMParser(new DOMParser()) using a DOM library ' +\n 'such as linkedom or jsdom.'\n );\n}\n","import { resolveDOMParser } from './dom.js';\n\n/**\n * Parse HTML string and extract inline <style> blocks.\n * Returns the content element and combined CSS text.\n */\nexport function parseHTML(html: string): { fragment: DocumentFragment; css: string } {\n const parser = resolveDOMParser();\n // Wrap in a full document: browsers do this implicitly for fragments, but\n // non-browser parsers (linkedom) need the body to exist explicitly.\n const doc = parser.parseFromString(\n `<!DOCTYPE html><html><head></head><body>${html}</body></html>`,\n 'text/html'\n ) as Document;\n\n // Extract all <style> tag contents\n const styleTags = doc.querySelectorAll('style');\n let css = '';\n for (const tag of styleTags) {\n css += tag.textContent + '\\n';\n tag.remove();\n }\n\n // Merge adjacent text nodes. Browsers already parse `big&nbsp;text` into a\n // single text node, but linkedom emits a node per entity boundary — which\n // would let the tokenizer break lines at entity seams. Normalizing in both\n // environments keeps parsing parity by construction. Scoped to body (all\n // content lives there); parseHTML can run in per-pixel fit loops.\n doc.body.normalize();\n\n // Move body children into a fragment owned by the same document — no\n // adoption needed (and non-browser DOMs may not implement adoptNode).\n const fragment = doc.createDocumentFragment();\n while (doc.body.firstChild) {\n fragment.appendChild(doc.body.firstChild);\n }\n\n return { fragment, css };\n}\n","import type { DecorationEntry, ResolvedStyle, StyledNode } from './types.js';\n\n// Node.TEXT_NODE / Node.ELEMENT_NODE without the ambient `Node` global\n// (unavailable in non-browser environments).\nconst ELEMENT_NODE = 1;\nconst TEXT_NODE = 3;\n\n// ─── CSS Parser ──────────────────────────────────────────────────────\n\ninterface CSSDeclaration {\n property: string;\n value: string;\n}\n\ninterface CSSRule {\n selectors: string[];\n declarations: CSSDeclaration[];\n}\n\n/**\n * Parse a simple CSS string into rules.\n * Supports: tag, .class, parent > child, comma-separated selectors.\n * Extracts @font-face rules separately for injection into the document.\n */\nfunction parseCSS(css: string): { rules: CSSRule[]; fontFaceRules: string[] } {\n const rules: CSSRule[] = [];\n const fontFaceRules: string[] = [];\n // Remove comments\n css = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '');\n\n let i = 0;\n while (i < css.length) {\n // Skip whitespace\n while (i < css.length && /\\s/.test(css[i])) i++;\n if (i >= css.length) break;\n\n // Handle at-rules (@font-face, @media, etc.)\n if (css[i] === '@') {\n const atStart = i;\n let braceDepth = 0;\n while (i < css.length) {\n if (css[i] === '{') braceDepth++;\n if (css[i] === '}') {\n braceDepth--;\n if (braceDepth <= 0) { i++; break; }\n }\n i++;\n }\n // Capture @font-face rules for injection\n const atRule = css.slice(atStart, i);\n if (atRule.startsWith('@font-face')) {\n fontFaceRules.push(atRule);\n }\n continue;\n }\n\n // Read selector(s) up to '{'\n const selectorStart = i;\n while (i < css.length && css[i] !== '{') i++;\n if (i >= css.length) break;\n const selectorStr = css.slice(selectorStart, i).trim();\n i++; // skip '{'\n\n // Read declarations up to '}'\n const declStart = i;\n while (i < css.length && css[i] !== '}') i++;\n const declStr = css.slice(declStart, i).trim();\n i++; // skip '}'\n\n if (!selectorStr) continue;\n\n // Parse selectors (comma-separated)\n const selectors = selectorStr.split(',').map(s => s.trim()).filter(Boolean);\n\n // Parse declarations\n const declarations: CSSDeclaration[] = [];\n for (const decl of declStr.split(';')) {\n const colonIdx = decl.indexOf(':');\n if (colonIdx === -1) continue;\n const property = decl.slice(0, colonIdx).trim().toLowerCase();\n const value = decl.slice(colonIdx + 1).trim();\n if (property && value) {\n declarations.push({ property, value });\n }\n }\n\n if (selectors.length > 0 && declarations.length > 0) {\n rules.push({ selectors, declarations });\n }\n }\n\n return { rules, fontFaceRules };\n}\n\n// ─── Selector Matching ───────────────────────────────────────────────\n\ninterface ElementContext {\n tagName: string;\n classes: Set<string>;\n parent: ElementContext | null;\n el: Element;\n}\n\n/**\n * Compute specificity for a simple selector.\n * Returns [ids, classes, tags] tuple.\n */\nfunction selectorSpecificity(selector: string): [number, number, number] {\n // Remove pseudo-elements for specificity calculation\n const sel = selector.replace(/::[\\w-]+/g, '');\n const parts = sel.split(/\\s*>\\s*|\\s+/);\n let ids = 0, classes = 0, tags = 0;\n for (const part of parts) {\n // Count #id\n const idMatches = part.match(/#[\\w-]+/g);\n if (idMatches) ids += idMatches.length;\n // Count .class\n const classMatches = part.match(/\\.[\\w-]+/g);\n if (classMatches) classes += classMatches.length;\n // Count tag (strip classes/ids/pseudo)\n const tagPart = part.replace(/[#.][\\w-]+/g, '').replace(/:[\\w-]+/g, '').trim();\n if (tagPart && tagPart !== '*') tags++;\n }\n return [ids, classes, tags];\n}\n\n/** Parsed representation of a simple selector part (e.g. \"ul.foo\") */\ninterface ParsedPart {\n tag: string; // '' if no tag, or the tag name\n classes: string[]; // class names without the dot\n}\n\nfunction parsePart(part: string): ParsedPart {\n const classMatches = part.match(/\\.[\\w-]+/g) || [];\n const tag = part.replace(/\\.[\\w-]+/g, '').replace(/:[\\w-]+/g, '').trim();\n return {\n tag: (tag && tag !== '*') ? tag : '',\n classes: classMatches.map(c => c.slice(1)),\n };\n}\n\n/**\n * Check if a parsed selector part matches an element context.\n */\nfunction matchesPart(part: string, ctx: ElementContext): boolean {\n const classMatches = part.match(/\\.[\\w-]+/g) || [];\n const tag = part.replace(/\\.[\\w-]+/g, '').replace(/:[\\w-]+/g, '').trim();\n\n if (tag && tag !== '*' && tag !== ctx.tagName) return false;\n for (const cls of classMatches) {\n if (!ctx.classes.has(cls.slice(1))) return false;\n }\n return true;\n}\n\nfunction matchesParsedPart(part: ParsedPart, ctx: ElementContext): boolean {\n if (part.tag && part.tag !== ctx.tagName) return false;\n for (const cls of part.classes) {\n if (!ctx.classes.has(cls)) return false;\n }\n return true;\n}\n\n/** Pre-parsed selector ready for fast matching */\ninterface ParsedSelector {\n /** Parsed parts, rightmost first (match order) */\n parts: ParsedPart[];\n /** Combinators between parts[i] and parts[i+1]: '>' or ' ' */\n combinators: string[];\n /** Rightmost part — used for index lookup */\n rightmost: ParsedPart;\n /** Whether the rightmost part is 'html' or 'body' (matches root) */\n rightmostIsRoot: boolean;\n /** Original specificity */\n spec: [number, number, number];\n /**\n * Pseudo-element on the rightmost compound. Currently we only honor\n * `::marker` — its declarations apply to the marker box of `<li>`,\n * not to the element body.\n */\n pseudoElement?: 'marker';\n}\n\n/**\n * Pre-parse and tokenize a selector string into a ParsedSelector.\n * Returns null for selectors we can't handle (pseudo-classes; pseudo-elements\n * other than `::marker`).\n */\nfunction parseSelector(selector: string): ParsedSelector | null {\n // Detect `::marker` on the rightmost compound. Other pseudo-elements\n // (::before, ::after, ::first-line, …) are still rejected.\n let pseudoElement: 'marker' | undefined;\n if (selector.includes('::')) {\n // Allow only a single trailing ::marker on the rightmost compound.\n // Anything else is unsupported.\n const otherPseudo = selector.replace(/::marker\\b/g, '');\n if (otherPseudo.includes('::')) return null;\n if (/::marker\\b/.test(selector)) {\n pseudoElement = 'marker';\n // Bare `::marker` (start of input or after whitespace/`>`) → `*`\n // so descendant/child combinators are preserved.\n selector = selector.replace(/(^|[\\s>])::marker\\b/g, '$1*');\n // `::marker` glued to a tag/class compound → strip the pseudo only.\n selector = selector.replace(/::marker\\b/g, '');\n } else {\n return null;\n }\n }\n if (/:(?:nth-|hover|focus|active|visited|first-child|last-child)/.test(selector)) return null;\n\n const tokens: string[] = [];\n const combinators: string[] = [];\n\n const raw = selector.trim().split(/\\s+/);\n for (let i = 0; i < raw.length; i++) {\n if (raw[i] === '>') {\n combinators.push('>');\n } else {\n if (tokens.length > combinators.length + 1) {\n combinators.push(' ');\n }\n tokens.push(raw[i]);\n }\n }\n while (combinators.length < tokens.length - 1) {\n combinators.push(' ');\n }\n\n if (tokens.length === 0) return null;\n\n const parts = tokens.map(parsePart);\n const rightmost = parts[parts.length - 1];\n const rightmostTag = rightmost.tag;\n\n return {\n parts,\n combinators,\n rightmost,\n rightmostIsRoot: rightmostTag === 'html' || rightmostTag === 'body',\n spec: selectorSpecificity(selector),\n pseudoElement,\n };\n}\n\n/**\n * Match a pre-parsed selector against an element context.\n */\nfunction matchesParsedSelector(sel: ParsedSelector, ctx: ElementContext): boolean {\n // Quick check: rightmost part must match current element\n if (sel.rightmostIsRoot) {\n if (ctx.parent !== null) return false; // html/body only match root\n } else {\n if (!matchesParsedPart(sel.rightmost, ctx)) return false;\n }\n\n // Single-part selector — already matched\n if (sel.parts.length === 1) return true;\n\n // Walk ancestors for remaining parts (right-to-left)\n let current: ElementContext | null = ctx.parent;\n for (let ti = sel.parts.length - 2; ti >= 0; ti--) {\n if (!current) return false;\n const part = sel.parts[ti];\n const combinator = sel.combinators[ti];\n\n if (combinator === '>') {\n // Direct child: current ancestor must match\n const isRoot = current.parent === null;\n if (isRoot && (part.tag === 'html' || part.tag === 'body')) {\n current = current.parent;\n } else if (matchesParsedPart(part, current)) {\n current = current.parent;\n } else {\n return false;\n }\n } else {\n // Descendant: any ancestor must match\n let found = false;\n while (current) {\n const isRoot = current.parent === null;\n if (isRoot && (part.tag === 'html' || part.tag === 'body')) {\n current = current.parent;\n found = true;\n break;\n }\n if (matchesParsedPart(part, current)) {\n current = current.parent;\n found = true;\n break;\n }\n current = current.parent;\n }\n if (!found) return false;\n }\n }\n\n return true;\n}\n\n// ─── Style Resolution ────────────────────────────────────────────────\n\n/** Default values for all ResolvedStyle properties */\nfunction defaultStyle(): ResolvedStyle {\n return {\n // Browsers default unstyled text to the UA serif font (Times). Match it so\n // HTML without an explicit font-family wraps/positions like the browser.\n fontFamily: 'serif',\n fontSize: 16,\n fontWeight: 400,\n fontStyle: 'normal',\n fontVariantCaps: 'normal',\n color: 'rgb(0, 0, 0)',\n textAlign: 'start',\n textAlignLast: 'auto',\n textIndent: 0,\n textTransform: 'none',\n textDecorationLine: 'none',\n textDecorationStyle: 'solid',\n textDecorationColor: 'rgb(0, 0, 0)',\n textDecorations: [],\n textUnderlineOffset: null,\n textDecorationThickness: null,\n textShadow: 'none',\n webkitTextStrokeWidth: 0,\n webkitTextStrokeColor: '',\n webkitTextStrokeImage: 'none',\n webkitTextFillColor: '',\n paintOrder: 'normal',\n strokeLinejoin: 'round',\n webkitBackgroundClip: '',\n backgroundImage: 'none',\n letterSpacing: 0,\n wordSpacing: 0,\n fontKerning: 'auto',\n lineHeight: 0,\n verticalAlign: 'baseline',\n whiteSpace: 'normal',\n wordBreak: 'normal',\n overflowWrap: 'normal',\n unicodeBidi: 'normal',\n direction: 'ltr',\n display: 'block',\n width: 0,\n minHeight: 0,\n paddingTop: 0,\n paddingRight: 0,\n paddingBottom: 0,\n paddingLeft: 0,\n marginTop: 0,\n marginRight: 0,\n marginBottom: 0,\n marginLeft: 0,\n backgroundColor: 'rgba(0, 0, 0, 0)',\n borderTopWidth: 0,\n borderTopColor: 'rgb(0, 0, 0)',\n borderTopStyle: 'none',\n borderRightWidth: 0,\n borderRightColor: 'rgb(0, 0, 0)',\n borderRightStyle: 'none',\n borderBottomWidth: 0,\n borderBottomColor: 'rgb(0, 0, 0)',\n borderBottomStyle: 'none',\n borderLeftWidth: 0,\n borderLeftColor: 'rgb(0, 0, 0)',\n borderLeftStyle: 'none',\n flexDirection: 'row',\n gap: 0,\n flexGrow: 0,\n listStyleType: 'disc',\n lineClamp: 0,\n };\n}\n\n/** Tag-level default display values and browser default margins */\nconst TAG_DEFAULTS: Record<string, Partial<ResolvedStyle>> = {\n span: { display: 'inline' },\n a: { display: 'inline' },\n strong: { display: 'inline', fontWeight: 700 },\n b: { display: 'inline', fontWeight: 700 },\n em: { display: 'inline', fontStyle: 'italic' },\n i: { display: 'inline', fontStyle: 'italic' },\n u: { display: 'inline', textDecorationLine: 'underline' },\n s: { display: 'inline', textDecorationLine: 'line-through' },\n strike: { display: 'inline', textDecorationLine: 'line-through' },\n del: { display: 'inline', textDecorationLine: 'line-through' },\n sub: { display: 'inline', verticalAlign: 'sub', fontSize: 0.83 },\n sup: { display: 'inline', verticalAlign: 'super', fontSize: 0.83 },\n code: { display: 'inline', fontFamily: 'monospace' },\n cite: { display: 'inline', fontStyle: 'italic' },\n bdo: { display: 'inline', unicodeBidi: 'bidi-override' },\n bdi: { display: 'inline', unicodeBidi: 'isolate' },\n p: { display: 'block', marginTop: -1, marginBottom: -1 }, // -1 = 1em, resolved later\n div: { display: 'block' },\n h1: { display: 'block', fontSize: 2, fontWeight: 700, marginTop: -0.67, marginBottom: -0.67 },\n h2: { display: 'block', fontSize: 1.5, fontWeight: 700, marginTop: -0.83, marginBottom: -0.83 },\n h3: { display: 'block', fontSize: 1.17, fontWeight: 700, marginTop: -1, marginBottom: -1 },\n h4: { display: 'block', fontSize: 1, fontWeight: 700, marginTop: -1.33, marginBottom: -1.33 },\n h5: { display: 'block', fontSize: 0.83, fontWeight: 700, marginTop: -1.67, marginBottom: -1.67 },\n h6: { display: 'block', fontSize: 0.67, fontWeight: 700, marginTop: -2.33, marginBottom: -2.33 },\n ul: { display: 'block', listStyleType: 'disc', marginTop: -1, marginBottom: -1 },\n ol: { display: 'block', listStyleType: 'decimal', marginTop: -1, marginBottom: -1 },\n li: { display: 'list-item' },\n blockquote: { display: 'block', marginTop: -1, marginBottom: -1, marginLeft: 40, marginRight: 40 },\n pre: { display: 'block', whiteSpace: 'pre', fontFamily: 'monospace', marginTop: -1, marginBottom: -1 },\n table: { display: 'table' },\n tr: { display: 'table-row' },\n td: { display: 'table-cell' },\n th: { display: 'table-cell', fontWeight: 700 },\n br: { display: 'inline' },\n hr: {\n display: 'block',\n borderTopWidth: 1,\n borderTopStyle: 'solid',\n borderTopColor: 'gray',\n marginTop: -0.5,\n marginBottom: -0.5,\n },\n};\n\n/**\n * Parse a CSS value to pixels given a parent font size for em/% resolution.\n */\nfunction parseValue(value: string, parentFontSize: number, containerWidth: number): number {\n if (!value || value === 'normal' || value === 'auto' || value === 'none') return 0;\n const trimmed = value.trim();\n\n if (trimmed.endsWith('em')) {\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : num * parentFontSize;\n }\n if (trimmed.endsWith('%')) {\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : (num / 100) * containerWidth;\n }\n if (trimmed.endsWith('px')) {\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : num;\n }\n // Bare number (for line-height, etc.)\n const num = parseFloat(trimmed);\n return isNaN(num) ? 0 : num;\n}\n\nfunction parseFontWeight(value: string): number {\n if (value === 'bold') return 700;\n if (value === 'normal') return 400;\n const num = parseInt(value, 10);\n return isNaN(num) ? 400 : num;\n}\n\n/**\n * Resolve `paint-order` to whether stroke is painted before fill.\n * Per CSS spec, missing tokens append in order: fill, stroke, markers.\n * So `stroke` alone implies `stroke fill markers` (stroke first).\n */\nexport function paintOrderHasStrokeFirst(paintOrder: string): boolean {\n const v = paintOrder.trim().toLowerCase();\n if (!v || v === 'normal') return false;\n const tokens = v.split(/\\s+/).filter(t => t === 'fill' || t === 'stroke');\n const strokeIdx = tokens.indexOf('stroke');\n const fillIdx = tokens.indexOf('fill');\n if (strokeIdx === -1) return false;\n if (fillIdx === -1) return true;\n return strokeIdx < fillIdx;\n}\n\n/** Split on whitespace, but only at paren-depth 0 — keeps `rgb(1, 2, 3)` intact. */\nfunction splitTopLevelWhitespace(value: string): string[] {\n const parts: string[] = [];\n let depth = 0;\n let cur = '';\n for (let i = 0; i < value.length; i++) {\n const ch = value[i];\n if (ch === '(') { depth++; cur += ch; }\n else if (ch === ')') { depth = Math.max(0, depth - 1); cur += ch; }\n else if (depth === 0 && /\\s/.test(ch)) {\n if (cur) { parts.push(cur); cur = ''; }\n } else cur += ch;\n }\n if (cur) parts.push(cur);\n return parts;\n}\n\n/**\n * Expand shorthand properties into individual ones.\n * E.g., margin: 10px 20px → marginTop/Right/Bottom/Left\n */\nexport function expandShorthand(property: string, value: string): CSSDeclaration[] {\n if (property === 'margin' || property === 'padding') {\n const parts = value.trim().split(/\\s+/);\n let top: string, right: string, bottom: string, left: string;\n if (parts.length === 1) {\n top = right = bottom = left = parts[0];\n } else if (parts.length === 2) {\n top = bottom = parts[0];\n right = left = parts[1];\n } else if (parts.length === 3) {\n top = parts[0]; right = left = parts[1]; bottom = parts[2];\n } else {\n top = parts[0]; right = parts[1]; bottom = parts[2]; left = parts[3];\n }\n return [\n { property: `${property}-top`, value: top },\n { property: `${property}-right`, value: right },\n { property: `${property}-bottom`, value: bottom },\n { property: `${property}-left`, value: left },\n ];\n }\n\n if (property === 'border' || property === 'border-top' || property === 'border-right' ||\n property === 'border-bottom' || property === 'border-left') {\n const parts = value.trim().split(/\\s+/);\n const borderStyles = ['solid', 'dashed', 'dotted', 'double', 'none', 'hidden'];\n const width = parts.find(p => p.endsWith('px') || /^\\d/.test(p)) || '0';\n const style = parts.find(p => borderStyles.includes(p)) || 'none';\n const color = parts.find(p => !p.endsWith('px') && !/^\\d/.test(p) && !borderStyles.includes(p)) || 'currentColor';\n const result: CSSDeclaration[] = [];\n const sides = property === 'border'\n ? ['top', 'right', 'bottom', 'left']\n : [property.replace('border-', '')];\n for (const side of sides) {\n result.push({ property: `border-${side}-width`, value: width });\n result.push({ property: `border-${side}-style`, value: style });\n result.push({ property: `border-${side}-color`, value: color });\n }\n return result;\n }\n\n if (property === 'list-style') {\n // list-style: none → list-style-type: none\n if (value === 'none') {\n return [{ property: 'list-style-type', value: 'none' }];\n }\n return [{ property: 'list-style-type', value }];\n }\n\n if (property === 'text-decoration') {\n const v = value.trim();\n // Thickness is a longhand of this shorthand (css-text-decor-4): the\n // shorthand resets it to `auto` when no thickness token is present. The\n // reset is emitted FIRST so an explicit token parsed below overrides it.\n if (v === 'inherit' || v === 'none') {\n return [\n { property: 'text-decoration-line', value: 'none' },\n { property: 'text-decoration-thickness', value: 'auto' },\n ];\n }\n // Extract color functions (rgb(...), hsl(...)) before splitting on whitespace,\n // because they contain spaces internally (e.g. \"rgb(231, 76, 60)\").\n let colorValue = '';\n const withoutColorFn = v.replace(/\\b(rgba?\\([^)]*\\)|hsla?\\([^)]*\\))/i, (match) => {\n colorValue = match;\n return '';\n });\n const parts = withoutColorFn.split(/\\s+/).filter(Boolean);\n const lineValues = ['underline', 'overline', 'line-through'];\n const styleValues = ['solid', 'double', 'dotted', 'dashed', 'wavy'];\n const result: CSSDeclaration[] = [\n { property: 'text-decoration-thickness', value: 'auto' },\n ];\n const lines: string[] = [];\n for (const p of parts) {\n if (lineValues.includes(p)) lines.push(p);\n else if (styleValues.includes(p)) result.push({ property: 'text-decoration-style', value: p });\n // Thickness values (2px, .5em, 10%) and its keywords go to the longhand.\n else if (/^[\\d.+-]/.test(p) || p === 'auto' || p === 'from-font')\n result.push({ property: 'text-decoration-thickness', value: p });\n // Remaining tokens are the color (#hex or named).\n else result.push({ property: 'text-decoration-color', value: p });\n }\n if (colorValue) result.push({ property: 'text-decoration-color', value: colorValue });\n if (lines.length > 0) result.unshift({ property: 'text-decoration-line', value: lines.join(' ') });\n return result;\n }\n\n if (property === '-webkit-text-stroke') {\n // -webkit-text-stroke: 1px #1e40af → width + color\n // Split on whitespace at paren-depth 0 so colors with internal spaces\n // (rgb(255, 255, 255), var(--c, ...), color(srgb 1 0 0), …) survive intact.\n const parts = splitTopLevelWhitespace(value.trim());\n const width = parts.find(p => p.endsWith('px') || /^\\d/.test(p)) || '0';\n const color = parts.find(p => !p.endsWith('px') && !/^\\d/.test(p)) || 'currentColor';\n return [\n { property: '-webkit-text-stroke-width', value: width },\n { property: '-webkit-text-stroke-color', value: color },\n ];\n }\n\n if (property === 'flex') {\n // flex: 1 → flex-grow: 1\n const parts = value.trim().split(/\\s+/);\n const grow = parseFloat(parts[0]);\n if (!isNaN(grow)) {\n return [{ property: 'flex-grow', value: String(grow) }];\n }\n return [];\n }\n\n if (property === 'border-collapse' || property === 'border-spacing') {\n // Ignored — table-specific properties we don't handle\n return [];\n }\n\n return [{ property, value }];\n}\n\n/** Normalize the (case-insensitive) currentColor keyword to '', the canonical unset value. */\nfunction normalizeCurrentColor(value: string): string {\n const v = value.trim();\n return v.toLowerCase() === 'currentcolor' ? '' : v;\n}\n\n/**\n * Apply a CSS declaration to a ResolvedStyle, resolving units.\n */\nfunction applyDeclaration(\n style: ResolvedStyle,\n property: string,\n value: string,\n parentFontSize: number,\n containerWidth: number,\n direction: string,\n): void {\n // Resolve the font-size first if that's what we're setting, since\n // em values for other properties depend on the element's own font-size\n const fontSize = style.fontSize || parentFontSize;\n\n switch (property) {\n // Font & text\n case 'font-family': style.fontFamily = value.trim(); break;\n case 'font-size': {\n const v = value.trim();\n if (v.endsWith('em')) {\n style.fontSize = parseFloat(v) * parentFontSize;\n } else if (v.endsWith('%')) {\n style.fontSize = (parseFloat(v) / 100) * parentFontSize;\n } else {\n style.fontSize = parseFloat(v) || parentFontSize;\n }\n break;\n }\n case 'font-weight': style.fontWeight = parseFontWeight(value); break;\n case 'font-style': style.fontStyle = value.trim(); break;\n // Canvas only renders the `small-caps` variant; map anything containing it\n // (incl. the font-variant shorthand) to small-caps, else normal.\n case 'font-variant':\n case 'font-variant-caps':\n style.fontVariantCaps = /\\bsmall-caps\\b/.test(value) ? 'small-caps' : 'normal';\n break;\n case 'color': style.color = value.trim(); break;\n case 'text-align': style.textAlign = value.trim(); break;\n case 'text-align-last': style.textAlignLast = value.trim(); break;\n case 'text-indent':\n style.textIndent = parseValue(value, fontSize, containerWidth); break;\n case 'text-transform': style.textTransform = value.trim(); break;\n case 'text-decoration-line': style.textDecorationLine = value.trim(); break;\n // text-decoration is expanded in expandShorthand, should not reach here\n // but handle just in case\n case 'text-decoration': break;\n case 'text-decoration-style': style.textDecorationStyle = value.trim(); break;\n case 'text-decoration-color': style.textDecorationColor = value.trim(); break;\n case 'text-underline-offset': {\n // px value or null for `auto`; the `_underlineOffsetPct` shadow lets\n // inheritFrom re-resolve a % per child (see the field doc in types.ts).\n // `= undefined` rather than `delete`: same semantics for the only\n // consumer (`!== undefined`), keeps the object's hidden class.\n const v = value.trim();\n if (v === 'auto') {\n (style as any)._underlineOffsetPct = undefined;\n style.textUnderlineOffset = null;\n } else if (isNaN(parseFloat(v))) {\n // Invalid declaration — ignored, like the browser (parseValue would\n // coerce it to 0 and pin the band at the baseline).\n } else if (v.endsWith('%')) {\n const num = parseFloat(v);\n style.textUnderlineOffset = (num / 100) * fontSize;\n (style as any)._underlineOffsetPct = num;\n } else {\n (style as any)._underlineOffsetPct = undefined;\n style.textUnderlineOffset = parseValue(v, fontSize, containerWidth);\n }\n break;\n }\n case 'text-decoration-thickness': {\n // px value or null for `auto`/`from-font` (see the field doc in\n // types.ts). A % resolves against the element's own font size.\n const v = value.trim();\n if (v === 'auto' || v === 'from-font') {\n style.textDecorationThickness = null;\n } else if (isNaN(parseFloat(v))) {\n // Invalid declaration — ignored, like the browser (parseValue would\n // coerce it to 0 and hide the band).\n } else if (v.endsWith('%')) {\n style.textDecorationThickness = (parseFloat(v) / 100) * fontSize;\n } else {\n style.textDecorationThickness = parseValue(v, fontSize, containerWidth);\n }\n break;\n }\n case 'text-shadow': style.textShadow = value.trim(); break;\n case '-webkit-text-stroke-width': style.webkitTextStrokeWidth = parseValue(value, fontSize, containerWidth); break;\n // '' is the canonical currentColor for these two: it must survive\n // inheritance as a keyword and resolve against each element's own\n // color at render time, so it is never eagerly resolved here.\n case '-webkit-text-stroke-color': style.webkitTextStrokeColor = normalizeCurrentColor(value); break;\n // A CSS custom property (not a real -webkit- property) so the browser keeps\n // it in the element's inline cssText — an unknown real property would be\n // dropped before render-tag reads it.\n case '--rt-text-stroke-image': style.webkitTextStrokeImage = value.trim(); break;\n case '-webkit-text-fill-color': style.webkitTextFillColor = normalizeCurrentColor(value); break;\n case 'paint-order': style.paintOrder = value.trim(); break;\n case 'stroke-linejoin': style.strokeLinejoin = value.trim(); break;\n case '-webkit-background-clip':\n case 'background-clip': style.webkitBackgroundClip = value.trim(); break;\n case 'background-image': style.backgroundImage = value.trim(); break;\n case 'letter-spacing':\n style.letterSpacing = value.trim() === 'normal' ? 0 : parseValue(value, fontSize, containerWidth); break;\n case 'word-spacing':\n style.wordSpacing = value.trim() === 'normal' ? 0 : parseValue(value, fontSize, containerWidth); break;\n case 'font-kerning': style.fontKerning = value.trim(); break;\n case 'line-height': {\n const v = value.trim();\n if (v === 'normal') {\n style.lineHeight = 0; // 0 signals \"normal\"\n } else if (v.endsWith('px')) {\n style.lineHeight = parseFloat(v) || 0;\n } else if (v.endsWith('em')) {\n style.lineHeight = parseFloat(v) * fontSize;\n } else if (v.endsWith('%')) {\n // Percentage — computed against the element's own font size and\n // inherited as that computed value (no multiplier for children),\n // same as the em branch. Without this branch \"120%\" used to fall\n // into the unitless path as parseFloat(\"120%\") = 120, producing a\n // 120x line height.\n const num = parseFloat(v);\n if (!isNaN(num)) {\n style.lineHeight = (num / 100) * fontSize;\n }\n } else {\n // Unitless multiplier — compute for this element's font size\n // and mark as unitless so children re-compute\n const num = parseFloat(v);\n if (!isNaN(num)) {\n style.lineHeight = num * fontSize;\n (style as any)._lineHeightMultiplier = num;\n }\n }\n break;\n }\n case '-webkit-line-clamp':\n case 'line-clamp': {\n // Spec accepts `none` / `auto` / positive integer. We map both\n // `none` and `auto` to 0 (no clamp); a non-positive integer also\n // means no clamp. Otherwise store the integer.\n const v = value.trim().toLowerCase();\n if (v === 'none' || v === 'auto') {\n style.lineClamp = 0;\n } else {\n const n = parseInt(v, 10);\n style.lineClamp = Number.isFinite(n) && n > 0 ? n : 0;\n }\n break;\n }\n case 'vertical-align': style.verticalAlign = value.trim(); break;\n case 'white-space': style.whiteSpace = value.trim(); break;\n case 'word-break': style.wordBreak = value.trim(); break;\n case 'overflow-wrap':\n case 'word-wrap': style.overflowWrap = value.trim(); break;\n case 'direction': style.direction = value.trim(); break;\n case 'unicode-bidi': style.unicodeBidi = value.trim(); break;\n\n // Box model\n case 'display': style.display = value.trim(); break;\n case 'width': {\n const v = value.trim();\n if (v === '100%') style.width = containerWidth;\n else if (v !== 'auto') style.width = parseValue(v, fontSize, containerWidth);\n break;\n }\n case 'min-height': style.minHeight = parseValue(value, fontSize, containerWidth); break;\n case 'padding-top': style.paddingTop = parseValue(value, fontSize, containerWidth); break;\n case 'padding-right': style.paddingRight = parseValue(value, fontSize, containerWidth); break;\n case 'padding-bottom': style.paddingBottom = parseValue(value, fontSize, containerWidth); break;\n case 'padding-left': style.paddingLeft = parseValue(value, fontSize, containerWidth); break;\n case 'margin-top': style.marginTop = parseValue(value, fontSize, containerWidth); break;\n case 'margin-right': style.marginRight = parseValue(value, fontSize, containerWidth); break;\n case 'margin-bottom': style.marginBottom = parseValue(value, fontSize, containerWidth); break;\n case 'margin-left': style.marginLeft = parseValue(value, fontSize, containerWidth); break;\n case 'background-color': style.backgroundColor = value.trim(); break;\n case 'background': {\n const v = value.trim();\n if (v.includes('gradient(')) {\n // background: linear-gradient(...) → backgroundImage\n style.backgroundImage = v;\n } else if (v.startsWith('#') || v.startsWith('rgb') || v.startsWith('hsl') ||\n ['transparent', 'none', 'inherit'].includes(v) ||\n /^[a-z]+$/.test(v)) {\n style.backgroundColor = v;\n }\n break;\n }\n\n // Logical properties → physical (based on direction)\n case 'padding-inline-start':\n if (direction === 'rtl') style.paddingRight = parseValue(value, fontSize, containerWidth);\n else style.paddingLeft = parseValue(value, fontSize, containerWidth);\n break;\n case 'padding-inline-end':\n if (direction === 'rtl') style.paddingLeft = parseValue(value, fontSize, containerWidth);\n else style.paddingRight = parseValue(value, fontSize, containerWidth);\n break;\n case 'margin-inline-start':\n if (direction === 'rtl') style.marginRight = parseValue(value, fontSize, containerWidth);\n else style.marginLeft = parseValue(value, fontSize, containerWidth);\n break;\n case 'margin-inline-end':\n if (direction === 'rtl') style.marginLeft = parseValue(value, fontSize, containerWidth);\n else style.marginRight = parseValue(value, fontSize, containerWidth);\n break;\n\n // Border\n case 'border-top-width': style.borderTopWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-top-color': style.borderTopColor = value.trim(); break;\n case 'border-top-style': style.borderTopStyle = value.trim(); break;\n case 'border-right-width': style.borderRightWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-right-color': style.borderRightColor = value.trim(); break;\n case 'border-right-style': style.borderRightStyle = value.trim(); break;\n case 'border-bottom-width': style.borderBottomWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-bottom-color': style.borderBottomColor = value.trim(); break;\n case 'border-bottom-style': style.borderBottomStyle = value.trim(); break;\n case 'border-left-width': style.borderLeftWidth = parseValue(value, fontSize, containerWidth); break;\n case 'border-left-color': style.borderLeftColor = value.trim(); break;\n case 'border-left-style': style.borderLeftStyle = value.trim(); break;\n\n // Flex\n case 'flex-direction': style.flexDirection = value.trim(); break;\n case 'gap': style.gap = parseValue(value, fontSize, containerWidth); break;\n case 'flex-grow': style.flexGrow = parseFloat(value) || 0; break;\n\n // List\n case 'list-style-type': style.listStyleType = value.trim(); break;\n\n // Ignored properties (not relevant for our layout)\n case 'position':\n case 'top':\n case 'left':\n case 'right':\n case 'bottom':\n case 'inset-inline-start':\n case 'inset-inline-end':\n case 'content':\n case 'counter-reset':\n case 'counter-increment':\n case 'border-radius':\n case 'border-top-left-radius':\n case 'border-top-right-radius':\n case 'border-bottom-left-radius':\n case 'border-bottom-right-radius':\n case 'cursor':\n case 'opacity':\n case 'overflow':\n case 'box-sizing':\n case 'outline':\n case 'transition':\n case 'transform':\n case 'font-stretch':\n case 'font-display':\n case 'src':\n case 'unicode-range':\n break;\n }\n}\n\n/** Inheritable property names (CSS kebab-case) mapped to ResolvedStyle keys */\nconst INHERITABLE_KEYS: [string, keyof ResolvedStyle][] = [\n ['font-family', 'fontFamily'],\n ['font-size', 'fontSize'],\n ['font-weight', 'fontWeight'],\n ['font-style', 'fontStyle'],\n ['color', 'color'],\n ['text-align', 'textAlign'],\n ['text-align-last', 'textAlignLast'],\n ['text-indent', 'textIndent'],\n ['text-transform', 'textTransform'],\n ['white-space', 'whiteSpace'],\n ['word-break', 'wordBreak'],\n ['overflow-wrap', 'overflowWrap'],\n ['direction', 'direction'],\n ['letter-spacing', 'letterSpacing'],\n ['word-spacing', 'wordSpacing'],\n ['line-height', 'lineHeight'],\n ['text-shadow', 'textShadow'],\n ['font-kerning', 'fontKerning'],\n ['list-style-type', 'listStyleType'],\n ['vertical-align', 'verticalAlign'],\n ['text-underline-offset', 'textUnderlineOffset'],\n ['paint-order', 'paintOrder'],\n ['stroke-linejoin', 'strokeLinejoin'],\n ['-webkit-text-stroke-width', 'webkitTextStrokeWidth'],\n ['-webkit-text-stroke-color', 'webkitTextStrokeColor'],\n ['-webkit-text-fill-color', 'webkitTextFillColor'],\n];\n\n/**\n * Inherit properties from parent style to child style for properties\n * not explicitly set (tracked via setProps).\n */\nfunction inheritFrom(child: ResolvedStyle, parent: ResolvedStyle, setProps: Set<string>): void {\n for (const [cssProp, key] of INHERITABLE_KEYS) {\n if (!setProps.has(cssProp)) {\n if (key === 'lineHeight') {\n // Unitless line-height: re-compute relative to child's font-size\n const multiplier = (parent as any)._lineHeightMultiplier;\n if (multiplier !== undefined) {\n child.lineHeight = multiplier * child.fontSize;\n (child as any)._lineHeightMultiplier = multiplier;\n } else {\n child.lineHeight = parent.lineHeight;\n }\n } else if (key === 'textUnderlineOffset') {\n // Percentage offset: re-resolve against the child's own font size\n // (Chrome-measured), same pattern as the line-height multiplier.\n const pct = (parent as any)._underlineOffsetPct;\n if (pct !== undefined) {\n child.textUnderlineOffset = (pct / 100) * child.fontSize;\n (child as any)._underlineOffsetPct = pct;\n } else {\n child.textUnderlineOffset = parent.textUnderlineOffset;\n }\n } else {\n (child as any)[key] = (parent as any)[key];\n }\n }\n }\n}\n\n// ─── Main resolver ───────────────────────────────────────────────────\n\ninterface MatchedDeclaration {\n property: string;\n value: string;\n specificity: [number, number, number];\n order: number;\n important: boolean;\n}\n\n/** A pre-processed rule entry with parsed selector and pre-expanded declarations */\ninterface ProcessedRule {\n selector: ParsedSelector;\n declarations: { property: string; value: string; important: boolean }[];\n /** Global order for cascade sorting */\n orderBase: number;\n}\n\n/**\n * Build an index of processed rules keyed by rightmost tag name and class names.\n * The '*' key holds rules that match any element (no tag or class constraint).\n */\nfunction buildRuleIndex(rules: CSSRule[]): {\n byTag: Map<string, ProcessedRule[]>;\n byClass: Map<string, ProcessedRule[]>;\n universal: ProcessedRule[];\n} {\n const byTag = new Map<string, ProcessedRule[]>();\n const byClass = new Map<string, ProcessedRule[]>();\n const universal: ProcessedRule[] = [];\n let orderBase = 0;\n\n for (const rule of rules) {\n // Pre-expand declarations once\n const expandedDecls: { property: string; value: string; important: boolean }[] = [];\n for (const decl of rule.declarations) {\n const isImportant = decl.value.includes('!important');\n const cleanValue = isImportant\n ? decl.value.replace(/\\s*!important\\s*/g, '').trim()\n : decl.value;\n const expanded = expandShorthand(decl.property, cleanValue);\n for (const exp of expanded) {\n expandedDecls.push({ property: exp.property, value: exp.value, important: isImportant });\n }\n }\n\n for (const sel of rule.selectors) {\n const parsed = parseSelector(sel);\n if (!parsed) continue;\n\n const entry: ProcessedRule = {\n selector: parsed,\n declarations: expandedDecls,\n orderBase: orderBase++,\n };\n\n const rm = parsed.rightmost;\n if (rm.tag && !parsed.rightmostIsRoot) {\n // Index by tag\n const list = byTag.get(rm.tag);\n if (list) list.push(entry);\n else byTag.set(rm.tag, [entry]);\n }\n if (rm.classes.length > 0) {\n // Index by first class (most selective)\n const cls = rm.classes[0];\n const list = byClass.get(cls);\n if (list) list.push(entry);\n else byClass.set(cls, [entry]);\n }\n if (!rm.tag && rm.classes.length === 0) {\n // Universal selector or html/body root\n universal.push(entry);\n }\n // Also add root-matching selectors to universal\n if (parsed.rightmostIsRoot) {\n universal.push(entry);\n }\n }\n }\n\n return { byTag, byClass, universal };\n}\n\n/** Format an integer using a CSS list-style-type. */\nfunction formatListMarker(n: number, type: string): string {\n switch (type) {\n case 'disc': return '•';\n case 'circle': return '○';\n case 'square': return '■';\n case 'none': return '';\n case 'decimal-leading-zero':\n return `${n < 10 && n >= 0 ? '0' + n : n}.`;\n case 'lower-roman': return `${toRoman(n).toLowerCase()}.`;\n case 'upper-roman': return `${toRoman(n)}.`;\n case 'lower-alpha':\n case 'lower-latin': return `${toAlpha(n).toLowerCase()}.`;\n case 'upper-alpha':\n case 'upper-latin': return `${toAlpha(n)}.`;\n case 'decimal':\n default:\n return `${n}.`;\n }\n}\n\nfunction toRoman(n: number): string {\n if (n < 1 || n > 3999) return `${n}`;\n const map: [number, string][] = [\n [1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],\n [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],\n [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I'],\n ];\n let out = '';\n for (const [v, s] of map) {\n while (n >= v) { out += s; n -= v; }\n }\n return out;\n}\n\nfunction toAlpha(n: number): string {\n if (n < 1) return `${n}`;\n let out = '';\n while (n > 0) {\n const r = (n - 1) % 26;\n out = String.fromCharCode(65 + r) + out;\n n = Math.floor((n - 1) / 26);\n }\n return out;\n}\n\n/**\n * Detect list marker text for a <li> element based on tree position,\n * honoring list-style-type, <ol start>, <ol reversed>, and <li value>.\n */\nfunction getListMarker(el: Element, listStyleType: string): string | undefined {\n const tag = el.tagName.toLowerCase();\n if (tag !== 'li') return undefined;\n if (listStyleType === 'none') return '';\n\n const parent = el.parentElement;\n const parentTag = parent?.tagName.toLowerCase();\n\n // Bullet markers: independent of position.\n if (listStyleType === 'disc' || listStyleType === 'circle' || listStyleType === 'square') {\n return formatListMarker(0, listStyleType);\n }\n\n // Numbered markers: compute index from siblings + ol attributes + li value.\n if (parentTag === 'ol' || parentTag === 'ul' || !parent) {\n const liItems = parent\n ? Array.from(parent.children).filter(c => c.tagName.toLowerCase() === 'li')\n : [el];\n const startAttr = parent?.getAttribute('start');\n const reversed = parent?.hasAttribute('reversed') ?? false;\n const start = startAttr ? parseInt(startAttr, 10) : (reversed ? liItems.length : 1);\n const step = reversed ? -1 : 1;\n let n = start;\n for (const item of liItems) {\n const valueAttr = item.getAttribute('value');\n if (valueAttr) {\n const v = parseInt(valueAttr, 10);\n if (!Number.isNaN(v)) n = v;\n }\n if (item === el) return formatListMarker(n, listStyleType || 'decimal');\n n += step;\n }\n return formatListMarker(n, listStyleType || 'decimal');\n }\n\n return undefined;\n}\n\n/**\n * Inline-style access without `instanceof HTMLElement` — duck-typed so nodes\n * from non-browser DOMs (linkedom, jsdom) qualify without their constructors\n * being installed as globals.\n */\nfunction inlineStyleOf(el: Element): CSSStyleDeclaration | null {\n const style = (el as HTMLElement).style;\n return style && typeof style.cssText === 'string' ? style : null;\n}\n\n/**\n * Parse inline style attribute into declarations.\n */\nfunction parseInlineStyle(styleAttr: string): CSSDeclaration[] {\n const declarations: CSSDeclaration[] = [];\n for (const decl of styleAttr.split(';')) {\n const colonIdx = decl.indexOf(':');\n if (colonIdx === -1) continue;\n const property = decl.slice(0, colonIdx).trim().toLowerCase();\n const value = decl.slice(colonIdx + 1).trim();\n if (property && value) {\n declarations.push({ property, value });\n }\n }\n return declarations;\n}\n\n/**\n * Resolve styles for a DOM tree without inserting into the document.\n * Parses CSS rules, matches selectors, resolves cascade + inheritance.\n */\nexport function resolveStylesFromCSS(\n fragment: DocumentFragment,\n css: string,\n containerWidth: number,\n): { tree: StyledNode; cleanup: () => void } {\n const { rules, fontFaceRules } = parseCSS(css);\n\n // Inject @font-face rules into the live document so fonts can load.\n // Browser-only side effect: in non-browser environments there is no font\n // loader to trigger, so skip silently (fonts come from the consumer there).\n let fontStyleEl: HTMLStyleElement | null = null;\n if (fontFaceRules.length > 0 && typeof document !== 'undefined' && document.head) {\n fontStyleEl = document.createElement('style');\n fontStyleEl.textContent = fontFaceRules.join('\\n');\n document.head.appendChild(fontStyleEl);\n }\n\n // Build indexed rule lookup\n const ruleIndex = buildRuleIndex(rules);\n\n // Wrap fragment in a container div so resolveElement has a single root\n // Element. Created from the fragment's own document so no ambient DOM is\n // required (the tree is never inserted into the live document).\n const container = fragment.ownerDocument!.createElement('div');\n container.appendChild(fragment);\n\n function buildContext(el: Element, parent: ElementContext | null): ElementContext {\n const classes = new Set<string>();\n const className = el.getAttribute('class');\n if (className) {\n for (const c of className.split(/\\s+/)) {\n if (c) classes.add(c);\n }\n }\n return {\n tagName: el.tagName.toLowerCase(),\n classes,\n parent,\n el,\n };\n }\n\n function resolveElement(\n el: Element,\n parentStyle: ResolvedStyle,\n parentCtx: ElementContext | null,\n ): StyledNode {\n const tag = el.tagName.toLowerCase();\n const ctx = buildContext(el, parentCtx);\n\n // Start with defaults\n const style = defaultStyle();\n\n // Track which properties are explicitly set (tag defaults, CSS rules, inline styles)\n const setProps = new Set<string>();\n\n // --- Step 1: Determine font-size first (needed for em/multiplier resolution) ---\n\n // Collect candidate rules from index (only rules that could match this element)\n const candidates: ProcessedRule[] = [];\n const seen = new Set<ProcessedRule>();\n\n const tagRules = ruleIndex.byTag.get(tag);\n if (tagRules) for (const r of tagRules) { seen.add(r); candidates.push(r); }\n\n for (const cls of ctx.classes) {\n const clsRules = ruleIndex.byClass.get(cls);\n if (clsRules) for (const r of clsRules) {\n if (!seen.has(r)) { seen.add(r); candidates.push(r); }\n }\n }\n\n for (const r of ruleIndex.universal) {\n if (!seen.has(r)) { seen.add(r); candidates.push(r); }\n }\n\n // Match candidates and collect pre-expanded declarations.\n // Rules with `::marker` are routed to a separate list and applied to the\n // <li>'s markerStyle later — they do not affect the element body.\n const matched: MatchedDeclaration[] = [];\n const matchedMarker: MatchedDeclaration[] = [];\n for (const candidate of candidates) {\n if (matchesParsedSelector(candidate.selector, ctx)) {\n const target = candidate.selector.pseudoElement === 'marker' ? matchedMarker : matched;\n for (const decl of candidate.declarations) {\n target.push({\n property: decl.property,\n value: decl.value,\n specificity: candidate.selector.spec,\n order: candidate.orderBase,\n important: decl.important,\n });\n }\n }\n }\n\n // Tag default font-size\n const tagDef = TAG_DEFAULTS[tag];\n let fontSizeSet = false;\n if (tagDef?.fontSize !== undefined) {\n const val = tagDef.fontSize as number;\n if (val < 10) {\n style.fontSize = val * parentStyle.fontSize;\n } else {\n style.fontSize = val;\n }\n fontSizeSet = true;\n setProps.add('font-size');\n }\n\n // Sort by: !important first, then specificity, then source order\n if (matched.length > 1) {\n matched.sort((a, b) => {\n if (a.important !== b.important) return a.important ? 1 : -1;\n const sa = a.specificity, sb = b.specificity;\n if (sa[0] !== sb[0]) return sa[0] - sb[0];\n if (sa[1] !== sb[1]) return sa[1] - sb[1];\n if (sa[2] !== sb[2]) return sa[2] - sb[2];\n return a.order - b.order;\n });\n }\n\n // Apply font-size from CSS rules\n for (const m of matched) {\n if (m.property === 'font-size') {\n applyDeclaration(style, m.property, m.value, parentStyle.fontSize, containerWidth, parentStyle.direction);\n fontSizeSet = true;\n }\n }\n\n // Apply font-size from inline styles\n const elStyle = inlineStyleOf(el);\n if (elStyle && elStyle.cssText) {\n const inlineDecls = parseInlineStyle(elStyle.cssText);\n for (const decl of inlineDecls) {\n if (decl.property === 'font-size') {\n applyDeclaration(style, decl.property, decl.value, parentStyle.fontSize, containerWidth, parentStyle.direction);\n fontSizeSet = true;\n }\n }\n }\n\n // Inherit font-size from parent if not set\n if (!fontSizeSet) {\n style.fontSize = parentStyle.fontSize;\n }\n\n // Now style.fontSize is the element's computed font-size\n const elemFontSize = style.fontSize;\n\n // --- Step 2: Apply all other properties using resolved font-size ---\n\n // Apply non-fontSize tag defaults\n if (tagDef) {\n for (const [key, val] of Object.entries(tagDef)) {\n if (key === 'fontSize') continue; // already handled\n (style as any)[key] = val;\n const cssKey = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());\n setProps.add(cssKey);\n }\n\n // Resolve negative margin values (em multipliers from tag defaults)\n if (style.marginTop < 0) style.marginTop = Math.abs(style.marginTop) * elemFontSize;\n if (style.marginBottom < 0) style.marginBottom = Math.abs(style.marginBottom) * elemFontSize;\n\n // Default padding-inline-start for lists (direction-aware)\n if (tag === 'ul' || tag === 'ol') {\n const dir = parentStyle.direction;\n if (dir === 'rtl') {\n style.paddingRight = 40;\n setProps.add('padding-right');\n } else {\n style.paddingLeft = 40;\n setProps.add('padding-left');\n }\n }\n }\n\n // Determine direction from parent for logical property resolution\n const direction = parentStyle.direction;\n\n // Property aliases: CSS name → canonical name for setProps tracking\n const PROP_ALIASES: Record<string, string> = {\n 'word-wrap': 'overflow-wrap',\n };\n\n // Apply matched CSS declarations (skip font-size, already applied)\n for (const m of matched) {\n if (m.property === 'font-size') continue;\n applyDeclaration(style, m.property, m.value, elemFontSize, containerWidth, direction);\n setProps.add(PROP_ALIASES[m.property] || m.property);\n }\n\n // Apply inline styles (highest specificity, skip font-size)\n const hasInlineWidth = !!elStyle?.width;\n if (elStyle && elStyle.cssText) {\n const inlineDecls = parseInlineStyle(elStyle.cssText);\n for (const decl of inlineDecls) {\n if (decl.property === 'font-size') {\n setProps.add('font-size');\n continue;\n }\n const expanded = expandShorthand(decl.property, decl.value);\n for (const exp of expanded) {\n applyDeclaration(style, exp.property, exp.value, elemFontSize, containerWidth, direction);\n setProps.add(PROP_ALIASES[exp.property] || exp.property);\n }\n }\n }\n\n // Only keep explicit width from inline styles (match DOM resolver behavior)\n if (!hasInlineWidth) {\n style.width = 0;\n }\n\n // Handle `dir` attribute\n const dirAttr = el.getAttribute('dir');\n if (dirAttr) {\n style.direction = dirAttr;\n setProps.add('direction');\n }\n\n // Inherit from parent for properties not explicitly set\n setProps.add('font-size'); // already resolved\n inheritFrom(style, parentStyle, setProps);\n\n // Auto-set currentColor defaults (browser default behavior).\n // Decorations: with no explicit text-decoration-color, Chrome paints the\n // line with -webkit-text-fill-color when that is set (measured: red color +\n // blue fill-color + <u> → blue underline; transparent fill-color → the\n // decoration disappears with the glyphs), falling back to `color`.\n if (!setProps.has('text-decoration-color')) {\n style.textDecorationColor = style.webkitTextFillColor || style.color;\n } else if (style.textDecorationColor === 'currentColor') {\n style.textDecorationColor = style.color;\n }\n for (const side of ['Top', 'Right', 'Bottom', 'Left'] as const) {\n const colorKey = `border${side}Color` as keyof ResolvedStyle;\n const propName = `border-${side.toLowerCase()}-color`;\n if (!setProps.has(propName)) {\n (style as any)[colorKey] = style.color;\n } else if ((style as any)[colorKey] === 'currentColor') {\n (style as any)[colorKey] = style.color;\n }\n }\n\n // Handle text-decoration inheritance (propagates visually, not via normal\n // inheritance). Each decoration keeps the color/style of the element that\n // DECLARED it (Chrome: a parent's red underline stays red across a blue\n // child <s>): ancestor entries ride along in `textDecorations`, own\n // entries are appended after them so they paint on top.\n // `textDecorationLine` stays the union of lines for cheap checks.\n const ownEntries: DecorationEntry[] = [];\n if (style.textDecorationLine && style.textDecorationLine !== 'none') {\n for (const d of style.textDecorationLine.split(/\\s+/)) {\n if (d && d !== 'none') {\n ownEntries.push({\n line: d,\n color: style.textDecorationColor,\n style: style.textDecorationStyle,\n // This element is the decorating box for every descendant the\n // entry rides down to.\n declarer: style,\n });\n }\n }\n }\n style.textDecorations = parentStyle.textDecorations.length\n ? [...parentStyle.textDecorations, ...ownEntries]\n : ownEntries;\n const decoSet = new Set(style.textDecorationLine.split(/\\s+/).filter(d => d && d !== 'none'));\n if (parentStyle.textDecorationLine && parentStyle.textDecorationLine !== 'none') {\n for (const d of parentStyle.textDecorationLine.split(/\\s+/)) {\n if (d && d !== 'none') decoSet.add(d);\n }\n }\n if (decoSet.size > 0) {\n style.textDecorationLine = [...decoSet].join(' ');\n }\n\n // List marker\n const marker = getListMarker(el, style.listStyleType);\n\n // Resolve `::marker` rules into a Partial<ResolvedStyle> override and a\n // hidden flag. We only do this for `<li>` because `::marker` only applies\n // to elements with `display: list-item` (in our model, just `<li>`).\n // The override records ONLY the keys actually written by marker\n // declarations, so the layout consumer can distinguish \"user set padding\n // to 0\" from \"no rule\".\n let markerStyle: Partial<ResolvedStyle> | undefined;\n let markerHidden = false;\n if (tag === 'li' && matchedMarker.length > 0) {\n // Sort by cascade order — same rules as element style.\n if (matchedMarker.length > 1) {\n matchedMarker.sort((a, b) => {\n if (a.important !== b.important) return a.important ? 1 : -1;\n const sa = a.specificity, sb = b.specificity;\n if (sa[0] !== sb[0]) return sa[0] - sb[0];\n if (sa[1] !== sb[1]) return sa[1] - sb[1];\n if (sa[2] !== sb[2]) return sa[2] - sb[2];\n return a.order - b.order;\n });\n }\n\n // Apply to a scratch style cloned from the resolved <li> style, then\n // copy out the keys that changed. Whitelist the physical fields we\n // actually consume in addListMarker — adding more later is a one-line\n // change once the layout side reads them.\n const TRACKED: (keyof ResolvedStyle)[] = [\n 'paddingLeft', 'paddingRight',\n 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle',\n 'color', 'letterSpacing',\n ];\n const scratch = { ...style } as ResolvedStyle;\n const touched = new Set<keyof ResolvedStyle>();\n for (const m of matchedMarker) {\n // `content: none` (and `content: ''`) suppresses the marker entirely,\n // matching DOM `::marker` behavior. `content` isn't part of\n // ResolvedStyle, so we handle it inline.\n if (m.property === 'content') {\n const v = m.value.trim().toLowerCase();\n if (v === 'none' || v === '\"\"' || v === \"''\" || v === 'normal') {\n // 'normal' is the initial value — no override\n markerHidden = (v === 'none' || v === '\"\"' || v === \"''\");\n }\n continue;\n }\n const before = TRACKED.map(k => scratch[k]);\n applyDeclaration(scratch, m.property, m.value, elemFontSize, containerWidth, direction);\n TRACKED.forEach((k, i) => {\n if (scratch[k] !== before[i]) touched.add(k);\n });\n }\n if (touched.size > 0) {\n markerStyle = {};\n for (const k of touched) (markerStyle as any)[k] = scratch[k];\n }\n }\n\n // Walk children\n const children: StyledNode[] = [];\n for (const child of el.childNodes) {\n const childNode = walkNode(child, style, ctx);\n if (childNode) children.push(childNode);\n }\n\n return {\n element: el,\n tagName: tag,\n style,\n children,\n textContent: null,\n listMarker: marker,\n markerStyle,\n markerHidden: markerHidden || undefined,\n };\n }\n\n function walkNode(\n node: Node,\n parentStyle: ResolvedStyle,\n parentCtx: ElementContext | null,\n ): StyledNode | null {\n if (node.nodeType === TEXT_NODE) {\n const text = node.textContent;\n if (!text) return null;\n\n if (text.trim() === '' && !text.includes('\\u00A0')) {\n const ws = parentStyle.whiteSpace;\n const prev = node.previousSibling;\n const next = node.nextSibling;\n const isInlineSibling = (n: Node | null) => {\n if (!n || n.nodeType !== ELEMENT_NODE) return n?.nodeType === TEXT_NODE;\n const tag = (n as Element).tagName.toLowerCase();\n const def = TAG_DEFAULTS[tag];\n const d = def?.display || 'block';\n return d === 'inline' || d === 'inline-block';\n };\n\n if (prev && next && !isInlineSibling(prev) && !isInlineSibling(next)) {\n if (ws === 'pre' || ws === 'pre-wrap' || ws === 'pre-line') {\n // Keep\n } else {\n return null;\n }\n }\n\n if (ws !== 'pre' && ws !== 'pre-wrap' && ws !== 'pre-line') {\n if (text.includes('\\n')) return null;\n }\n }\n\n // Clone parent style for text node (text nodes don't match CSS rules)\n const style = { ...parentStyle };\n\n // CSS Text 3 §4.1.1: in `normal` and `nowrap`, a source newline is\n // collapsed to a single space (no forced break). Only `pre`,\n // `pre-wrap`, `pre-line`, and `break-spaces` preserve newlines.\n // <br>-derived text nodes are created separately below with `\\n`\n // and are not touched here, so they keep forcing breaks.\n const ws = parentStyle.whiteSpace;\n let normalizedText = text;\n if (ws !== 'pre' && ws !== 'pre-wrap' && ws !== 'pre-line' && ws !== 'break-spaces') {\n normalizedText = text.replace(/[\\n\\r]/g, ' ');\n }\n\n return {\n element: null,\n tagName: '#text',\n style,\n children: [],\n textContent: normalizedText,\n };\n }\n\n if (node.nodeType !== ELEMENT_NODE) return null;\n\n const el = node as Element;\n const tag = el.tagName.toLowerCase();\n if (tag === 'style' || tag === 'script') return null;\n\n // <br> → text node with newline\n if (tag === 'br') {\n return {\n element: null,\n tagName: '#text',\n style: { ...parentStyle },\n children: [],\n textContent: '\\n',\n };\n }\n\n return resolveElement(el, parentStyle, parentCtx);\n }\n\n const rootStyle = defaultStyle();\n const tree = resolveElement(container, rootStyle, null);\n\n const cleanup = () => {\n if (fontStyleEl) fontStyleEl.remove();\n };\n\n return { tree, cleanup };\n}\n","import type { StyledNode, LayoutNode, LayoutBox, LayoutText, ResolvedStyle, LayoutLine, DecorationEntry } from './types.js';\n\n// Module-level flag controlling DOM measurement usage.\n// Set by buildLayoutTree() based on the useDomMeasurements option.\nlet _useDomMeasurements = true;\nlet _debug: ((entry: import('./types.ts').DebugEntry) => void) | undefined;\n\n// Lines emitted during layout. Reset at the start of buildLayoutTree();\n// layoutInlineContent appends one entry per committed line.\nlet _lines: LayoutLine[] = [];\n\n// ─── measureText width cache ──────────────────────────────────────────\n// Caches ctx.measureText(text).width keyed by \"font\\0text\".\n// Cleared at the start of each buildLayoutTree() call.\nconst _measureCache = new Map<string, number>();\n\nfunction cachedMeasureWidth(ctx: CanvasRenderingContext2D, text: string): number {\n // ctx.font and ctx.letterSpacing must already be set by caller.\n // letterSpacing is part of the key because it changes measured width.\n const key = ctx.font + '\\0' + (ctx.letterSpacing || '') + '\\0' + text;\n const cached = _measureCache.get(key);\n if (cached !== undefined) return cached;\n const w = ctx.measureText(text).width;\n _measureCache.set(key, w);\n return w;\n}\n\n\n/**\n * Check if a line has mixed fonts (different fontFamily/fontSize/fontWeight/fontStyle).\n */\nfunction hasMixedFonts(words: Word[]): boolean {\n let font = '';\n for (const w of words) {\n if (!w.text || w.isSpace) continue;\n const f = buildCanvasFont(w.style);\n if (font && f !== font) return true;\n font = f;\n }\n return false;\n}\n\n// ─── Canvas font helpers ───────────────────────────────────────────────\n\n/**\n * Set canvas font and kerning from resolved style.\n */\nexport function applyFont(ctx: CanvasRenderingContext2D, style: ResolvedStyle): void {\n ctx.font = buildCanvasFont(style);\n ctx.fontKerning = style.fontKerning === 'none' ? 'none' : 'normal';\n}\n\n/** Format a letter-spacing value (px) as a canvas `ctx.letterSpacing` string. */\nfunction formatLetterSpacing(value: number): string {\n // Negative letter-spacing is valid and narrows text — Chrome applies it per\n // character (trailing included). Clamping it to 0 measured text wider than\n // the browser renders it, causing earlier/extra line wraps. Guard against\n // non-finite values (undefined/NaN), which would produce an invalid\n // \"undefinedpx\"/\"NaNpx\" string that canvas silently ignores.\n return Number.isFinite(value) && value !== 0 ? `${value}px` : '0px';\n}\n\n/**\n * Build a canvas font string from resolved style. Results are cached.\n */\nconst _fontStringCache = new Map<string, string>();\nexport function buildCanvasFont(style: ResolvedStyle): string {\n const key = `${style.fontStyle}|${style.fontVariantCaps}|${style.fontWeight}|${style.fontSize}|${style.fontFamily}`;\n const cached = _fontStringCache.get(key);\n if (cached) return cached;\n const parts: string[] = [];\n // CSS font shorthand order: style, variant, weight, size, family.\n if (style.fontStyle !== 'normal') parts.push(style.fontStyle);\n if (style.fontVariantCaps === 'small-caps') parts.push('small-caps');\n if (style.fontWeight !== 400) parts.push(String(style.fontWeight));\n parts.push(`${style.fontSize}px`);\n parts.push(style.fontFamily);\n const result = parts.join(' ');\n _fontStringCache.set(key, result);\n return result;\n}\n\n/**\n * Cache for DOM-measured line heights.\n * Key: \"font|lineHeight|probeType\" → actual pixel height from the browser.\n */\nconst _lineHeightCache = new Map<string, number>();\n\n// Probe elements: a <div> for general use, and a <ul><li> for unordered list items.\n// Firefox renders <ul><li> with bullet markers (disc/circle/square) 1.5px taller\n// than other elements for the same line-height, due to the ::marker pseudo-element.\n// <ol><li> items do NOT have this extra height.\nlet _blockProbe: HTMLDivElement | null = null;\nlet _ulProbeContainer: HTMLUListElement | null = null;\nlet _ulProbeLi: HTMLLIElement | null = null;\n\nconst BULLET_MARKERS = new Set(['disc', 'circle', 'square']);\n\n/**\n * Measure the actual line height using a hidden DOM element.\n * Uses an actual <li> inside a <ul> when listStyleType is a bullet marker\n * (disc/circle/square) to capture Firefox's ::marker line box contribution.\n * Results are cached per font+lineHeight+probeType combination.\n */\nfunction measureDomLineHeight(font: string, lineHeight: string, useBulletProbe = false): number {\n const key = `${font}|${lineHeight}|${useBulletProbe ? 'ul-li' : 'block'}`;\n const cached = _lineHeightCache.get(key);\n if (cached !== undefined) return cached;\n\n if (typeof document === 'undefined' || !document.body) {\n throw new Error(\n \"render-tag: accuracy 'balanced' requires a browser DOM for line-height probes; use the default 'performance' mode in non-browser environments.\"\n );\n }\n\n let probe: HTMLElement;\n if (useBulletProbe) {\n if (!_ulProbeContainer) {\n _ulProbeContainer = document.createElement('ul');\n _ulProbeContainer.style.cssText =\n 'position:absolute;top:-9999px;left:-9999px;visibility:hidden;padding:0;margin:0;border:0;list-style:disc;';\n _ulProbeLi = document.createElement('li');\n _ulProbeLi.style.cssText = 'white-space:nowrap;padding:0;margin:0;border:0;';\n _ulProbeLi.textContent = 'Mg';\n _ulProbeContainer.appendChild(_ulProbeLi);\n document.body.appendChild(_ulProbeContainer);\n }\n probe = _ulProbeLi!;\n } else {\n if (!_blockProbe) {\n _blockProbe = document.createElement('div');\n _blockProbe.style.cssText =\n 'position:absolute;top:-9999px;left:-9999px;visibility:hidden;white-space:nowrap;padding:0;margin:0;border:0;';\n _blockProbe.textContent = 'Mg';\n document.body.appendChild(_blockProbe);\n }\n probe = _blockProbe;\n }\n\n probe.style.font = font;\n probe.style.lineHeight = lineHeight;\n const height = probe.getBoundingClientRect().height;\n\n _lineHeightCache.set(key, height);\n return height;\n}\n\n/**\n * Get the effective line height for a style.\n * Uses DOM measurement for accuracy across browsers (Firefox vs Chrome).\n * Falls back to canvas metrics for \"normal\" line-height.\n */\nfunction getLineHeight(ctx: CanvasRenderingContext2D, style: ResolvedStyle, useBulletProbe = false): number {\n if (style.lineHeight > 0) {\n if (_useDomMeasurements) {\n const font = buildCanvasFont(style);\n return measureDomLineHeight(font, `${style.lineHeight}px`, useBulletProbe);\n }\n // Canvas-only: use the CSS line-height value directly\n return style.lineHeight;\n }\n\n if (_useDomMeasurements) {\n const font = buildCanvasFont(style);\n return measureDomLineHeight(font, 'normal', useBulletProbe);\n }\n\n // Canvas-only fallback for \"normal\" line-height: use font bounding box\n // fontBoundingBoxAscent + fontBoundingBoxDescent already represents the\n // full line box height, no multiplier needed.\n const { ascent, descent } = getFontMetrics(ctx, style);\n return ascent + descent;\n}\n\n/**\n * Which engine's line rules to follow. Only the UA string can say, because\n * `accuracy: 'performance'` promises not to touch the DOM.\n *\n * Blink is the DEFAULT, and the other two are what we detect: a server-side\n * render (no navigator, or jsdom) targets headless Chrome, so anything we\n * cannot positively identify has to round the way Chrome does.\n *\n * - Gecko is the one engine that still sends a real `Gecko/<date>` product\n * token; Blink and WebKit carry only the \"like Gecko\" comment, no slash.\n * - Safari is WebKit that says neither `Chrome/` nor `jsdom/`. jsdom borrows\n * WebKit's UA and would otherwise be mistaken for it.\n * - `Chrome/` is matched with NO word boundary, because headless Chrome sends\n * `HeadlessChrome/`.\n */\nconst UA = typeof navigator === 'undefined' ? '' : navigator.userAgent;\nconst IS_GECKO = /\\bGecko\\/\\d/.test(UA);\nconst IS_SAFARI =\n /AppleWebKit/.test(UA) && !/Chrome\\/\\d/.test(UA) && !/\\bjsdom\\//.test(UA);\n\n/**\n * True where the engine floors a line's baseline onto a whole CSS pixel.\n *\n * Blink alone does (`FontHeight::AddLeading`). Gecko and WebKit both lay the\n * exact half-leading out — measured over the whole 530-case corpus, giving\n * Safari the Blink branch cost 214 wins against 223 losses (avg 7.50% ->\n * 9.21%) where the exact value wins 48 against 3 (7.50% -> 6.52%).\n */\nexport const FLOORS_LINE_BASELINE = !IS_GECKO && !IS_SAFARI;\n\n/**\n * `super` and `sub` are engine constants, not CSS. Blink and WebKit share\n * theirs (`fontSize/3 + 1`, `fontSize/5 + 1`); Gecko raises by 0.34em and\n * lowers by 0.20em. A SEPARATE question from the rounding above — Safari\n * rounds like nobody and shifts like Blink — so never gate one on the other.\n */\nexport const BLINK_SUPER_SUB = !IS_GECKO;\n\n/**\n * Baseline offset from the top of a line box, the way the engine places it:\n * the half-leading `(lineHeight - (ascent + descent)) / 2` below the line top,\n * plus the ascent, rounded as `FLOORS_LINE_BASELINE` says.\n *\n * Public API, because this is the ONE rule every renderer that places a\n * baseline beside a render-tag canvas has to share (@polotno/svg-export, the\n * editor's list marker). Call it rather than restate it, or the two drift.\n */\nexport function lineBaselineOffset(lineHeight: number, ascent: number, descent: number): number {\n const exact = (lineHeight - (ascent + descent)) / 2 + ascent;\n return FLOORS_LINE_BASELINE ? Math.floor(exact) : exact;\n}\n\n/**\n * The vertical space an inline-block's margin box adds around its content, over\n * and above the font's own leading. Written once because the wrap pass grows\n * the line by the same six values.\n */\nfunction inlineBlockExtra(bs: ResolvedStyle): { top: number; bottom: number } {\n return {\n top: bs.marginTop + bs.borderTopWidth + bs.paddingTop,\n bottom: bs.paddingBottom + bs.borderBottomWidth + bs.marginBottom,\n };\n}\n\n/**\n * One box's half of a line: how far it reaches above its own baseline and how\n * far below, over its OWN line-height. This is the inline box CSS 2.1 §10.8\n * talks about — the font's content area plus its half-leading — not the bare\n * font metrics. `vertical-align: text-top` and `text-bottom` align THIS box's\n * edges, and the line box is the union of these over everything on the line.\n */\nfunction leadedBox(\n ctx: CanvasRenderingContext2D,\n style: ResolvedStyle,\n useBulletProbe = false,\n): { ascent: number; descent: number } {\n const { ascent, descent } = getFontMetrics(ctx, style);\n const lineHeight = getLineHeight(ctx, style, useBulletProbe);\n const boxAscent = lineBaselineOffset(lineHeight, ascent, descent);\n return { ascent: boxAscent, descent: lineHeight - boxAscent };\n}\n\nfunction applyTextTransform(text: string, transform: string): string {\n\n switch (transform) {\n case 'uppercase': return text.toUpperCase();\n case 'lowercase': return text.toLowerCase();\n // Capitalize the first letter of each word. A mid-word apostrophe is NOT a\n // word boundary (UAX#29), so \"o'clock\" → \"O'clock\", not \"O'Clock\".\n case 'capitalize': return text.replace(/(^|[\\s\\p{P}])(\\p{L})/gu, (m, p, c) =>\n p === \"'\" || p === '’' ? m : p + c.toUpperCase());\n default: return text;\n }\n}\n\nfunction isInline(node: StyledNode): boolean {\n if (node.tagName === '#text') return true;\n const d = node.style.display;\n return d === 'inline' || d === 'inline-block';\n}\n\nfunction hasOnlyInlineChildren(node: StyledNode): boolean {\n return node.children.length > 0 && node.children.every(isInline);\n}\n\nexport function isTransparent(color: string): boolean {\n return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';\n}\n\n/**\n * Get font ascent and descent metrics. Results are cached per font string.\n */\nconst _fontMetricsCache = new Map<string, { ascent: number; descent: number }>();\nexport function getFontMetrics(ctx: CanvasRenderingContext2D, style: ResolvedStyle): { ascent: number; descent: number } {\n const font = buildCanvasFont(style);\n const cached = _fontMetricsCache.get(font);\n if (cached) return cached;\n // Restore what the caller had set: this measures with its OWN font, and a\n // measurement must not move the ctx. Leaving it moved made the function\n // behave differently on a cache miss than on a hit, so a caller that set a\n // font and then measured through this was correct only while the cache was\n // warm — `addListMarker` measured its marker on the li's face on a cold one.\n const prev = ctx.font;\n ctx.font = font;\n const m = ctx.measureText('M');\n ctx.font = prev;\n const ascent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;\n const descent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;\n const result = { ascent, descent };\n _fontMetricsCache.set(font, result);\n return result;\n}\n\n/**\n * Baseline shift (canvas pixels, positive = downward) for a vertical-align\n * value, applied on top of the line baseline. Returns 0 for 'baseline' and for\n * the line-box-relative keywords 'top'/'bottom' — those need a second layout\n * pass (the box position depends on the final line box it helps size), so they\n * fall back to baseline rather than being approximated wrongly.\n *\n * - super/sub the engine's own rule, measured off the DOM across\n * 8-56px × sans-serif/serif/monospace and fitting every\n * point to within 0.06px (LayoutUnit's 1/64). Neither\n * engine reads the font's metrics — the family does not\n * move the number.\n * - text-top/-bottom the box's LEADED edge against the parent's CONTENT-area\n * edge (bare ascent/descent, no leading). Taking the box's\n * bare metrics instead costs 25px on a line holding both.\n * - middle box midpoint at parent baseline + half the x-height\n * - <length>/<%> raise (positive value) by the length / % of line-height\n */\nfunction verticalAlignShift(\n va: string,\n ctx: CanvasRenderingContext2D, style: ResolvedStyle, parentStyle: ResolvedStyle,\n useBulletProbe: boolean,\n): number {\n switch (va) {\n case 'super':\n return BLINK_SUPER_SUB\n ? -(parentStyle.fontSize / 3 + 1) : -parentStyle.fontSize * 0.34;\n case 'sub':\n return BLINK_SUPER_SUB\n ? parentStyle.fontSize / 5 + 1 : parentStyle.fontSize * 0.2;\n // Against the PARENT's content area (CSS 2.1 §10.8.1) — its bare\n // ascent/descent, no leading. Measured against Chrome, taking the line's\n // tallest box instead of the real parent put this 14px out.\n case 'text-top':\n return leadedBox(ctx, style, useBulletProbe).ascent - getFontMetrics(ctx, parentStyle).ascent;\n case 'text-bottom':\n return getFontMetrics(ctx, parentStyle).descent - leadedBox(ctx, style, useBulletProbe).descent;\n case 'middle': {\n const { ascent, descent } = getFontMetrics(ctx, style);\n return -(parentStyle.fontSize * 0.25) - (descent - ascent) / 2;\n }\n default: {\n // baseline / top / bottom / '' all parseFloat to NaN → 0, which is what\n // an unshifted run wants — the line-box pass calls this for every word.\n const n = parseFloat(va);\n if (!Number.isFinite(n)) return 0;\n // A percentage resolves against the ELEMENT's own line-height (CSS 2.1\n // §10.8.1), not the line's. Measured against Chrome: the line's put the\n // box 10px out on a line whose tallest run was not this one.\n return va.endsWith('%')\n ? -(n / 100) * getLineHeight(ctx, style, useBulletProbe)\n : -n;\n }\n }\n}\n\n/** True when a vertical-align value moves content off the baseline. */\nexport function isShiftedVAlign(va: string): boolean {\n return va !== 'baseline' && va !== 'top' && va !== 'bottom' && va !== '';\n}\n\n/**\n * Two entries put the band in the same place, at the same thickness — the\n * geometry half only, so each caller keeps comparing color its own way (raw\n * here, canonicalized in the path renderer, where `red` and `#ff0000` must\n * still share one dash phase).\n *\n * Identity settles the normal case: entries ride down the tree by reference,\n * so every run under one declarer holds the same object. Two SEPARATE\n * declarers still count as equal when they would draw the same band, which\n * keeps a shaping group whole across siblings that declare the same thing.\n */\nexport function sameDecorationBand(a: DecorationEntry, b: DecorationEntry): boolean {\n if (a === b) return true;\n const da = a.declarer, db = b.declarer;\n return (\n da.fontSize === db.fontSize &&\n da.fontFamily === db.fontFamily &&\n da.fontWeight === db.fontWeight &&\n da.fontStyle === db.fontStyle &&\n da.fontVariantCaps === db.fontVariantCaps &&\n // The declarer's own vertical-align decides which baseline an underline\n // hangs off, so two declarers that differ there draw two bands.\n da.verticalAlign === db.verticalAlign &&\n // Explicit offset/thickness are band geometry too — two declarers that\n // differ there must not merge into one band.\n da.textUnderlineOffset === db.textUnderlineOffset &&\n da.textDecorationThickness === db.textDecorationThickness\n );\n}\n\n/** Same decoration set: entries must match pairwise, so runs whose decorations\n * would paint differently don't merge and take the first one's band. */\nfunction sameDecorations(a: ResolvedStyle, b: ResolvedStyle): boolean {\n const da = a.textDecorations, db = b.textDecorations;\n if (da === db) return true;\n if (!da || !db || da.length !== db.length) return false;\n for (let i = 0; i < da.length; i++) {\n if (\n da[i].line !== db[i].line ||\n da[i].color !== db[i].color ||\n da[i].style !== db[i].style ||\n !sameDecorationBand(da[i], db[i])\n ) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Check if two styles have the same text rendering properties.\n */\nfunction sameTextStyle(a: ResolvedStyle, b: ResolvedStyle): boolean {\n return a.fontFamily === b.fontFamily &&\n a.fontSize === b.fontSize &&\n a.fontWeight === b.fontWeight &&\n a.fontStyle === b.fontStyle &&\n a.color === b.color &&\n a.textDecorationLine === b.textDecorationLine &&\n sameDecorations(a, b) &&\n a.backgroundColor === b.backgroundColor;\n}\n\nfunction hasVisibleBoxStyles(style: ResolvedStyle): boolean {\n if (!isTransparent(style.backgroundColor)) return true;\n if (style.borderTopWidth > 0 && style.borderTopStyle !== 'none') return true;\n if (style.borderRightWidth > 0 && style.borderRightStyle !== 'none') return true;\n if (style.borderBottomWidth > 0 && style.borderBottomStyle !== 'none') return true;\n if (style.borderLeftWidth > 0 && style.borderLeftStyle !== 'none') return true;\n return false;\n}\n\n/** True for an element declaring `background-clip:text` with a visible\n * background (gradient image or solid color) — the fill/decorations of every\n * glyph it covers must sample that background instead of painting it as a box. */\nexport function hasTextClip(style: ResolvedStyle): boolean {\n return style.webkitBackgroundClip === 'text' &&\n ((!!style.backgroundImage && style.backgroundImage !== 'none') ||\n !isTransparent(style.backgroundColor));\n}\n\n// ─── Inline text run types ─────────────────────────────────────────────\n\ninterface TextRun {\n text: string;\n style: ResolvedStyle;\n /**\n * The style of the PARENT of the element this run's style came from — what\n * `vertical-align` measures its shift against (CSS 2.1 §10.8.1). Not the\n * tallest run on the line, which is what a line-level maximum would give:\n * a 40px sibling put a sup 8px out of place.\n */\n parentStyle?: ResolvedStyle;\n /** If this run came from an inline element with visible box styles */\n boxStyle?: ResolvedStyle;\n /** Marks the start of an inline box */\n boxOpen?: ResolvedStyle;\n /** Marks the end of an inline box */\n boxClose?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring background-clip:text + background */\n clipStyle?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring --rt-text-stroke-image */\n strokeImageStyle?: ResolvedStyle;\n}\n\ninterface Word {\n text: string;\n width: number;\n style: ResolvedStyle;\n /** See `TextRun.parentStyle`. */\n parentStyle?: ResolvedStyle;\n isSpace: boolean;\n /** Tab character — width computed dynamically based on position */\n isTab?: boolean;\n /** Word came from soft-hyphen split — show '-' if this word ends a line */\n isSoftHyphenBreak?: boolean;\n /**\n * No soft-wrap opportunity before this word: it abuts the previous word with\n * no whitespace (e.g. adjacent inline spans `<span>a</span><span>b</span>`),\n * so the browser treats them as one unbreakable unit at that boundary.\n */\n noBreakBefore?: boolean;\n boxStyle?: ResolvedStyle;\n /** Marks the start of an inline box (adds left padding/border) */\n boxOpen?: ResolvedStyle;\n /** Marks the end of an inline box (adds right padding/border) */\n boxClose?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring background-clip:text + background */\n clipStyle?: ResolvedStyle;\n /** Nearest inline ancestor-or-self declaring --rt-text-stroke-image */\n strokeImageStyle?: ResolvedStyle;\n}\n\ninterface PositionedLine {\n words: Word[];\n totalWidth: number;\n lineHeight: number;\n /** True if this line ends at a forced break (\\n or <br>). Such a line is\n * treated as a \"last line\" for text-align — never justified. */\n endedByHardBreak?: boolean;\n}\n\n/** True for atomic inline-block words (boxOpen && boxClose && text together). */\nfunction isAtomicInlineBlock(w: Word): boolean {\n return !!(w.boxOpen && w.boxClose && w.text);\n}\n\n/**\n * Truncate a PositionedLine's trailing words and append \"…\" so the line\n * fits within maxWidth. Used by `-webkit-line-clamp` to mark the visible\n * cut-off on the Nth line.\n *\n * Trim strategy:\n * 1. Pick the style of the last NON-empty, NON-atomic-inline-block word\n * — so the ellipsis font matches the surrounding text, not the button\n * or pill it was sitting next to.\n * 2. Drop trailing isSpace words (genuine spaces only — box markers carry\n * padding/border that we must keep).\n * 3. Back-trim: pop trailing non-space words until ellipsis fits. If we\n * end up with a single text word that STILL doesn't fit, pop it too —\n * the ellipsis stands alone rather than overflowing the container.\n * Box-open markers earlier on the line stay; they preserve inline-box\n * padding/border that the emit loop needs.\n * 4. Inherit boxStyle from the trailing context so inline `<span>`\n * backgrounds/borders extend across the ellipsis.\n */\nfunction applyEllipsisToLine(\n ctx: CanvasRenderingContext2D,\n line: PositionedLine,\n maxWidth: number,\n): void {\n // 1. Find the last word whose style should drive the ellipsis.\n // Skip empty-text markers AND atomic inline-blocks (their style is\n // the inline-block element's, not the surrounding text).\n let styleIdx = line.words.length - 1;\n while (\n styleIdx >= 0 &&\n (line.words[styleIdx].text === '' || isAtomicInlineBlock(line.words[styleIdx]))\n ) styleIdx--;\n if (styleIdx < 0) return;\n const lastStyle = line.words[styleIdx].style;\n const boxStyle = line.words[styleIdx].boxStyle;\n // The ellipsis takes the trimmed run's style, so it has to take the parent\n // that style's vertical-align measures against too.\n const parentStyle = line.words[styleIdx].parentStyle;\n applyFont(ctx, lastStyle);\n // ALWAYS assign (don't gate on truthy) — otherwise a previous segment's\n // non-zero letter-spacing leaks into the ellipsis measurement.\n ctx.letterSpacing = `${lastStyle.letterSpacing || 0}px` as any;\n const ellipsisWidth = cachedMeasureWidth(ctx, '…');\n\n // Helper: pop trailing isSpace words. Box markers (text === '' with\n // boxOpen/boxClose) are NOT popped — they carry inline-box padding the\n // emit loop relies on.\n const popTrailingSpaces = () => {\n while (\n line.words.length > 0 &&\n line.words[line.words.length - 1].isSpace\n ) {\n const r = line.words.pop()!;\n line.totalWidth -= r.width;\n }\n };\n\n // 2. Strip purely trailing whitespace.\n popTrailingSpaces();\n\n // 3. Back-trim non-space text words until the ellipsis fits.\n // Atomic inline-blocks are non-space too; they pop along with words.\n const isTrimmableText = (w: Word) =>\n !w.isSpace && w.text !== '' && !w.boxOpen && !w.boxClose;\n while (\n line.totalWidth + ellipsisWidth > maxWidth &&\n line.words.length > 0\n ) {\n const last = line.words[line.words.length - 1];\n if (!isTrimmableText(last) && !isAtomicInlineBlock(last)) break;\n line.totalWidth -= last.width;\n line.words.pop();\n popTrailingSpaces();\n }\n\n // 4. Append the ellipsis. Inherit boxStyle so inline-span backgrounds /\n // borders extend over the ellipsis.\n const ellipsisWord: Word = {\n text: '…',\n width: ellipsisWidth,\n style: lastStyle,\n parentStyle,\n isSpace: false,\n boxStyle,\n };\n line.words.push(ellipsisWord);\n line.totalWidth += ellipsisWidth;\n}\n\n// ─── Inline layout ─────────────────────────────────────────────────────\n\n/**\n * Collect text runs from inline children, preserving style and tracking\n * inline elements with visible backgrounds. Emits open/close markers\n * for inline boxes so padding/border can be applied.\n */\nfunction collectTextRuns(node: StyledNode): TextRun[] {\n const runs: TextRun[] = [];\n\n function walk(\n n: StyledNode,\n boxStyle?: ResolvedStyle,\n clipStyle?: ResolvedStyle,\n strokeImageStyle?: ResolvedStyle,\n parentStyle?: ResolvedStyle,\n ) {\n if (n.tagName === '#text' && n.textContent) {\n // A #text node carries its parent ELEMENT's style, so the element that\n // owns any vertical-align here is that parent — and what the shift\n // measures against is ITS parent, which is the `parentStyle` handed to\n // this element's walk.\n runs.push({\n text: n.textContent, style: n.style, parentStyle, boxStyle, clipStyle, strokeImageStyle,\n });\n return;\n }\n const isInlineBlock = n.style.display === 'inline-block';\n // Inline-block always needs box treatment (padding/margin affect layout)\n const isBox = isInlineBlock || (isInline(n) && hasVisibleBoxStyles(n.style));\n const newBoxStyle = isBox ? n.style : boxStyle;\n // Track the nearest inline element declaring a background-clip:text\n // background or a --rt-text-stroke-image, so those paints reach descendant\n // runs that don't carry the (non-inheriting) properties themselves.\n const newClipStyle = isInline(n) && hasTextClip(n.style) ? n.style : clipStyle;\n const newStrokeImageStyle =\n isInline(n) && n.style.webkitTextStrokeImage && n.style.webkitTextStrokeImage !== 'none'\n ? n.style : strokeImageStyle;\n const hasHorizSpacing = isBox && (n.style.paddingLeft > 0 || n.style.paddingRight > 0 ||\n n.style.borderLeftWidth > 0 || n.style.borderRightWidth > 0);\n\n if (isInlineBlock) {\n // Inline-block is fully atomic — the entire element (margins + padding + text)\n // wraps as one unit. We emit a single \"atomic\" TextRun with a special marker\n // so the tokenizer creates one non-splittable word with the full box width.\n const allText = n.element?.textContent || '';\n runs.push({\n text: allText,\n style: n.style,\n parentStyle,\n boxStyle: newBoxStyle,\n clipStyle: newClipStyle,\n strokeImageStyle: newStrokeImageStyle,\n // Store the full box info for atomic inline-block handling\n boxOpen: n.style, // signals this is a boxed element\n boxClose: n.style,\n });\n return;\n }\n\n // unicode-bidi: bidi-override (e.g. <bdo dir=\"rtl\">) forces visual order.\n // For an RTL override, reverse both the characters of each descendant run\n // and the order of the runs, so the subtree renders right-to-left.\n const ub = n.style.unicodeBidi;\n const overrideRtl = (ub === 'bidi-override' || ub === 'isolate-override') &&\n n.style.direction === 'rtl';\n const overrideStart = runs.length;\n\n if (hasHorizSpacing) {\n runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxOpen: n.style });\n }\n\n for (const child of n.children) {\n walk(\n child, isBox ? newBoxStyle : boxStyle, newClipStyle, newStrokeImageStyle,\n // An element child measures against this element; a text child's\n // vertical-align belongs to this element, so it measures against what\n // this element measures against.\n child.tagName === '#text' ? parentStyle : n.style,\n );\n }\n\n if (hasHorizSpacing) {\n runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxClose: n.style });\n }\n\n if (overrideRtl && runs.length > overrideStart) {\n const seg = runs.splice(overrideStart);\n for (const r of seg) {\n if (r.text) {\n r.text = [...r.text].reverse().join('');\n // The glyphs are now in visual (reversed) order, so render them\n // left-to-right; otherwise renderText would right-anchor x and the\n // LTR emission (which set x as the left edge) would misposition them.\n r.style = { ...r.style, direction: 'ltr' };\n }\n }\n seg.reverse();\n runs.push(...seg);\n }\n }\n\n // The block itself is the parent every top-level run measures against.\n for (const child of node.children) {\n walk(child, undefined, undefined, undefined, node.style);\n }\n return runs;\n}\n\n/**\n * Check if text needs Intl.Segmenter for word breaking (Thai, Khmer, Lao, Myanmar).\n * These scripts don't use spaces between words.\n */\nfunction needsSegmenter(text: string): boolean {\n for (let i = 0; i < text.length; i++) {\n const code = text.codePointAt(i)!;\n if (\n (code >= 0x0E00 && code <= 0x0E7F) || // Thai\n (code >= 0x0E80 && code <= 0x0EFF) || // Lao\n (code >= 0x1000 && code <= 0x109F) || // Myanmar\n (code >= 0x1780 && code <= 0x17FF) // Khmer\n ) return true;\n if (code > 0xFFFF) i++; // skip surrogate pair\n }\n return false;\n}\n\nlet _segmenter: Intl.Segmenter | undefined;\nfunction getSegmenter(): Intl.Segmenter | null {\n if (_segmenter) return _segmenter;\n if (typeof Intl !== 'undefined' && Intl.Segmenter) {\n _segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });\n return _segmenter;\n }\n return null;\n}\n\n/**\n * Tokenize a single string into words based on whitespace mode.\n */\nfunction tokenizeString(ctx: CanvasRenderingContext2D, text: string, run: TextRun, allWords: Word[], cumState?: { cumText: string; cumWidth: number }): void {\n // Split on zero-width spaces and soft hyphens (break opportunities).\n // Pass cumulative state through so pieces are measured as one text run\n // (preserving kerning accuracy across break points).\n if (text.includes('\\u200B') || text.includes('\\u00AD')) {\n const parts = text.split(/(\\u200B|\\u00AD)/);\n // Share cumulative state across all sub-parts for accurate measurement\n const sharedState = cumState ?? { cumText: '', cumWidth: 0 };\n let nextIsSoftHyphen = false;\n for (const part of parts) {\n if (part === '\\u00AD') {\n nextIsSoftHyphen = true;\n continue;\n }\n if (part === '\\u200B' || part === '') {\n nextIsSoftHyphen = false;\n continue;\n }\n const prevLen = allWords.length;\n tokenizeString(ctx, part, run, allWords, sharedState);\n if (nextIsSoftHyphen && prevLen > 0) {\n allWords[prevLen - 1].isSoftHyphenBreak = true;\n }\n nextIsSoftHyphen = false;\n }\n if (nextIsSoftHyphen && allWords.length > 0) {\n allWords[allWords.length - 1].isSoftHyphenBreak = true;\n }\n return;\n }\n\n // `pre-line` preserves newlines (handled by the \\n pre-split in\n // tokenizeRuns) but collapses spaces and tabs — so it goes through the\n // non-preserving branch below, same as `normal`.\n const isPreserve = run.style.whiteSpace === 'pre' ||\n run.style.whiteSpace === 'pre-wrap' ||\n run.style.whiteSpace === 'break-spaces';\n\n if (isPreserve) {\n // Split on spaces and tabs, keeping delimiters\n const words = text.split(/( +|\\t)/);\n const tabStopInterval = cachedMeasureWidth(ctx, ' ') * 8; // CSS default: 8 spaces\n for (const w of words) {\n if (w === '') continue;\n if (w === '\\t') {\n // Tab width depends on current position — mark it for dynamic calculation\n allWords.push({\n text: '\\t',\n width: tabStopInterval, // placeholder — recalculated in flowWordsIntoLines\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: true,\n isTab: true,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n continue;\n }\n const isSpace = /^ +$/.test(w);\n allWords.push({\n text: w,\n width: cachedMeasureWidth(ctx, w),\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n }\n } else {\n // Split on whitespace but NOT on non-breaking spaces (\\u00A0).\n // Then add a break opportunity AFTER \"?\" inside an otherwise-unbreakable\n // token (the URL query delimiter): Chrome wraps \"\\u2026/q3?\" | \"lang=ar&\\u2026\"\n // even with overflow-wrap:normal. It does NOT break at \"/\", \"&\", \"=\", \".\"\n // or \":\" (verified against the browser), so only \"?\" is split here. The\n // \"?\" stays with the preceding fragment; a trailing \"?\" (no follower) is\n // left intact. Fragments measure cumulatively so kerning stays accurate.\n const words = text\n .split(/([ \\t\\n\\r\\f\\v]+)/)\n .flatMap((w) =>\n /^[ \\t\\n\\r\\f\\v]+$/.test(w) ? [w] : w.split(/(?<=\\?)(?=.)/),\n );\n\n // Use cumulative measurement to avoid rounding error accumulation\n // within a single text run. When cumState is provided (from \\u200B/\\u00AD\n // split), continue from the previous cumulative position to preserve\n // kerning accuracy across break points.\n let cumText = cumState?.cumText ?? '';\n let cumWidth = cumState?.cumWidth ?? 0;\n\n for (const w of words) {\n if (w === '') continue;\n const isSpace = /^[ \\t\\n\\r\\f\\v]+$/.test(w);\n\n if (isSpace) {\n const prevCum = cumWidth;\n cumText += ' ';\n cumWidth = ctx.measureText(cumText).width;\n const spaceWidth = cumWidth - prevCum + (run.style.wordSpacing || 0);\n allWords.push({\n text: ' ',\n width: spaceWidth,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: true,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n continue;\n }\n\n // Use Intl.Segmenter for scripts without spaces (Thai, Khmer, etc.)\n if (needsSegmenter(w)) {\n const segmenter = getSegmenter();\n if (segmenter) {\n for (const seg of segmenter.segment(w)) {\n const s = seg.segment;\n const prevCum = cumWidth;\n cumText += s;\n cumWidth = ctx.measureText(cumText).width;\n allWords.push({\n text: s,\n width: cumWidth - prevCum,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: false,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n }\n continue;\n }\n }\n\n const prevCum = cumWidth;\n cumText += w;\n cumWidth = ctx.measureText(cumText).width;\n let width = cumWidth - prevCum;\n const directWidth = cachedMeasureWidth(ctx, w);\n if (_debug) {\n _debug({\n type: 'measure-word',\n message: `\"${w}\" delta=${width.toFixed(2)} direct=${directWidth.toFixed(2)} diff=${(width - directWidth).toFixed(2)} cumText=\"${cumText}\"`,\n data: { text: w, deltaWidth: width, directWidth, cumWidth, prevCum, font: run.style.fontFamily, fontSize: run.style.fontSize },\n });\n }\n allWords.push({\n text: w,\n width,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: false,\n boxStyle: run.boxStyle,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n }\n\n // Propagate cumulative state back to caller (for \\u200B/\\u00AD splits)\n if (cumState) {\n cumState.cumText = cumText;\n cumState.cumWidth = cumWidth;\n }\n }\n}\n\n/**\n * Tokenize text runs into words for line wrapping.\n */\nfunction tokenizeRuns(ctx: CanvasRenderingContext2D, runs: TextRun[]): Word[] {\n const allWords: Word[] = [];\n\n for (const run of runs) {\n // Handle inline-block margins (empty text, no boxOpen/boxClose)\n if (run.text === '' && !run.boxOpen && !run.boxClose) {\n const margin = run.style.display === 'inline-block'\n ? (run.style.marginLeft || run.style.marginRight || 0)\n : 0;\n if (margin > 0) {\n allWords.push({ text: '', width: margin, style: run.style, isSpace: false, boxStyle: run.boxStyle });\n }\n continue;\n }\n\n // Atomic inline-block: entire element (margin + padding + text) is one word\n // Must check before boxOpen/boxClose handlers since atomic has both set.\n if (run.boxOpen && run.boxClose && run.text) {\n applyFont(ctx, run.style);\n ctx.letterSpacing = formatLetterSpacing(run.style.letterSpacing);\n const text = applyTextTransform(run.text, run.style.textTransform);\n const s = run.style;\n const textWidth = cachedMeasureWidth(ctx, text);\n const totalWidth = s.marginLeft + s.borderLeftWidth + s.paddingLeft +\n textWidth + s.paddingRight + s.borderRightWidth + s.marginRight;\n allWords.push({\n text,\n width: totalWidth,\n style: run.style,\n parentStyle: run.parentStyle,\n isSpace: false,\n boxStyle: run.boxStyle,\n boxOpen: run.boxOpen,\n boxClose: run.boxClose,\n clipStyle: run.clipStyle,\n strokeImageStyle: run.strokeImageStyle,\n });\n continue;\n }\n\n // Handle inline box open/close markers (padding)\n if (run.boxOpen) {\n const pad = run.boxOpen.paddingLeft + run.boxOpen.borderLeftWidth;\n if (pad > 0) {\n allWords.push({ text: '', width: pad, style: run.style, isSpace: false, boxStyle: run.boxStyle, boxOpen: run.boxOpen });\n }\n continue;\n }\n if (run.boxClose) {\n const pad = run.boxClose.paddingRight + run.boxClose.borderRightWidth;\n if (pad > 0) {\n allWords.push({ text: '', width: pad, style: run.style, isSpace: false, boxStyle: run.boxStyle, boxClose: run.boxClose });\n }\n continue;\n }\n\n applyFont(ctx, run.style);\n ctx.letterSpacing = formatLetterSpacing(run.style.letterSpacing);\n const text = applyTextTransform(run.text, run.style.textTransform);\n\n // Mark the first word produced from `startLen` as having no soft-wrap\n // opportunity before it when it directly abuts real text from a previous\n // run (adjacent inline elements with no whitespace between them). The\n // preceding word must be actual text — not a space, newline, empty\n // box-padding marker, or box edge — so a whitespace/padding boundary still\n // allows a break.\n const markGlue = (startLen: number) => {\n const first = allWords[startLen];\n if (!first || first.isSpace || !first.text || first.text === '\\n') return;\n const prev = allWords[startLen - 1];\n if (\n !prev || prev.isSpace || !prev.text.trim() ||\n prev.boxOpen || prev.boxClose\n ) return;\n // CJK, emoji and segmenter-driven scripts (Thai/Khmer/…) have break\n // opportunities between characters regardless of element boundaries, so\n // an element edge between them is NOT a no-break point. Only glue when\n // both sides are ordinary (Latin-like) text with no intrinsic break.\n // Take the boundary characters as GRAPHEME clusters — indexing by code\n // unit reads past the end of a surrogate pair, and indexing by code point\n // splits VS16 emoji (❤️ = U+2764 U+FE0F) so the cluster reads as non-emoji.\n const firstChar = graphemes(first.text)[0];\n const prevClusters = graphemes(prev.text);\n const prevChar = prevClusters[prevClusters.length - 1];\n if (\n isCJK(firstChar) || isCJK(prevChar) ||\n isEmojiCluster(firstChar) || isEmojiCluster(prevChar) ||\n needsSegmenter(first.text) || needsSegmenter(prev.text)\n ) return;\n first.noBreakBefore = true;\n };\n\n // Handle explicit newlines (from <br> or pre-wrap) — always force line break\n if (text.includes('\\n')) {\n const parts = text.split('\\n');\n for (let i = 0; i < parts.length; i++) {\n if (i > 0) {\n allWords.push({ text: '\\n', width: 0, style: run.style, isSpace: false, boxStyle: run.boxStyle });\n }\n if (parts[i]) {\n const startLen = allWords.length;\n tokenizeString(ctx, parts[i], run, allWords);\n markGlue(startLen);\n }\n }\n } else {\n const startLen = allWords.length;\n tokenizeString(ctx, text, run, allWords);\n markGlue(startLen);\n }\n }\n\n return allWords;\n}\n\n/**\n * Check if a character is CJK (Chinese/Japanese/Korean) — these wrap at character level.\n */\nfunction isCJK(char: string): boolean {\n const code = char.codePointAt(0) || 0;\n return (\n (code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified\n (code >= 0x3400 && code <= 0x4DBF) || // CJK Extension A\n (code >= 0x3000 && code <= 0x303F) || // CJK Symbols\n (code >= 0x3040 && code <= 0x309F) || // Hiragana\n (code >= 0x30A0 && code <= 0x30FF) || // Katakana\n (code >= 0xAC00 && code <= 0xD7AF) || // Hangul\n (code >= 0xFF00 && code <= 0xFFEF) || // Fullwidth\n (code >= 0x20000 && code <= 0x2A6DF) // CJK Extension B\n );\n}\n\nlet _graphemeSegmenter: Intl.Segmenter | undefined;\nfunction getGraphemeSegmenter(): Intl.Segmenter | null {\n if (_graphemeSegmenter) return _graphemeSegmenter;\n if (typeof Intl !== 'undefined' && Intl.Segmenter) {\n _graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n return _graphemeSegmenter;\n }\n return null;\n}\n\n/**\n * Split into grapheme clusters — falls back to code points when\n * Intl.Segmenter is unavailable.\n */\nfunction graphemes(text: string): string[] {\n const seg = getGraphemeSegmenter();\n return seg ? [...seg.segment(text)].map((s) => s.segment) : [...text];\n}\n\nconst EMOJI_PICTOGRAPHIC = /\\p{Extended_Pictographic}/u;\n/**\n * Is this grapheme cluster an emoji that creates a line-break opportunity?\n * Restricted to emoji-presentation clusters (emoji planes, regional-indicator\n * flags, and ZWJ/VS16 sequences) so plain text symbols like ©/®/™ — which are\n * Extended_Pictographic but render as text and do NOT break — are excluded.\n */\nfunction isEmojiCluster(s: string): boolean {\n for (const ch of s) {\n const cp = ch.codePointAt(0)!;\n if (cp >= 0x1f000) return true; // emoji planes (incl. regional indicators)\n }\n if (s.includes('\\u200D') || s.includes('\\uFE0F')) {\n return EMOJI_PICTOGRAPHIC.test(s); // ZWJ sequence or VS16 emoji presentation\n }\n return false;\n}\n\n/**\n * Break a word into character-level pieces if it contains CJK/emoji or if\n * overflow-wrap: break-word is set and the word is too wide.\n */\nfunction breakWordIfNeeded(\n ctx: CanvasRenderingContext2D,\n word: Word,\n contentWidth: number,\n currentLineWidth: number,\n): Word[] {\n // Check if word has CJK characters — always break at character level\n const hasCJK = [...word.text].some(isCJK);\n\n // Emoji form their own break opportunities (a run of emoji wraps between\n // clusters). Only meaningful when a grapheme segmenter is available so ZWJ\n // sequences / skin-tone / flag pairs stay intact.\n const hasEmoji = EMOJI_PICTOGRAPHIC.test(word.text) && !!getGraphemeSegmenter();\n\n // Check if word needs break-word splitting — when it won't fit on a fresh line\n const needsBreak = word.width > contentWidth &&\n (word.style.overflowWrap === 'break-word' || word.style.wordBreak === 'break-all');\n\n if (!hasCJK && !hasEmoji && !needsBreak) return [word];\n\n // overflow-wrap:break-word is a LAST RESORT — the browser first uses any\n // normal break opportunity inside the word (a hyphen) before breaking\n // mid-character. So split a hyphenated word at its hyphens first and only\n // char-break the segments that are themselves still too wide. (word-break:\n // break-all genuinely allows breaking between any two characters, so it\n // skips this and falls through to the char loop below.)\n if (needsBreak && word.style.wordBreak !== 'break-all' &&\n word.style.overflowWrap === 'break-word') {\n const segTexts = word.text.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/).filter((s) => s.length);\n if (segTexts.length > 1) {\n ctx.font = buildCanvasFont(word.style);\n ctx.letterSpacing = formatLetterSpacing(word.style.letterSpacing);\n const out: Word[] = [];\n for (const segText of segTexts) {\n const segWidth = cachedMeasureWidth(ctx, segText);\n if (segWidth <= contentWidth) {\n out.push({ ...word, text: segText, width: segWidth });\n } else {\n // Segment still overflows — char-break just this segment.\n out.push(...breakWordIfNeeded(ctx, { ...word, text: segText, width: segWidth }, contentWidth, 0));\n }\n }\n return out;\n }\n }\n\n // Split into characters using cumulative measurement for accuracy.\n // Measuring each char individually ignores kerning — the sum of individual\n // widths diverges from the true string width over many characters.\n ctx.font = buildCanvasFont(word.style);\n // Re-assert letter-spacing: tokenizeRuns may have left ctx at a later run's\n // value, but break points must use THIS word's letter-spacing.\n ctx.letterSpacing = formatLetterSpacing(word.style.letterSpacing);\n // When the word contains emoji, iterate by GRAPHEME cluster so multi-codepoint\n // emoji (ZWJ families, skin tones, flags) are never split mid-cluster.\n const chars = hasEmoji ? graphemes(word.text) : [...word.text];\n const pieces: Word[] = [];\n\n let current = '';\n let currentWidth = 0;\n\n for (const char of chars) {\n // Emoji clusters each get their own word — a break opportunity between\n // adjacent emoji, matching the browser line breaker.\n if (hasEmoji && isEmojiCluster(char)) {\n if (current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n current = '';\n currentWidth = 0;\n }\n pieces.push({ ...word, text: char, width: cachedMeasureWidth(ctx, char) });\n continue;\n }\n\n // CJK chars always get their own word for wrapping\n if (isCJK(char)) {\n if (current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n current = '';\n currentWidth = 0;\n }\n const charWidth = cachedMeasureWidth(ctx, char);\n pieces.push({ ...word, text: char, width: charWidth });\n continue;\n }\n\n // Use cumulative measurement: measure the growing string, not individual chars\n const candidateText = current + char;\n const candidateWidth = cachedMeasureWidth(ctx, candidateText);\n\n // For break-word: break when adding this char would exceed container\n if (needsBreak && candidateWidth > contentWidth && current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n current = char;\n currentWidth = cachedMeasureWidth(ctx, char);\n continue;\n }\n\n current = candidateText;\n currentWidth = candidateWidth;\n }\n\n if (current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n }\n\n return pieces;\n}\n\n/** Punctuation that cannot start a line — stays with the preceding word. */\nconst TRAILING_PUNCT = /^[,.\\;:!?\\)\\]\\}'\"»›]+$/;\n\n/**\n * Flow words into lines that fit within contentWidth.\n * Handles: word wrapping, nowrap, break-word, CJK character wrapping.\n */\nfunction flowWordsIntoLines(\n ctx: CanvasRenderingContext2D,\n words: Word[],\n contentWidth: number,\n whiteSpace: string,\n useBulletProbe = false,\n textIndent = 0,\n tabMetrics?: { interval: number; halfSpace: number },\n strutLineHeight = 0,\n): PositionedLine[] {\n const lines: PositionedLine[] = [];\n // Every line box starts at the block's own \"strut\" height (its font +\n // line-height), so a line whose only content is a SMALLER inline font is\n // still at least the block's line-height tall — matching CSS. See callers.\n const newLine = (): PositionedLine => ({\n words: [],\n totalWidth: 0,\n lineHeight: strutLineHeight,\n });\n let currentLine: PositionedLine = newLine();\n const noWrap = whiteSpace === 'nowrap' || whiteSpace === 'pre';\n // text-indent reduces the first line's width budget; subsequent lines use full width.\n const effWidth = () => contentWidth - (lines.length === 0 ? textIndent : 0);\n\n const isPreWrap = whiteSpace === 'pre-wrap' || whiteSpace === 'pre' || whiteSpace === 'pre-line';\n // `pre`, `pre-wrap`, and `break-spaces` preserve author whitespace\n // (leading and trailing); the others collapse it.\n const preservesWhitespace =\n whiteSpace === 'pre' || whiteSpace === 'pre-wrap' || whiteSpace === 'break-spaces';\n\n function pushLine(isSoftWrap = false) {\n const hadWords = currentLine.words.length > 0;\n // Trim trailing spaces. `break-spaces` preserves them even at soft wraps;\n // `pre`/`pre-wrap` preserve them at hard breaks and end-of-content but not\n // at soft wraps (per CSS Text 3 §4.1.1).\n const preserveTrailing = whiteSpace === 'break-spaces'\n || (preservesWhitespace && !isSoftWrap);\n if (!preserveTrailing) {\n while (currentLine.words.length > 0 && currentLine.words[currentLine.words.length - 1].isSpace) {\n currentLine.totalWidth -= currentLine.words[currentLine.words.length - 1].width;\n currentLine.words.pop();\n }\n }\n // Soft hyphen: if this is a soft wrap and the last word has a soft-hyphen\n // break, append a visible '-' since the word is being broken here.\n if (isSoftWrap && currentLine.words.length > 0) {\n const lastWord = currentLine.words[currentLine.words.length - 1];\n if (lastWord.isSoftHyphenBreak) {\n applyFont(ctx, lastWord.style);\n const hyphenWidth = cachedMeasureWidth(ctx, '-');\n currentLine.words.push({\n text: '-',\n width: hyphenWidth,\n style: lastWord.style,\n parentStyle: lastWord.parentStyle,\n isSpace: false,\n // The visible hyphen continues the broken word, so it inherits the\n // word's clip/stroke-image declarer (else it paints transparent).\n clipStyle: lastWord.clipStyle,\n strokeImageStyle: lastWord.strokeImageStyle,\n });\n currentLine.totalWidth += hyphenWidth;\n }\n }\n // In pre-wrap mode, space-only lines still need height (they are content)\n if (currentLine.words.length > 0 || (hadWords && isPreWrap)) {\n if (_debug) {\n const text = currentLine.words.map(w => w.text).join('');\n _debug({\n type: 'line-commit',\n message: `Line ${lines.length}: \"${text}\" width=${currentLine.totalWidth.toFixed(2)} / ${contentWidth}`,\n data: { lineIndex: lines.length, text, totalWidth: currentLine.totalWidth, contentWidth },\n });\n }\n lines.push(currentLine);\n }\n currentLine = newLine();\n }\n\n let afterHardBreak = true; // start of content is like after a hard break\n\n for (let wordIndex = 0; wordIndex < words.length; wordIndex++) {\n const word = words[wordIndex];\n let wordLineHeight = getLineHeight(ctx, word.style, useBulletProbe);\n // Inline-block elements expand line height with their vertical padding+margin\n if (word.boxStyle && word.boxStyle.display === 'inline-block') {\n // Clamped at 0: negative margins shrink the margin box, but the original\n // `Math.max(h, h + extra)` never let them shrink the LINE, and nothing\n // here is measuring a case that says they should.\n const extra = inlineBlockExtra(word.boxStyle);\n wordLineHeight += Math.max(0, extra.top + extra.bottom);\n }\n\n if (word.text === '\\n') {\n if (currentLine.words.length === 0) {\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n currentLine.endedByHardBreak = true;\n lines.push(currentLine);\n currentLine = newLine();\n } else {\n currentLine.endedByHardBreak = true;\n pushLine();\n }\n afterHardBreak = true;\n continue;\n }\n\n // No wrapping mode — everything on one line\n if (noWrap) {\n currentLine.words.push(word);\n currentLine.totalWidth += word.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n continue;\n }\n\n // Breaking a word that is split across a run boundary. A single word split\n // across adjacent inline runs (e.g. <span>E</span>xperience, a font-size\n // change mid-word, or <span>wel</span>l-being) is several Words glued by\n // `noBreakBefore`. Per-word break logic can't see the whole word, so its\n // internal break opportunities — hyphens, and break-word char points — are\n // lost and the unit overflows the edge. Detect the maximal glued chain\n // starting here and break it across the run boundaries like the browser.\n if (!word.isSpace && word.text && !word.noBreakBefore && !word.boxOpen && !word.boxClose) {\n let end = wordIndex;\n while (end + 1 < words.length) {\n const nx = words[end + 1];\n if (!nx.text || nx.isSpace || nx.boxOpen || nx.boxClose || !nx.noBreakBefore) break;\n end++;\n }\n if (end > wordIndex) {\n const breakWord = word.style.overflowWrap === 'break-word' || word.style.wordBreak === 'break-all';\n let combined = 0;\n for (let j = wordIndex; j <= end; j++) combined += words[j].width;\n // Flatten the chain into styled characters (per-run style retained).\n // Carry the run's clip/stroke-image declarer too, else a break-word\n // split drops it and a gradient/stroke fragment paints nothing (the\n // inherited transparent fill has no clip box to reveal).\n // `parentStyle` rides along for the same reason: dropping it made a\n // split `vertical-align` run measure its shift against the block\n // instead of its real parent, 8px out on a narrow break-word line.\n type Cell = {\n ch: string;\n style: ResolvedStyle;\n parentStyle?: ResolvedStyle;\n clipStyle?: ResolvedStyle;\n strokeImageStyle?: ResolvedStyle;\n };\n const cells: Cell[] = [];\n for (let j = wordIndex; j <= end; j++)\n for (const ch of [...words[j].text])\n cells.push({\n ch,\n style: words[j].style,\n parentStyle: words[j].parentStyle,\n clipStyle: words[j].clipStyle,\n strokeImageStyle: words[j].strokeImageStyle,\n });\n const combinedText = cells.map((c) => c.ch).join('');\n // Hyphen break opportunities (same rule as the single-word hyphen path).\n const segTexts = combinedText.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/).filter((s) => s.length);\n const hyphenMode = segTexts.length > 1;\n const fitsLine = currentLine.totalWidth + combined <= effWidth();\n // A hyphen is an ordinary break opportunity — intervene whenever the\n // unit doesn't fit the remaining space. break-word is last-resort —\n // only when the unit can't fit a full line at all (otherwise the normal\n // flow + glued-tail fit check correctly wraps it whole to a fresh line).\n const enter = !fitsLine && (hyphenMode || (breakWord && combined > effWidth()));\n if (enter) {\n // Atomic units for breaking: hyphen segments, else the whole chain.\n const segs: Cell[][] = [];\n let ci = 0;\n for (const st of segTexts) {\n const len = [...st].length;\n segs.push(cells.slice(ci, ci + len));\n ci += len;\n }\n // Place a segment's cells onto the current line, splitting same-style\n // runs into pieces. When `chars` is set, wrap at the line edge between\n // characters (break-word); otherwise place atomically (it may overflow\n // its own line, e.g. a hyphen prefix wider than the container).\n const placeCells = (cs: Cell[], chars: boolean) => {\n let i = 0;\n while (i < cs.length) {\n const st = cs[i].style;\n // clip/stroke declarer is 1:1 with the style run (same source\n // word), so capturing it at the run start covers every push below.\n const clipStyle = cs[i].clipStyle;\n const strokeImageStyle = cs[i].strokeImageStyle;\n const parentStyle = cs[i].parentStyle;\n applyFont(ctx, st);\n ctx.letterSpacing = formatLetterSpacing(st.letterSpacing);\n const lh = getLineHeight(ctx, st, useBulletProbe);\n const run: { ch: string; style: ResolvedStyle }[] = [];\n let cur = '';\n let curW = 0;\n while (i < cs.length && cs[i].style === st) {\n const ch = cs[i].ch;\n const candW = cachedMeasureWidth(ctx, cur + ch);\n if (chars && currentLine.totalWidth + candW > effWidth() &&\n (currentLine.words.length > 0 || cur)) {\n if (cur) {\n currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });\n currentLine.totalWidth += curW;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);\n }\n pushLine(true);\n afterHardBreak = false;\n cur = ch;\n curW = cachedMeasureWidth(ctx, ch);\n } else {\n cur += ch;\n curW = candW;\n }\n i++;\n }\n if (cur) {\n currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });\n currentLine.totalWidth += curW;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);\n afterHardBreak = false;\n }\n }\n };\n const measureSeg = (cs: Cell[]) => {\n let w = 0;\n let i = 0;\n while (i < cs.length) {\n const st = cs[i].style;\n let txt = '';\n while (i < cs.length && cs[i].style === st) { txt += cs[i].ch; i++; }\n applyFont(ctx, st);\n ctx.letterSpacing = formatLetterSpacing(st.letterSpacing);\n w += cachedMeasureWidth(ctx, txt);\n }\n return w;\n };\n // Pure break-word (no hyphen) is last-resort: move the whole word to a\n // fresh line first (using the preceding space), then break it there.\n if (!hyphenMode && currentLine.words.length > 0) {\n pushLine(true);\n afterHardBreak = false;\n }\n for (const seg of segs) {\n const segW = measureSeg(seg);\n if (currentLine.words.length > 0 && currentLine.totalWidth + segW > effWidth()) {\n pushLine(true);\n afterHardBreak = false;\n }\n // Char-break a segment only when break-word and it can't fit a line.\n placeCells(seg, breakWord && segW > effWidth());\n }\n wordIndex = end;\n continue;\n }\n }\n }\n\n // Break long words / CJK characters if needed\n const pieces = (!word.isSpace && word.text.length > 1)\n ? breakWordIfNeeded(ctx, word, effWidth(), currentLine.totalWidth)\n : [word];\n\n // Glued tail: content immediately after this word that cannot start a new\n // line — trailing punctuation (\",.)]}…\"), an inline span's right\n // padding/border (empty boxClose markers), and a word continuation that\n // abuts this word across a run boundary with no soft-wrap opportunity\n // (noBreakBefore — e.g. one word split across two inline spans with\n // different font sizes). The browser includes all of it when deciding\n // whether this word fits, so the unit wraps together: if \"Music Experie\"\n // doesn't leave room for the glued \"nce\", the whole word wraps as one.\n // Stops at whitespace or the next breakable word.\n let gluedTailWidth = 0;\n for (let j = wordIndex + 1; j < words.length; j++) {\n const nw = words[j];\n if (nw.isSpace || nw.text === '\\n') break;\n const isPunct = !!nw.text && TRAILING_PUNCT.test(nw.text);\n const isCloseMarker = !nw.text && !!nw.boxClose;\n const isGluedCont = !!nw.text && !!nw.noBreakBefore;\n if (isPunct || isCloseMarker || isGluedCont) { gluedTailWidth += nw.width; continue; }\n break;\n }\n\n for (const piece of pieces) {\n const isLastPiece = piece === pieces[pieces.length - 1];\n // Only the last piece of the word carries the glued tail.\n const tail = isLastPiece ? gluedTailWidth : 0;\n // Trailing punctuation (e.g. comma after </span>) should not wrap\n // independently — browsers keep it with the preceding word.\n const isTrailingPunct = !piece.isSpace && piece.text.length > 0 &&\n TRAILING_PUNCT.test(piece.text) &&\n currentLine.words.length > 0 &&\n !currentLine.words[currentLine.words.length - 1].isSpace;\n\n // A word that abuts the previous run with no whitespace has no soft-wrap\n // opportunity before it — keep it with the preceding word like trailing\n // punctuation. Only the FIRST piece carries the flag; a break-word split\n // inside the word may still wrap mid-word.\n const isGlued = piece === pieces[0] && piece.noBreakBefore &&\n currentLine.words.length > 0 &&\n !currentLine.words[currentLine.words.length - 1].isSpace;\n\n // Leading inline padding/border (an empty boxOpen marker) must not be\n // stranded at the end of a line — it belongs with the span's following\n // content (CSS applies padding-left at the box's start). Include the next\n // content word's width in this marker's fit test so the two wrap together\n // and the left padding lands on the new line with the content.\n let headExtra = 0;\n if (!piece.text && piece.boxOpen) {\n const next = words[wordIndex + 1];\n if (next && !next.isSpace && next.text) {\n // Only the next word's first BREAKABLE unit must stay with the leading\n // padding — the whole word for unbreakable Latin, but just the first\n // character for CJK / break-word (which wrap per character). Using the\n // whole word here would over-wrap a long CJK run that follows padding.\n const np = next.text.length > 1\n ? breakWordIfNeeded(ctx, next, effWidth(), 0)\n : [next];\n headExtra = np[0].width;\n }\n }\n\n // A soft-hyphen break point draws a visible '-' when the line breaks\n // right after this piece. Chrome only allows a break there if the prefix\n // PLUS the hyphen fits, so reserve the hyphen advance in the overflow\n // test — otherwise we pack one extra segment and the appended hyphen\n // overflows the line (breaking one segment later than the browser).\n let shReserve = 0;\n if (piece.isSoftHyphenBreak) {\n applyFont(ctx, piece.style);\n ctx.letterSpacing = formatLetterSpacing(piece.style.letterSpacing);\n shReserve = cachedMeasureWidth(ctx, '-');\n }\n\n // Would this piece overflow?\n if (!piece.isSpace && !isTrailingPunct && !isGlued && currentLine.words.length > 0 &&\n currentLine.totalWidth + piece.width + shReserve + tail + headExtra > effWidth()) {\n const overflow = currentLine.totalWidth + piece.width + shReserve + tail + headExtra - effWidth();\n\n // For borderline cases (overflow < 1px), word-by-word delta\n // accumulation may introduce rounding errors. Re-measure the\n // full candidate line as a single string for accuracy.\n // Only works for single-font lines — mixed fonts can't be\n // measured as one string.\n let reallyOverflows = true;\n if (overflow < 1 && !hasMixedFonts([...currentLine.words, piece])) {\n applyFont(ctx, piece.style);\n const fullText = currentLine.words.map(w => w.text).join('') + piece.text +\n (piece.isSoftHyphenBreak ? '-' : '');\n // Empty-text words carry non-glyph advance (inline padding/border\n // markers, inline-block margins) that measureText(fullText) misses —\n // add them back so padded inline spans aren't under-measured.\n let markerWidth = 0;\n for (const w of currentLine.words) if (!w.text) markerWidth += w.width;\n if (!piece.text) markerWidth += piece.width;\n const fullWidth = cachedMeasureWidth(ctx, fullText) + markerWidth + tail + headExtra;\n // Allow only a hair of sub-pixel overflow. measureText matches the\n // browser's rendered width to ~0.01px, so a larger slack would keep\n // lines the browser actually wraps (packing one extra word per\n // borderline line and drifting the whole document's breaks).\n if (fullWidth <= effWidth() + 0.02) {\n reallyOverflows = false;\n }\n }\n\n // Hyphen break on current line: before wrapping the whole word,\n // try fitting a hyphen prefix on the current line. Browsers prefer\n // keeping content on the current line by splitting at hyphens.\n if (reallyOverflows && piece.text.includes('-')) {\n const parts = piece.text.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/);\n if (parts.length > 1) {\n applyFont(ctx, piece.style);\n let fitted = '';\n let fittedWidth = 0;\n let partIdx = 0;\n const available = effWidth() - currentLine.totalWidth;\n for (; partIdx < parts.length; partIdx++) {\n const candidate = fitted + parts[partIdx];\n const candidateWidth = cachedMeasureWidth(ctx, candidate);\n if (candidateWidth > available) break;\n fitted = candidate;\n fittedWidth = candidateWidth;\n }\n if (partIdx > 0 && partIdx < parts.length) {\n currentLine.words.push({ ...piece, text: fitted, width: fittedWidth });\n currentLine.totalWidth += fittedWidth;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n pushLine(true);\n afterHardBreak = false;\n const remainder = parts.slice(partIdx).join('');\n const remainderWidth = cachedMeasureWidth(ctx, remainder);\n currentLine.words.push({ ...piece, text: remainder, width: remainderWidth });\n currentLine.totalWidth += remainderWidth;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n continue;\n }\n }\n }\n\n if (reallyOverflows) {\n if (_debug) {\n const lineText = currentLine.words.map(w => w.text).join('');\n _debug({\n type: 'line-wrap',\n message: `\"${piece.text}\" overflow=${overflow.toFixed(2)} wrap=true lineWidth=${currentLine.totalWidth.toFixed(2)} pieceWidth=${piece.width.toFixed(2)} contentWidth=${contentWidth} line=\"${lineText}\"`,\n data: { text: piece.text, overflow, lineWidth: currentLine.totalWidth, pieceWidth: piece.width, contentWidth, lineText },\n });\n }\n pushLine(true);\n afterHardBreak = false;\n }\n }\n\n // Skip leading spaces at the start of a line. Preserving modes\n // (pre/pre-wrap/break-spaces) keep them after hard breaks; collapsing\n // modes (normal/nowrap/pre-line) drop them in all cases.\n if (piece.isSpace && currentLine.words.length === 0\n && (!afterHardBreak || !preservesWhitespace)) continue;\n\n // Tab: advance to the next tab stop (stops measured from the content\n // edge). Chrome rule: when the next stop is closer than half a space\n // width, skip to the following stop (Blink Font::TabWidth).\n let pieceWidth = piece.width;\n if (piece.isTab) {\n const interval = tabMetrics?.interval || piece.width;\n const halfSpace = tabMetrics?.halfSpace ?? 0;\n const currentPos = (lines.length === 0 ? textIndent : 0) + currentLine.totalWidth;\n let advance = interval - (currentPos % interval);\n if (advance < halfSpace) advance += interval;\n pieceWidth = advance;\n piece.width = pieceWidth;\n }\n\n // Hyphen break on a fresh line when word still too wide.\n if (currentLine.words.length === 0 && pieceWidth > effWidth() &&\n !piece.isSpace && piece.text.includes('-')) {\n const subParts = piece.text.split(/(?<=-)(?!\\d)|(?<=[^\\d]-)/);\n if (subParts.length > 1) {\n applyFont(ctx, piece.style);\n // Inject sub-parts as individual pieces — they'll flow through\n // the normal overflow/wrap logic on subsequent iterations.\n const newPieces: Word[] = subParts.filter(p => p).map(p => ({\n ...piece,\n text: p,\n width: cachedMeasureWidth(ctx, p),\n }));\n // Replace current piece with the sub-parts by splicing into the pieces array\n // Since we're iterating `pieces`, we push remaining sub-parts after the first\n // onto the current line normally, letting the overflow check handle wrapping.\n let first = true;\n for (const sp of newPieces) {\n if (first) {\n first = false;\n // First sub-part: add to current line (it fits since it's smaller)\n currentLine.words.push(sp);\n currentLine.totalWidth += sp.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n } else if (currentLine.totalWidth + sp.width > effWidth()) {\n // Overflow: wrap to next line\n pushLine(true);\n afterHardBreak = false;\n currentLine.words.push(sp);\n currentLine.totalWidth += sp.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n } else {\n currentLine.words.push(sp);\n currentLine.totalWidth += sp.width;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n }\n }\n continue;\n }\n }\n\n currentLine.words.push(piece);\n currentLine.totalWidth += pieceWidth;\n currentLine.lineHeight = Math.max(currentLine.lineHeight, wordLineHeight);\n if (!piece.isSpace) afterHardBreak = false;\n }\n }\n pushLine();\n return lines;\n}\n\n/**\n * Shared line budget for `-webkit-line-clamp` on a block container whose\n * text lives in block descendants (Chrome legacy `-webkit-box` semantics:\n * line boxes are counted across ALL descendants; the Nth line gets an\n * ellipsis and everything after it is dropped). Created in layoutBlock at\n * the clamped element and threaded through descendant layout calls.\n *\n * Known limitation: when the budget runs out exactly at a paragraph\n * boundary (Nth line is a paragraph's last line), following content is\n * dropped but the already-emitted Nth line gets no ellipsis — its layout\n * nodes were positioned before we learned more content follows.\n */\ninterface LineClampState {\n /** Line boxes still allowed before the cut. */\n remaining: number;\n /** Truncation point reached — all subsequent content is dropped. */\n exhausted: boolean;\n}\n\n/**\n * Layout inline content: text wrapping + positioning using pure canvas measurement.\n * Returns layout nodes and the total height consumed.\n */\nfunction layoutInlineContent(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n x: number,\n y: number,\n contentWidth: number,\n useBulletProbe = false,\n clamp?: LineClampState,\n): { nodes: LayoutNode[]; height: number } {\n const results: LayoutNode[] = [];\n // Text nodes covered by an inline element declaring background-clip:text\n // (clipRuns) or --rt-text-stroke-image (strokeImageRuns), mapped to that\n // declaring element's style. A post-pass turns each per-line run of\n // same-declarer nodes into a fragment-spanning paint box.\n const clipRuns = new Map<LayoutText, ResolvedStyle>();\n const strokeImageRuns = new Map<LayoutText, ResolvedStyle>();\n if (clamp && (clamp.exhausted || clamp.remaining <= 0)) {\n // An ancestor's clamp already used its line budget — drop this content.\n clamp.exhausted = true;\n return { nodes: results, height: 0 };\n }\n const runs = collectTextRuns(node);\n if (runs.length === 0) return { nodes: results, height: 0 };\n\n const words = tokenizeRuns(ctx, runs);\n const textIndent = node.style.textIndent || 0;\n // Tab stops follow the BLOCK's style, not the inline run the tab sits in:\n // Chrome sizes the interval as tab-size(8) × the block font's space advance\n // plus letter- and word-spacing (css-text-3 §tab-size) — verified against\n // the DOM: a tab inside a bold span still uses the regular-weight space.\n applyFont(ctx, node.style);\n const prevLetterSpacing = ctx.letterSpacing;\n ctx.letterSpacing = '0px';\n const blockSpaceWidth = cachedMeasureWidth(ctx, ' ');\n ctx.letterSpacing = prevLetterSpacing;\n const tabMetrics = {\n interval: (blockSpaceWidth + (node.style.letterSpacing || 0) + (node.style.wordSpacing || 0)) * 8,\n halfSpace: blockSpaceWidth / 2,\n };\n // The block's own font + line-height set the strut: the minimum height of\n // every line box, even a line holding only smaller inline content.\n const strutLineHeight = getLineHeight(ctx, node.style, useBulletProbe);\n const lines = flowWordsIntoLines(ctx, words, contentWidth, node.style.whiteSpace, useBulletProbe, textIndent, tabMetrics, strutLineHeight);\n\n // `-webkit-line-clamp` / `line-clamp`: truncate to N lines and append a\n // CSS-style ellipsis (\"…\") to the Nth line, back-trimming trailing words\n // until the ellipsis fits within contentWidth. The budget comes from an\n // ancestor's shared clamp state when one is active (clamp on a block\n // container with block children), else from this element's own style.\n const clampN = clamp ? clamp.remaining : node.style.lineClamp;\n if (clampN > 0 && lines.length > clampN) {\n lines.length = clampN;\n const lastLine = lines[clampN - 1];\n // First line has reduced width because of text-indent; a cut on this\n // element's first line (effective budget of 1) hits it.\n const lineMaxForEllipsis = contentWidth - (clampN === 1 ? textIndent : 0);\n applyEllipsisToLine(ctx, lastLine, lineMaxForEllipsis);\n // Tag the truncated line so per-line alignment (text-align vs\n // text-align-last) still picks the right branch.\n lastLine.endedByHardBreak = true;\n if (clamp) {\n clamp.remaining = 0;\n clamp.exhausted = true;\n }\n } else if (clamp) {\n clamp.remaining -= lines.length;\n }\n\n const isRTL = node.style.direction === 'rtl';\n const resolveDir = (a: string) => {\n if (a === 'start') return isRTL ? 'right' : 'left';\n if (a === 'end') return isRTL ? 'left' : 'right';\n return a;\n };\n let textAlign = resolveDir(node.style.textAlign);\n // text-align-last: 'auto' inherits from text-align except when text-align is\n // 'justify', then defaults to 'start' (CSS Text 3 §7.2).\n let textAlignLast = node.style.textAlignLast || 'auto';\n if (textAlignLast === 'auto') {\n textAlignLast = node.style.textAlign === 'justify' ? (isRTL ? 'right' : 'left') : textAlign;\n } else {\n textAlignLast = resolveDir(textAlignLast);\n }\n\n // The block strut also participates in the line's baseline, not just its\n // height: inline content aligns to the block-font baseline, so a line whose\n // only content is a SMALLER inline font sits on the strut baseline (lower in\n // the box), not centered in it. Seed each line's ascent/descent with the\n // block font's metrics so the baseline lands where the DOM puts it.\n // The block is the parent of any run with no inline ancestor, and the emit\n // loop shadows `node` with the LayoutText it builds.\n const blockStyle = node.style;\n\n let curY = y;\n\n for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {\n const line = lines[lineIdx];\n if (line.words.length === 0) {\n curY += line.lineHeight;\n continue;\n }\n\n const isLastLine = lineIdx === lines.length - 1;\n const isFirstLine = lineIdx === 0;\n\n // Per-line alignment: lines ending at a forced break or the last line\n // use text-align-last; all others use text-align (CSS Text 3 §7.1, §7.2).\n const useLast = isLastLine || line.endedByHardBreak;\n const align = useLast ? textAlignLast : textAlign;\n\n // text-indent narrows the first line's available width.\n const indent = isFirstLine ? textIndent : 0;\n const lineMaxWidth = contentWidth - indent;\n\n // Justify: expand spaces to fill the line.\n let justifyExtraPerSpace = 0;\n if (align === 'justify' && line.totalWidth < lineMaxWidth) {\n const spaceCount = line.words.filter(w => w.isSpace).length;\n if (spaceCount > 0) {\n justifyExtraPerSpace = (lineMaxWidth - line.totalWidth) / spaceCount;\n }\n }\n\n // text-align (with first-line indent baked into curX).\n // When the line overflows its container, browsers fall back to start\n // alignment (per CSS Text 3 §7.1) instead of pushing the line outside\n // the box. Common trigger: wide letter-spacing on text that doesn't\n // wrap at letter boundaries (no break-word/break-all), where centering\n // would put glyphs at negative x. Sub-pixel tolerance avoids switching\n // to start for rounding noise on lines that visually fit.\n // Start edge differs by direction. LTR lines start at the left (x+indent).\n // RTL lines are anchored at the right, inset from the content's right edge\n // by text-indent — and lineMaxWidth already subtracts indent, so the RTL\n // right edge is x+lineMaxWidth. `align` here is physically resolved\n // (start/end → left/right via resolveDir), so RTL with align==='left'\n // (explicit left, or end) correctly falls through to left alignment.\n const overflows = line.totalWidth > lineMaxWidth + 0.5;\n let curX = x + indent;\n if (overflows) {\n // Overflow fallback: pin to the start edge (CSS Text 3 §7.1).\n curX = isRTL ? x + lineMaxWidth - line.totalWidth : x + indent;\n } else if (align === 'center') {\n curX = x + indent + (lineMaxWidth - line.totalWidth) / 2;\n } else if (align === 'right') {\n curX = (isRTL ? x + lineMaxWidth : x + indent + lineMaxWidth) - line.totalWidth;\n } else if (align === 'justify' && isRTL) {\n // RTL justify: anchor the right edge at the inset start; spaces expand left.\n curX = x + lineMaxWidth - line.totalWidth;\n }\n // Snapshot the line's left edge before LTR emission advances curX.\n const lineLeftX = curX;\n\n // Inline background boxes and text are emitted after baseline computation\n // (below) so that emitInlineBox can use line-level metrics for alignment.\n\n // The line box is the union of every box on it — strut, run, shifted run,\n // inline-block — each carrying its own leading over its own line-height:\n // lineAscent = max(ascent - shift), lineDescent = max(descent + shift).\n // One font, one line-height and no shift collapse that back to the plain\n // half-leading every single-style line already had.\n const strutBox = leadedBox(ctx, node.style, useBulletProbe);\n let lineAscent = strutBox.ascent;\n let lineDescent = strutBox.descent;\n // A shift moves the box, not the line's baseline: positive is downward, so\n // it lifts the box's demand on the ascent side and adds to the descent one.\n const grow = (b: { ascent: number; descent: number }, shift: number) => {\n if (b.ascent - shift > lineAscent) lineAscent = b.ascent - shift;\n if (b.descent + shift > lineDescent) lineDescent = b.descent + shift;\n };\n for (const word of line.words) {\n if (word.text === '') continue;\n // A wrapper element with no text of its own — `<span lh:3><span>x</span>`\n // — never becomes a Word, but it is still a box on the line and still\n // brings its own line-height. Its run children carry it as `parentStyle`,\n // so take it from there, AT ITS OWN SHIFT: added unshifted, a wrapper\n // that carries a vertical-align and direct text enters the union twice\n // at two different places, and the line spans both (measured 60px where\n // the DOM has 40). With the shift it is idempotent — a wrapper with\n // direct text contributes the identical box through its own run.\n if (word.parentStyle) {\n grow(\n leadedBox(ctx, word.parentStyle, useBulletProbe),\n verticalAlignShift(\n word.parentStyle.verticalAlign, ctx, word.parentStyle, blockStyle, useBulletProbe),\n );\n }\n const box = leadedBox(ctx, word.style, useBulletProbe);\n // An inline-block joins the line as an ATOMIC box: its own content\n // baseline with its margin box stacked around it. It takes the extra\n // space, but no shift — the emit pass puts its content on the line\n // baseline and does not honour vertical-align on it, so shifting the box\n // here would grow the line one way while the paint went the other.\n const atomic = word.boxStyle?.display === 'inline-block' ? word.boxStyle : null;\n if (atomic) {\n const extra = inlineBlockExtra(atomic);\n box.ascent += extra.top;\n box.descent += extra.bottom;\n }\n grow(box, atomic ? 0 : verticalAlignShift(\n word.style.verticalAlign, ctx, word.style,\n word.parentStyle ?? blockStyle, useBulletProbe));\n }\n const lineBoxHeight = lineAscent + lineDescent;\n const lineBaselineY = curY + lineAscent;\n\n // Emit inline background box using line-level baseline for vertical alignment.\n // Uses the line's ascent/descent (not the box's own font) so box aligns with text.\n const emitInlineBox = (style: ResolvedStyle, bx: number, bw: number) => {\n // The box's OWN font decides its height, not the line's largest. An\n // inline-block's content box is its LINE-HEIGHT, though, not the bare\n // font metrics — measured against Chrome, bare metrics put it at\n // y=6 h=29 where the DOM has y=4 h=33.2.\n const { ascent: boxAscent, descent: boxDescent } =\n style.display === 'inline-block'\n ? leadedBox(ctx, style, useBulletProbe)\n : getFontMetrics(ctx, style);\n const padTop = style.paddingTop + style.borderTopWidth;\n const padBottom = style.paddingBottom + style.borderBottomWidth;\n const boxHeight = boxAscent + boxDescent + padTop + padBottom;\n // Every inline box hangs off the line's baseline, an inline-block too:\n // its content is emitted on that baseline, so a box pinned to the line\n // TOP instead detached from its own glyphs as soon as something taller\n // shared the line — measured, a background at y 4..33 around text whose\n // baseline was 46.\n const boxY = lineBaselineY - boxAscent - padTop;\n results.push({\n type: 'box', style, x: bx, y: boxY, width: bw, height: boxHeight,\n tagName: 'span', children: [],\n });\n };\n\n // LTR: emit inline background boxes (Pass 1) before text.\n if (!isRTL) {\n let scanX = curX;\n let boxStartX = scanX;\n let currentBoxStyle: ResolvedStyle | undefined;\n let boxHasText = false;\n\n for (const word of line.words) {\n if (word.boxOpen && word.boxClose && word.text) {\n if (currentBoxStyle) {\n if (boxHasText) emitInlineBox(currentBoxStyle, boxStartX, scanX - boxStartX);\n currentBoxStyle = undefined;\n boxHasText = false;\n }\n const s = word.style;\n const textWidth = word.width - s.marginLeft - s.borderLeftWidth - s.paddingLeft\n - s.paddingRight - s.borderRightWidth - s.marginRight;\n const boxX = scanX + s.marginLeft;\n const boxW = s.borderLeftWidth + s.paddingLeft + textWidth + s.paddingRight + s.borderRightWidth;\n emitInlineBox(s, boxX, boxW);\n boxHasText = false;\n scanX += word.width;\n continue;\n }\n\n if (word.boxStyle !== currentBoxStyle) {\n if (currentBoxStyle && boxHasText) {\n emitInlineBox(currentBoxStyle, boxStartX, scanX - boxStartX);\n }\n currentBoxStyle = word.boxStyle;\n boxStartX = scanX;\n boxHasText = false;\n }\n if (word.text && !word.isSpace) boxHasText = true;\n scanX += word.width + (word.isSpace ? justifyExtraPerSpace : 0);\n }\n if (currentBoxStyle && boxHasText) {\n emitInlineBox(currentBoxStyle, boxStartX, scanX - boxStartX);\n }\n }\n\n // Emit text nodes.\n const textWords = line.words.filter(w => w.text !== '');\n const allSameStyle = textWords.length > 0 && textWords.every(w =>\n sameTextStyle(w.style, textWords[0].style)\n );\n\n if (isRTL) {\n // RTL: build groups, compute positions, emit boxes then text.\n // Groups join consecutive same-style words for proper glyph shaping.\n // Padding markers between groups create spacing.\n interface StyledGroup {\n text: string; style: ResolvedStyle; width: number;\n boxStyle?: ResolvedStyle; clipStyle?: ResolvedStyle;\n strokeImageStyle?: ResolvedStyle; x: number;\n padBefore: number; // padding before this group (from boxOpen/boxClose markers)\n }\n const groups: StyledGroup[] = [];\n let currentGroup: StyledGroup | null = null;\n let pendingPad = 0;\n\n for (const word of line.words) {\n if (word.text === '') {\n // Padding marker — accumulate for the next group boundary\n if (currentGroup) { groups.push(currentGroup); currentGroup = null; }\n pendingPad += word.width;\n continue;\n }\n if (word.isSpace && justifyExtraPerSpace > 0) {\n // Justify only: break the shaping group at the space and fold the\n // expansion into the inter-group advance so the line fills the width.\n // (Arabic does not join across spaces, so this is shaping-safe.)\n // When not justifying, spaces stay merged into the group text below\n // so the canvas BiDi engine can reorder embedded LTR runs/numbers.\n if (currentGroup) { groups.push(currentGroup); currentGroup = null; }\n pendingPad += word.width + justifyExtraPerSpace;\n continue;\n }\n if (currentGroup && sameTextStyle(currentGroup.style, word.style)) {\n currentGroup.text += word.text;\n currentGroup.width += word.width;\n } else {\n if (currentGroup) groups.push(currentGroup);\n currentGroup = { text: word.text, style: word.style, width: word.width, boxStyle: word.boxStyle, clipStyle: word.clipStyle, strokeImageStyle: word.strokeImageStyle, x: 0, padBefore: pendingPad };\n pendingPad = 0;\n }\n }\n if (currentGroup) groups.push(currentGroup);\n\n // Compute positions right-to-left: group-level measureText for accuracy,\n // with padding markers creating spacing between groups.\n let rtlX = curX + line.totalWidth;\n for (const group of groups) {\n rtlX -= group.padBefore; // spacing from padding markers\n applyFont(ctx, group.style);\n const measuredWidth = cachedMeasureWidth(ctx, group.text);\n rtlX -= measuredWidth;\n group.x = rtlX;\n group.width = measuredWidth;\n }\n\n // Emit inline boxes first (behind text).\n // Include padding/border from boxStyle in box dimensions.\n for (const group of groups) {\n if (group.boxStyle && hasVisibleBoxStyles(group.boxStyle)) {\n const bs = group.boxStyle;\n const padLeft = bs.paddingLeft + bs.borderLeftWidth;\n const padRight = bs.paddingRight + bs.borderRightWidth;\n emitInlineBox(bs, group.x - padLeft, group.width + padLeft + padRight);\n }\n }\n\n // Emit text groups\n for (const group of groups) {\n const node: LayoutText = {\n type: 'text',\n text: group.text,\n x: group.x + group.width, // x = right edge for RTL textAlign\n y: lineBaselineY,\n width: group.width,\n style: { ...group.style, direction: 'rtl' },\n };\n results.push(node);\n if (group.clipStyle) clipRuns.set(node, group.clipStyle);\n if (group.strokeImageStyle) strokeImageRuns.set(node, group.strokeImageStyle);\n }\n } else {\n // LTR with mixed BiDi scripts: emit the entire line as one fillText call\n // so the canvas engine handles BiDi reordering (Arabic/Hebrew in LTR).\n // Only do this when the line contains RTL characters — pure LTR lines\n // are more accurate with word-by-word positioning.\n const lineText = line.words.map(w => w.text).join('');\n const hasBidiMix = allSameStyle && /[\\u0590-\\u08FF\\uFB50-\\uFDFF\\uFE70-\\uFEFF]/.test(lineText) &&\n !line.words.some(w => w.boxOpen || w.boxClose ||\n w.style.verticalAlign === 'super' || w.style.verticalAlign === 'sub');\n if (hasBidiMix) {\n applyFont(ctx, textWords[0].style);\n const measuredWidth = cachedMeasureWidth(ctx, lineText);\n // This line belongs to an LTR block (we're in the !isRTL branch), so it\n // must be painted with an LTR base direction even when its first word is\n // RTL (an RTL span that wrapped onto this line). Without forcing LTR the\n // node inherits the first word's direction:'rtl' and the paint path\n // right-aligns the whole line at the left edge (x=curX), drawing it\n // off-screen. The canvas BiDi engine still reorders the embedded\n // Arabic/Hebrew runs within the LTR line.\n const node: LayoutText = {\n type: 'text',\n text: lineText,\n x: curX,\n y: lineBaselineY,\n width: measuredWidth,\n style: { ...textWords[0].style, direction: 'ltr' },\n };\n results.push(node);\n if (textWords[0].clipStyle) clipRuns.set(node, textWords[0].clipStyle);\n if (textWords[0].strokeImageStyle) strokeImageRuns.set(node, textWords[0].strokeImageStyle);\n } else {\n // Mixed styles: word by word\n for (const word of line.words) {\n if (word.text === '') {\n curX += word.width;\n continue;\n }\n\n // Atomic inline-block: position text inside the box (after margin + padding)\n if (word.boxOpen && word.boxClose) {\n const s = word.style;\n const textX = curX + s.marginLeft + s.borderLeftWidth + s.paddingLeft;\n const node: LayoutText = {\n type: 'text',\n text: word.text,\n x: textX,\n y: lineBaselineY,\n width: cachedMeasureWidth(ctx, word.text),\n style: word.style,\n };\n results.push(node);\n if (word.clipStyle) clipRuns.set(node, word.clipStyle);\n if (word.strokeImageStyle) strokeImageRuns.set(node, word.strokeImageStyle);\n curX += word.width;\n continue;\n }\n\n // Adjust baseline for vertical-align\n let baselineY = lineBaselineY;\n const va = word.style.verticalAlign;\n if (isShiftedVAlign(va)) {\n baselineY += verticalAlignShift(\n va, ctx, word.style, word.parentStyle ?? blockStyle, useBulletProbe);\n }\n const effectiveWidth = word.width + (word.isSpace ? justifyExtraPerSpace : 0);\n\n const node: LayoutText = {\n type: 'text',\n text: word.text,\n x: curX,\n y: baselineY,\n width: effectiveWidth,\n style: word.style,\n // Only when vertical-align moved this run off the line — an\n // underline from an unshifted declarer still hangs off the line.\n ...(baselineY !== lineBaselineY ? { lineBaselineY } : {}),\n };\n results.push(node);\n if (word.clipStyle) clipRuns.set(node, word.clipStyle);\n if (word.strokeImageStyle) strokeImageRuns.set(node, word.strokeImageStyle);\n\n curX += effectiveWidth;\n }\n }\n }\n\n // Emit a public LayoutLine record for this committed line.\n // bounds.width: justified lines fill lineMaxWidth (spaces expanded);\n // others use the measured words width.\n const lineWidth =\n align === 'justify' && justifyExtraPerSpace > 0\n ? lineMaxWidth\n : line.totalWidth;\n _lines.push({\n y: Math.round(lineBaselineY),\n text: line.words.map(w => w.text).join(''),\n bounds: {\n x: lineLeftX,\n // The line box starts at curY — this is the CSS line box, which\n // `lineAscent`/`lineDescent` grew to cover every box on the line. Ink\n // can still overflow it (an ascender under `line-height: 1`), exactly\n // as it does in the DOM; a caller that clips must allow for that.\n y: curY,\n width: lineWidth,\n height: lineBoxHeight,\n },\n });\n\n curY += lineBoxHeight;\n }\n\n assignInlineFragmentBoxes(ctx, results, clipRuns, (node, s, box) => {\n node.clip = {\n image: s.backgroundImage && s.backgroundImage !== 'none' ? s.backgroundImage : undefined,\n color: !isTransparent(s.backgroundColor) ? s.backgroundColor : undefined,\n ...box,\n };\n });\n assignInlineFragmentBoxes(ctx, results, strokeImageRuns, (node, s, box) => {\n node.strokeImage = { image: s.webkitTextStrokeImage, ...box };\n });\n\n return { nodes: results, height: curY - y };\n}\n\n/**\n * Give each text run covered by an inline paint declarer (background-clip:text\n * background, --rt-text-stroke-image) a paint box spanning the declaring\n * element's fragment on its line.\n *\n * Browsers paint the declaring element's background over its inline fragment\n * (the run of glyphs it covers on one line) and clip it to the text; with\n * `background-size:100% 100%` the gradient fills that fragment box. Consecutive\n * text nodes sharing the same declaring element (same style object) on the\n * same baseline form one fragment; a wrap to the next line starts a new one\n * (box-decoration-break:clone semantics — Chrome's default `slice` continues\n * the gradient across line fragments; accepted approximation), and unlike a\n * per-run gradient it never restarts per word.\n */\nfunction assignInlineFragmentBoxes(\n ctx: CanvasRenderingContext2D,\n results: LayoutNode[],\n runs: Map<LayoutText, ResolvedStyle>,\n assign: (\n node: LayoutText,\n declarer: ResolvedStyle,\n box: { x: number; y: number; width: number; height: number },\n ) => void,\n): void {\n if (runs.size === 0) return;\n const edges = (n: LayoutText) =>\n n.style.direction === 'rtl'\n ? { left: n.x - n.width, right: n.x } // RTL x is the right edge\n : { left: n.x, right: n.x + n.width };\n for (let i = 0; i < results.length;) {\n const first = results[i];\n const declarer = first.type === 'text' ? runs.get(first) : undefined;\n if (!declarer) { i++; continue; }\n let j = i;\n let left = Infinity, right = -Infinity;\n while (j < results.length) {\n const n = results[j];\n if (n.type !== 'text' || runs.get(n) !== declarer || n.y !== first.y) break;\n const e = edges(n);\n if (e.left < left) left = e.left;\n if (e.right > right) right = e.right;\n j++;\n }\n const { ascent, descent } = getFontMetrics(ctx, declarer);\n const box = {\n x: left,\n y: first.y - ascent,\n width: right - left,\n height: ascent + descent,\n };\n for (let k = i; k < j; k++) assign(results[k] as LayoutText, declarer, box);\n i = j;\n }\n}\n\n// ─── Block layout ──────────────────────────────────────────────────────\n\n/**\n * Collapse margins between two adjacent block elements.\n * Returns the effective spacing (max of the two margins, not sum).\n */\nfunction collapseMargins(prevMarginBottom: number, nextMarginTop: number): number {\n // Both positive: take the larger\n if (prevMarginBottom >= 0 && nextMarginTop >= 0) {\n return Math.max(prevMarginBottom, nextMarginTop);\n }\n // Both negative: take the more negative\n if (prevMarginBottom < 0 && nextMarginTop < 0) {\n return Math.min(prevMarginBottom, nextMarginTop);\n }\n // One positive, one negative: sum them\n return prevMarginBottom + nextMarginTop;\n}\n\n/**\n * Check if a node is a block-level display.\n */\nfunction isBlock(node: StyledNode): boolean {\n const d = node.style.display;\n return d === 'block' || d === 'list-item' || d === 'flex' || d === 'table' ||\n d === 'table-row' || d === 'table-cell' || d === 'table-row-group' ||\n d === 'table-header-group' || d === 'table-footer-group';\n}\n\nfunction allowsMarginCollapseThrough(node: StyledNode): boolean {\n const display = node.style.display;\n return (display === 'block' || display === 'list-item') &&\n (node.tagName === 'li' || node.tagName === 'ul' || node.tagName === 'ol' ||\n node.tagName === 'dd' || node.tagName === 'dt');\n}\n\nfunction collapsibleMarginTop(node: StyledNode): number {\n const marginTop = node.style.marginTop;\n if (!allowsMarginCollapseThrough(node) || node.style.paddingTop !== 0 ||\n node.style.borderTopWidth !== 0) {\n return marginTop;\n }\n const firstChild = node.children[0];\n return firstChild && isBlock(firstChild)\n ? collapseMargins(marginTop, collapsibleMarginTop(firstChild))\n : marginTop;\n}\n\n/**\n * Layout a block-level element and all its children.\n * Returns the LayoutBox and total height consumed (including margins).\n */\nfunction layoutBlock(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n x: number,\n y: number,\n availableWidth: number,\n clamp?: LineClampState,\n): { box: LayoutBox; height: number; marginBottomOut: number } {\n const style = node.style;\n\n // `-webkit-line-clamp` on a block container: start a shared line budget\n // here and thread it through descendant layout so the count spans block\n // children (Chrome legacy -webkit-box semantics). An ancestor's active\n // clamp wins over a nested one.\n if (!clamp && style.lineClamp > 0) {\n clamp = { remaining: style.lineClamp, exhausted: false };\n }\n\n // Box model\n const marginLeft = style.marginLeft;\n const marginRight = style.marginRight;\n const borderLeft = style.borderLeftWidth;\n const borderRight = style.borderRightWidth;\n const borderTop = style.borderTopWidth;\n const borderBottom = style.borderBottomWidth;\n const padLeft = style.paddingLeft;\n const padRight = style.paddingRight;\n const padTop = style.paddingTop;\n const padBottom = style.paddingBottom;\n\n const boxX = x + marginLeft;\n // If element has explicit width, use it; otherwise fill available width\n const boxWidth = (style.width > 0)\n ? style.width\n : availableWidth - marginLeft - marginRight;\n const contentX = boxX + borderLeft + padLeft;\n const contentWidth = Math.max(0, boxWidth - borderLeft - borderRight - padLeft - padRight);\n\n const boxY = y;\n const contentStartY = boxY + borderTop + padTop;\n\n const box: LayoutBox = {\n type: 'box',\n style,\n x: boxX,\n y: boxY,\n width: boxWidth,\n height: 0, // computed below\n tagName: node.tagName,\n children: [],\n listMarker: node.listMarker,\n };\n\n // Flex layout\n if (style.display === 'flex') {\n const result = layoutFlex(ctx, node, contentX, contentStartY, contentWidth);\n box.children = result.children;\n box.height = borderTop + padTop + result.height + padBottom + borderBottom;\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n }\n\n // Table layout\n if (style.display === 'table') {\n const result = layoutTable(ctx, node, contentX, contentStartY, contentWidth);\n box.children = result.children;\n box.height = borderTop + padTop + result.height + padBottom + borderBottom;\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n }\n\n // Empty block elements: zero content height (CSS spec — no line boxes created).\n // Only min-height or padding/border contribute to height.\n if (node.children.length === 0) {\n box.height = borderTop + padTop + padBottom + borderBottom;\n if (style.minHeight > 0) box.height = Math.max(box.height, style.minHeight);\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n }\n\n // Layout children\n if (hasOnlyInlineChildren(node)) {\n // Inline formatting context\n const bulletProbe = node.tagName === 'li' && BULLET_MARKERS.has(style.listStyleType);\n const { nodes, height } = layoutInlineContent(ctx, node, contentX, contentStartY, contentWidth, bulletProbe, clamp);\n box.children = nodes;\n box.height = borderTop + padTop + height + padBottom + borderBottom;\n } else {\n // Block formatting context — stack children vertically\n let curY = contentStartY;\n let prevMarginBottom = 0;\n let hasContent = false; // tracks whether we've placed any content\n // Margin collapsing through parent: only for list elements.\n const allowCollapseThrough = allowsMarginCollapseThrough(node);\n\n for (let ci = 0; ci < node.children.length; ci++) {\n const child = node.children[ci];\n\n // Line-clamp budget exhausted — everything below the cut is dropped,\n // including the margin trailing the cut line.\n if (clamp && (clamp.exhausted || clamp.remaining <= 0)) {\n clamp.exhausted = true;\n prevMarginBottom = 0;\n break;\n }\n\n if (child.tagName === '#text' || isInline(child)) {\n // Collect ALL consecutive inline/text children into one group\n const inlineChildren: StyledNode[] = [child];\n while (ci + 1 < node.children.length) {\n const next = node.children[ci + 1];\n if (next.tagName === '#text' || isInline(next)) {\n inlineChildren.push(next);\n ci++;\n } else {\n break;\n }\n }\n\n // Apply pending margin before inline content\n curY += prevMarginBottom;\n prevMarginBottom = 0;\n\n const inlineGroup: StyledNode = {\n element: null,\n tagName: 'div',\n style: { ...node.style, display: 'block', marginTop: 0, marginBottom: 0, paddingTop: 0, paddingBottom: 0, borderTopWidth: 0, borderBottomWidth: 0 },\n children: inlineChildren,\n textContent: null,\n };\n const bulletProbe2 = node.tagName === 'li' && BULLET_MARKERS.has(style.listStyleType);\n const { nodes, height } = layoutInlineContent(ctx, inlineGroup, contentX, curY, contentWidth, bulletProbe2, clamp);\n box.children.push(...nodes);\n curY += height;\n prevMarginBottom = 0;\n hasContent = true;\n continue;\n }\n\n // Block child — collapse margins\n const childMarginTop = collapsibleMarginTop(child);\n\n // First child margin-top collapses through parent if parent has no\n // top padding/border and doesn't establish a new BFC.\n if (!hasContent && padTop === 0 && borderTop === 0 && allowCollapseThrough) {\n // Skip — margin collapses with parent's margin\n } else {\n const collapsed = collapseMargins(prevMarginBottom, childMarginTop);\n curY += collapsed;\n }\n\n const { box: childBox, height: childTotalHeight, marginBottomOut } = layoutBlock(\n ctx, child, contentX, curY, contentWidth, clamp,\n );\n box.children.push(childBox);\n curY += childTotalHeight;\n // A child truncated by line-clamp clips its trailing margin too.\n prevMarginBottom = clamp?.exhausted ? 0 : marginBottomOut;\n hasContent = true;\n }\n\n // Last child's margin-bottom collapses through parent if no bottom border/padding.\n // Root container does NOT collapse last-child margin (it defines the content height).\n let marginBottomOut = style.marginBottom;\n const canCollapseThrough = padBottom === 0 && borderBottom === 0 &&\n style.minHeight === 0 && allowCollapseThrough;\n if (canCollapseThrough) {\n // Last child's margin passes through to become parent's effective margin-bottom\n marginBottomOut = collapseMargins(style.marginBottom, prevMarginBottom);\n }\n\n // Include last child's margin-bottom in parent height when it can't collapse through\n let contentEnd = curY - contentStartY;\n if (!canCollapseThrough) {\n contentEnd += prevMarginBottom;\n }\n contentEnd = Math.max(0, contentEnd);\n box.height = borderTop + padTop + contentEnd + padBottom + borderBottom;\n if (style.minHeight > 0) box.height = Math.max(box.height, style.minHeight);\n return { box, height: box.height, marginBottomOut };\n }\n\n if (style.minHeight > 0) box.height = Math.max(box.height, style.minHeight);\n return { box, height: box.height, marginBottomOut: style.marginBottom };\n}\n\n// ─── Table layout ──────────────────────────────────────────────────────\n\nfunction layoutTable(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n contentX: number,\n contentY: number,\n contentWidth: number,\n): { children: LayoutNode[]; height: number } {\n const children: LayoutNode[] = [];\n\n // Collect rows from thead, tbody, tfoot, or direct tr children\n const rows: StyledNode[] = [];\n for (const child of node.children) {\n if (child.tagName === 'tr') {\n rows.push(child);\n } else if (['thead', 'tbody', 'tfoot'].includes(child.tagName)) {\n for (const grandchild of child.children) {\n if (grandchild.tagName === 'tr') rows.push(grandchild);\n }\n }\n }\n\n if (rows.length === 0) return { children, height: 0 };\n\n // Determine column count from first row\n const colCount = Math.max(...rows.map(r => r.children.filter(c => c.tagName === 'td' || c.tagName === 'th').length));\n if (colCount === 0) return { children, height: 0 };\n\n // Equal column widths (simple approach)\n const colWidth = contentWidth / colCount;\n\n let curY = contentY;\n\n for (const row of rows) {\n const cells = row.children.filter(c => c.tagName === 'td' || c.tagName === 'th');\n let maxCellHeight = 0;\n const cellBoxes: LayoutBox[] = [];\n\n for (let i = 0; i < cells.length; i++) {\n const cell = cells[i];\n const cellX = contentX + i * colWidth;\n\n const { box: cellBox, height: cellHeight } = layoutBlock(ctx, cell, cellX, curY, colWidth);\n cellBoxes.push(cellBox);\n maxCellHeight = Math.max(maxCellHeight, cellHeight);\n }\n\n // Normalize cell heights to the tallest cell in the row\n for (const cellBox of cellBoxes) {\n cellBox.height = maxCellHeight;\n children.push(cellBox);\n }\n\n curY += maxCellHeight;\n }\n\n return { children, height: curY - contentY };\n}\n\n// ─── Flex layout ───────────────────────────────────────────────────────\n\nfunction layoutFlex(\n ctx: CanvasRenderingContext2D,\n node: StyledNode,\n contentX: number,\n contentY: number,\n contentWidth: number,\n): { children: LayoutNode[]; height: number } {\n const style = node.style;\n const gap = style.gap;\n const children: LayoutNode[] = [];\n\n const flexChildren = node.children.filter(c => c.tagName !== '#text' || c.textContent?.trim());\n if (flexChildren.length === 0) return { children, height: 0 };\n\n if (style.flexDirection === 'row' || style.flexDirection === '') {\n // Row layout\n const totalGaps = gap * (flexChildren.length - 1);\n const totalGrow = flexChildren.reduce((s, c) => s + (c.style.flexGrow || 0), 0);\n const flexBasis = (contentWidth - totalGaps) / (totalGrow || flexChildren.length);\n\n let curX = contentX;\n let maxHeight = 0;\n\n for (const child of flexChildren) {\n if (child.tagName === '#text') continue;\n const grow = child.style.flexGrow || (totalGrow === 0 ? 1 : 0);\n const childWidth = flexBasis * grow;\n\n const { box, height } = layoutBlock(ctx, child, curX, contentY, childWidth);\n children.push(box);\n maxHeight = Math.max(maxHeight, height);\n curX += childWidth + gap;\n }\n\n return { children, height: maxHeight };\n }\n\n // Column layout (fallback)\n let curY = contentY;\n for (const child of flexChildren) {\n if (child.tagName === '#text') continue;\n const { box, height } = layoutBlock(ctx, child, contentX, curY, contentWidth);\n children.push(box);\n curY += height + gap;\n }\n return { children, height: curY - contentY };\n}\n\n// ─── List marker layout ────────────────────────────────────────────────\n\n/**\n * Add list marker to a layout box if applicable.\n */\nfunction addListMarker(\n ctx: CanvasRenderingContext2D,\n box: LayoutBox,\n node: StyledNode,\n): void {\n if (!node.listMarker) return;\n // `::marker { content: none }` suppresses the marker entirely —\n // canonical CSS behavior, matches the DOM reference.\n if (node.markerHidden) return;\n\n const style = node.style;\n // Marker style = li style with explicit `::marker` overrides applied on top.\n // `markerStyle` holds only keys explicitly set by `::marker` rules, so a\n // missing key falls back to the li style. A present key (incl. 0) wins.\n const ms = node.markerStyle;\n const markerStyleObj: ResolvedStyle = ms ? { ...style, ...ms } : style;\n\n ctx.font = buildCanvasFont(markerStyleObj);\n // ascent + descent is the li's line-height by construction, so one call\n // gives both the marker's baseline and the box it reports.\n const strut = leadedBox(ctx, style);\n const baselineY = box.y + style.borderTopWidth + style.paddingTop + strut.ascent;\n\n const markerWidth = cachedMeasureWidth(ctx, node.listMarker);\n const isRTL = style.direction === 'rtl';\n const isBullet = BULLET_MARKERS.has(style.listStyleType);\n // Gap between marker and content, matching Chrome (measured empirically):\n // - bullets: Chrome paints a symbol (diameter ascent/3) whose ink ends\n // 7px + ascent/3 before the content edge, centered ascent/3 above the\n // baseline. We keep the glyph but position its ink to land there.\n // - text markers (\"1.\"): Chrome's marker text carries a \". \" suffix, so\n // the gap is one space advance and the baseline is the line baseline.\n // `::marker { padding-inline-end: <length> }` overrides the gap — we honor\n // the direction-resolved physical padding (paddingRight in LTR, paddingLeft\n // in RTL) when explicitly set on the marker.\n const explicitGap = isRTL ? ms?.paddingLeft : ms?.paddingRight;\n\n let markerX: number;\n let markerY = baselineY;\n let markerDirection = 'ltr';\n // Style/width the marker glyph is actually DRAWN with. Numbers draw at the\n // li font (unchanged); bullets scale up (see below), so keep these separate.\n let markerDrawStyle: ResolvedStyle = markerStyleObj;\n let markerDrawWidth = markerWidth;\n const contentStartX = box.x + style.borderLeftWidth + style.paddingLeft;\n const boxRightEdge = box.x + box.width;\n if (isBullet) {\n const { ascent } = getFontMetrics(ctx, markerStyleObj);\n const m = ctx.measureText(node.listMarker);\n // Blink's marker unit: the disc DIAMETER, the variable part of the gap, and\n // the vertical centering all key off this one value (a 2/3·ascent marker\n // box with a half-filling disc → ascent/3). Named once so tuning one keeps\n // the trio in sync.\n const markerUnit = ascent / 3;\n const gap = explicitGap !== undefined ? explicitGap : 7 + markerUnit;\n // Chrome paints bullet symbols (disc/circle/square) as a SYNTHETIC shape of\n // that diameter, NOT the font's smaller '•'/'○'/'■' glyph (Roboto's '•' ink\n // is ~0.22em vs Chrome's ~0.31em disc). Match it by scaling the glyph so its\n // ink height equals markerUnit. Keeping the marker a text node means fill /\n // stroke / shadow / gradient still apply exactly as before.\n const inkH = (m.actualBoundingBoxAscent ?? 0) + (m.actualBoundingBoxDescent ?? 0);\n const scale = inkH > 0 ? markerUnit / inkH : 1;\n const inkRight = (m.actualBoundingBoxRight ?? markerWidth) * scale;\n const inkLeft = (m.actualBoundingBoxLeft ?? 0) * scale;\n const glyphInkCenter =\n (((m.actualBoundingBoxAscent ?? 0) - (m.actualBoundingBoxDescent ?? 0)) / 2) * scale;\n if (isRTL) {\n // actualBoundingBoxLeft is positive when ink extends left of origin\n markerX = boxRightEdge + gap + inkLeft;\n } else {\n markerX = contentStartX - gap - inkRight;\n }\n markerY = baselineY - markerUnit + glyphInkCenter;\n markerDrawStyle = { ...markerStyleObj, fontSize: markerStyleObj.fontSize * scale };\n markerDrawWidth = markerWidth * scale;\n } else {\n const gap = explicitGap !== undefined\n ? explicitGap\n : cachedMeasureWidth(ctx, ' ');\n if (isRTL) {\n // RTL: marker in the parent's right padding area (outside the li box).\n // Numbered markers (\"1.\") need RTL direction to display as \".1\".\n // With textAlign='right', x is the right edge — so add markerWidth.\n const isNumbered = /\\d/.test(node.listMarker);\n if (isNumbered) {\n markerDirection = 'rtl';\n markerX = boxRightEdge + gap + markerWidth;\n } else {\n markerX = boxRightEdge + gap;\n }\n } else {\n // LTR: marker in the parent's left padding area (outside the li box).\n markerX = contentStartX - markerWidth - gap;\n }\n }\n\n box.children.unshift({\n type: 'text',\n text: node.listMarker,\n x: markerX,\n y: markerY,\n width: markerDrawWidth,\n style: { ...markerDrawStyle, textDecorationLine: 'none', textDecorations: [], fontWeight: ms?.fontWeight ?? 400, fontStyle: ms?.fontStyle ?? 'normal', direction: markerDirection },\n });\n\n // Also publish the marker through the LayoutLine stream so result.lines\n // sees the bullet/number alongside the item text. Markers are added AFTER\n // inline content is laid out, so they don't go through layoutInlineContent.\n // The buildLayoutTree sort+merge step picks up the marker by its baseline.\n // RTL numbered markers store their right edge in markerX (textAlign trick).\n // bounds.width is the (scaled) glyph ADVANCE, not its ink extent — same\n // convention as numbered markers; the ink right edge itself is pinned to\n // contentStart - gap above.\n const markerLeftX = markerDirection === 'rtl' ? markerX - markerDrawWidth : markerX;\n _lines.push({\n y: Math.round(baselineY),\n text: node.listMarker,\n bounds: {\n x: markerLeftX,\n y: box.y + style.borderTopWidth + style.paddingTop,\n width: markerDrawWidth,\n height: strut.ascent + strut.descent,\n },\n });\n}\n\n// ─── Main entry ────────────────────────────────────────────────────────\n\n/**\n * Build the layout tree from the styled tree using pure canvas measurement.\n * No DOM measurements used — all positions computed from CSS values + canvas.measureText.\n */\nexport function buildLayoutTree(\n ctx: CanvasRenderingContext2D,\n styledTree: StyledNode,\n containerWidth: number,\n useDomMeasurements = true,\n debug?: (entry: import('./types.ts').DebugEntry) => void,\n): { root: LayoutBox; height: number; lines: LayoutLine[] } {\n _useDomMeasurements = useDomMeasurements;\n _debug = debug;\n\n // Clear caches — fonts may have loaded since last call\n _lineHeightCache.clear();\n _fontMetricsCache.clear();\n _fontStringCache.clear();\n _measureCache.clear();\n _lines = [];\n\n // The styledTree root is our container div — layout its children as a block flow\n const { box, height } = layoutBlock(ctx, styledTree, 0, 0, containerWidth);\n\n // Add list markers post-layout\n addListMarkersRecursive(ctx, box, styledTree);\n\n // Sort by baseline y, then by left edge so cross-cell content merges in\n // reading order (LTR). List markers sit at smaller x than their content\n // and so come first, producing \"• Item\" rather than \"Item •\".\n const sorted = _lines.slice().sort((a, b) =>\n (a.y - b.y) || (a.bounds.x - b.bounds.x)\n );\n const lines: LayoutLine[] = [];\n for (const candidate of sorted) {\n const last = lines[lines.length - 1];\n // Tolerance keys off the candidate's line height (matches the legacy\n // extractLines behavior). Using max(last, candidate) is symmetric but\n // grows after each merge as last.bounds.height becomes the union — that\n // leaks across rows in tight multi-column layouts.\n const tolerance = candidate.bounds.height * 0.5;\n if (last && Math.abs(candidate.y - last.y) < tolerance) {\n // Cross-cell merge: insert a space separator so the text stays\n // readable when N cells of a table row collapse into one LayoutLine.\n // Skip if either side already has a boundary space.\n const needsSep = last.text.length > 0 && candidate.text.length > 0 &&\n !/\\s$/.test(last.text) && !/^\\s/.test(candidate.text);\n last.text += (needsSep ? ' ' : '') + candidate.text;\n // Carry baseline forward so the next comparison uses the running\n // edge of the group, not the stale first element's baseline.\n last.y = Math.max(last.y, candidate.y);\n const x1 = Math.min(last.bounds.x, candidate.bounds.x);\n const y1 = Math.min(last.bounds.y, candidate.bounds.y);\n const x2 = Math.max(last.bounds.x + last.bounds.width, candidate.bounds.x + candidate.bounds.width);\n const y2 = Math.max(last.bounds.y + last.bounds.height, candidate.bounds.y + candidate.bounds.height);\n last.bounds = { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };\n } else {\n lines.push({ y: candidate.y, text: candidate.text, bounds: { ...candidate.bounds } });\n }\n }\n return { root: box, height, lines };\n}\n\nfunction addListMarkersRecursive(\n ctx: CanvasRenderingContext2D,\n box: LayoutBox,\n node: StyledNode,\n): void {\n addListMarker(ctx, box, node);\n\n // Match children — box.children may have extra text/inline nodes,\n // so we correlate by walking both in parallel\n let boxChildIdx = 0;\n for (const styledChild of node.children) {\n if (styledChild.tagName === '#text' || isInline(styledChild)) {\n continue;\n }\n // Find the matching LayoutBox\n while (boxChildIdx < box.children.length) {\n const layoutChild = box.children[boxChildIdx];\n if (layoutChild.type === 'box' && layoutChild.tagName === styledChild.tagName) {\n addListMarkersRecursive(ctx, layoutChild, styledChild);\n boxChildIdx++;\n break;\n }\n boxChildIdx++;\n }\n }\n}\n","import type { DecorationEntry, LayoutNode, LayoutBox, LayoutText, ResolvedStyle } from './types.js';\nimport { buildCanvasFont, isTransparent, getFontMetrics, hasTextClip, isShiftedVAlign } from './layout.js';\nimport { paintOrderHasStrokeFirst } from './css-resolver.js';\n\n/**\n * Parse a CSS text-shadow string into individual shadow values.\n * Format: \"2px 2px 4px rgba(0,0,0,0.3), ...\"\n */\nexport function parseTextShadows(shadow: string): Array<{\n offsetX: number;\n offsetY: number;\n blur: number;\n color: string;\n}> {\n if (!shadow || shadow === 'none') return [];\n\n const shadows: Array<{ offsetX: number; offsetY: number; blur: number; color: string }> = [];\n\n // Split by comma but not within parentheses\n const parts = shadow.split(/,(?![^(]*\\))/);\n\n for (const part of parts) {\n const trimmed = part.trim();\n // Extract color (rgb/rgba or named) and numbers\n const colorMatch = trimmed.match(/(rgb[a]?\\([^)]+\\)|#[0-9a-fA-F]+|\\b[a-z]+\\b)(?:\\s|$)/i);\n const numMatches = trimmed.match(/-?[\\d.]+px/g);\n\n if (numMatches && numMatches.length >= 2) {\n const nums = numMatches.map(n => parseFloat(n));\n shadows.push({\n offsetX: nums[0],\n offsetY: nums[1],\n blur: nums[2] || 0,\n color: colorMatch ? colorMatch[1] : 'rgba(0,0,0,1)',\n });\n }\n }\n\n return shadows;\n}\n\n/**\n * Check if a border is visible.\n */\nfunction hasBorder(style: ResolvedStyle, side: 'Top' | 'Right' | 'Bottom' | 'Left'): boolean {\n const width = style[`border${side}Width` as keyof ResolvedStyle] as number;\n const borderStyle = style[`border${side}Style` as keyof ResolvedStyle] as string;\n return width > 0 && borderStyle !== 'none';\n}\n\n/**\n * Draw a decoration line with the given style (solid, dotted, dashed, double, wavy).\n */\nexport function drawDecorationLine(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n width: number,\n lineWidth: number,\n decoStyle: string,\n color: string | CanvasGradient,\n): void {\n // Chrome paints decorations as crisp integer-pixel bands. Snap the stroke\n // center so the band edges land on the pixel grid.\n y = Math.round(y - lineWidth / 2) + lineWidth / 2;\n ctx.save();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n\n if (decoStyle === 'double') {\n const gap = Math.max(lineWidth, 2);\n ctx.lineWidth = Math.max(0.5, lineWidth * 0.5);\n ctx.beginPath();\n ctx.moveTo(x, y - gap / 2);\n ctx.lineTo(x + width, y - gap / 2);\n ctx.moveTo(x, y + gap / 2);\n ctx.lineTo(x + width, y + gap / 2);\n ctx.stroke();\n } else if (decoStyle === 'wavy') {\n const amplitude = Math.max(1.5, lineWidth);\n const wavelength = amplitude * 4;\n ctx.beginPath();\n ctx.moveTo(x, y);\n for (let cx = x; cx < x + width; cx += wavelength) {\n ctx.quadraticCurveTo(cx + wavelength / 4, y - amplitude, cx + wavelength / 2, y);\n ctx.quadraticCurveTo(cx + wavelength * 3 / 4, y + amplitude, cx + wavelength, y);\n }\n ctx.stroke();\n } else {\n // solid, dotted, dashed\n if (decoStyle === 'dotted') ctx.setLineDash([lineWidth, lineWidth * 2]);\n else if (decoStyle === 'dashed') ctx.setLineDash([lineWidth * 3, lineWidth * 2]);\n ctx.beginPath();\n ctx.moveTo(x, y);\n ctx.lineTo(x + width, y);\n ctx.stroke();\n }\n\n ctx.setLineDash([]);\n ctx.restore();\n}\n\n/**\n * Parse a CSS linear-gradient into canvas CanvasGradient.\n */\nexport function parseLinearGradient(\n ctx: CanvasRenderingContext2D,\n bgImage: string,\n x: number,\n width: number,\n y: number,\n height: number,\n): CanvasGradient | null {\n // Extract content inside linear-gradient(...) handling nested parens\n const startIdx = bgImage.indexOf('linear-gradient(');\n if (startIdx === -1) return null;\n let depth = 0;\n let endIdx = -1;\n for (let i = startIdx + 16; i < bgImage.length; i++) {\n if (bgImage[i] === '(') depth++;\n else if (bgImage[i] === ')') {\n if (depth === 0) { endIdx = i; break; }\n depth--;\n }\n }\n if (endIdx === -1) return null;\n const innerContent = bgImage.slice(startIdx + 16, endIdx);\n\n // Split by commas not inside parentheses\n const parts: string[] = [];\n depth = 0;\n let start = 0;\n const inner = innerContent;\n for (let i = 0; i < inner.length; i++) {\n if (inner[i] === '(') depth++;\n else if (inner[i] === ')') depth--;\n else if (inner[i] === ',' && depth === 0) {\n parts.push(inner.slice(start, i).trim());\n start = i + 1;\n }\n }\n parts.push(inner.slice(start).trim());\n // Parse angle/direction\n let angle = 180; // default top to bottom\n let colorStartIdx = 0;\n const firstPart = parts[0];\n if (firstPart.endsWith('deg')) {\n angle = parseFloat(firstPart);\n colorStartIdx = 1;\n } else if (firstPart === 'to right') {\n angle = 90; colorStartIdx = 1;\n } else if (firstPart === 'to left') {\n angle = 270; colorStartIdx = 1;\n } else if (firstPart === 'to bottom') {\n angle = 180; colorStartIdx = 1;\n } else if (firstPart === 'to top') {\n angle = 0; colorStartIdx = 1;\n }\n\n const rad = (angle - 90) * Math.PI / 180;\n const cx = x + width / 2;\n const cy = y + height / 2;\n const len = Math.abs(width * Math.cos(rad)) + Math.abs(height * Math.sin(rad));\n const dx = Math.cos(rad) * len / 2;\n const dy = Math.sin(rad) * len / 2;\n\n const gradient = ctx.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy);\n\n const colors = parts.slice(colorStartIdx);\n for (let i = 0; i < colors.length; i++) {\n const entry = colors[i].trim();\n // Match color followed by optional percentage: \"rgb(220, 38, 38) 0%\"\n // The percentage is always at the very end after the last space outside parens\n let color = entry;\n let stop = i / Math.max(1, colors.length - 1);\n const percentMatch = entry.match(/\\s+([\\d.]+%)\\s*$/);\n if (percentMatch) {\n stop = parseFloat(percentMatch[1]) / 100;\n color = entry.slice(0, entry.length - percentMatch[0].length).trim();\n }\n try {\n gradient.addColorStop(stop, color);\n } catch {\n // Invalid color, skip\n }\n }\n\n return gradient;\n}\n\n/** The solid fill color for text: -webkit-text-fill-color if set, else color. */\nexport function textFillColor(style: ResolvedStyle): string {\n return style.webkitTextFillColor && style.webkitTextFillColor !== 'transparent'\n ? style.webkitTextFillColor : style.color;\n}\n\n/**\n * Text decoration thickness for `auto`. Chromium paints an integer-pixel\n * band of max(1, floor(fontSize / 10)) regardless of font (measured across\n * 6 fonts × 16-64px against the DOM raster).\n */\nexport function decorationThickness(fontSize: number): number {\n return Math.max(1, Math.floor(fontSize / 10));\n}\n\n/**\n * The band width for one decoration entry: the declarer's explicit\n * text-decoration-thickness when set (Chrome draws round(T) rows; a declared\n * 0 hides the band — callers skip on 0), else the auto thickness from the\n * declarer's font size. Shared by both renderers.\n */\nexport function bandWidthFor(deco: DecorationEntry): number {\n const t = deco.declarer.textDecorationThickness;\n if (t === null) return decorationThickness(deco.declarer.fontSize);\n return t <= 0 ? 0 : Math.max(1, Math.round(t));\n}\n\n/**\n * Band-center delta below the baseline for EXPLICIT underline geometry, or\n * null for auto (each renderer keeps its own auto formula). Chrome-measured:\n * an explicit offset puts the band TOP at baseline + offset; auto offset\n * with an explicit thickness T puts it at baseline + ceil(T/2) — measured\n * exactly for T ∈ {1, 3, 4, 5, 8, 10}.\n */\nexport function explicitUnderlineDelta(\n deco: DecorationEntry,\n lineWidth: number,\n): number | null {\n const offset = deco.declarer.textUnderlineOffset;\n if (offset !== null) return offset + lineWidth / 2;\n if (deco.declarer.textDecorationThickness !== null)\n return Math.ceil(lineWidth / 2) + lineWidth / 2;\n return null;\n}\n\n/** Apply the canvas stroke settings for -webkit-text-stroke. A gradient stroke\n * (webkitTextStrokeImage, pre-resolved to a CanvasGradient) wins over the solid\n * stroke color, mirroring how a background-clip:text gradient wins over `color`\n * for the fill. */\nexport function applyTextStroke(\n ctx: CanvasRenderingContext2D,\n style: ResolvedStyle,\n strokeGradient?: CanvasGradient | null,\n): void {\n ctx.strokeStyle = strokeGradient || style.webkitTextStrokeColor || style.color;\n ctx.lineWidth = style.webkitTextStrokeWidth;\n const join = style.strokeLinejoin;\n ctx.lineJoin = join === 'miter' || join === 'bevel' ? join : 'round';\n}\n\n/**\n * Render a single text node to canvas.\n * @param gradientFill — pre-computed gradient for background-clip:text spanning full element\n * @param strokeGradient — pre-computed gradient for -webkit-text-stroke-image spanning full element\n */\nfunction renderText(\n ctx: CanvasRenderingContext2D,\n node: LayoutText,\n gradientFill?: CanvasGradient | string | null,\n strokeGradient?: CanvasGradient | null,\n): void {\n const { style } = node;\n\n ctx.save();\n ctx.font = buildCanvasFont(style);\n ctx.textBaseline = 'alphabetic';\n ctx.fontKerning = style.fontKerning === 'none' ? 'none' : 'normal';\n if (Number.isFinite(style.letterSpacing) && style.letterSpacing !== 0) {\n ctx.letterSpacing = `${style.letterSpacing}px`;\n }\n if (style.wordSpacing) {\n (ctx as any).wordSpacing = `${style.wordSpacing}px`;\n }\n if (style.direction === 'rtl') {\n ctx.direction = 'rtl';\n ctx.textAlign = 'right';\n }\n\n const hasOwnClip = hasTextClip(style);\n const isStrokedText = style.webkitTextStrokeWidth > 0;\n const isFillTransparent = style.webkitTextFillColor === 'transparent' ||\n style.color === 'transparent';\n\n // The background-clip:text paint from a declaring INLINE ancestor (e.g.\n // <span>/<s>) whose non-inheriting background this run's own style doesn't\n // carry: a gradient and/or solid color. Layout resolves the geometry — a box\n // spanning the declaring element's fragment on this line (see\n // assignInlineFragmentBoxes) — and this paint wins over any ancestor block\n // `gradientFill`, because Chrome clips the NEAREST declaring element's\n // background to the glyphs.\n const inlineClipPaint: CanvasGradient | string | null = node.clip\n ? (node.clip.image\n ? parseLinearGradient(\n ctx, node.clip.image,\n node.clip.x, node.clip.width,\n node.clip.y, node.clip.height,\n )\n : null) ?? node.clip.color ?? null\n : null;\n\n // An ancestor's clip paint only shows when this run's own fill is\n // transparent — an opaque own color paints over the clipped background and\n // wins.\n const isGradientText = hasOwnClip ||\n ((gradientFill != null || inlineClipPaint != null) && isFillTransparent);\n\n // What actually fills this run's glyphs (and any clipped decoration band):\n // the nearest inline declarer's paint if present, else the ancestor block's.\n const effectiveGradient = inlineClipPaint ?? gradientFill ?? null;\n\n // Same for the stroke gradient: an inline --rt-text-stroke-image declarer's\n // fragment gradient wins over an ancestor block's threaded one.\n const inlineStrokeGradient = node.strokeImage\n ? parseLinearGradient(\n ctx, node.strokeImage.image,\n node.strokeImage.x, node.strokeImage.width,\n node.strokeImage.y, node.strokeImage.height,\n )\n : null;\n const effectiveStrokeGradient = inlineStrokeGradient ?? strokeGradient ?? null;\n\n // Text shadow (drawn behind the text). Cast the shadow from the shape that\n // is actually painted: the fill when it's visible, and/or the stroke. This\n // matters for stroked text with a transparent fill (color:transparent +\n // -webkit-text-stroke), where CSS casts the shadow from the stroke outline\n // rather than the invisible fill.\n const shadows = parseTextShadows(style.textShadow);\n if (shadows.length > 0) {\n const hasVisibleFill = isGradientText || !isFillTransparent;\n for (const shadow of shadows) {\n ctx.save();\n ctx.shadowOffsetX = shadow.offsetX;\n ctx.shadowOffsetY = shadow.offsetY;\n ctx.shadowBlur = shadow.blur;\n ctx.shadowColor = shadow.color;\n if (hasVisibleFill) {\n ctx.fillStyle = isGradientText && effectiveGradient ? effectiveGradient : textFillColor(style);\n ctx.fillText(node.text, node.x, node.y);\n }\n if (isStrokedText) {\n applyTextStroke(ctx, style, effectiveStrokeGradient);\n ctx.strokeText(node.text, node.x, node.y);\n }\n ctx.restore();\n }\n }\n\n const drawFill = () => {\n if (isGradientText) {\n ctx.save();\n ctx.fillStyle = effectiveGradient || style.color;\n ctx.fillText(node.text, node.x, node.y);\n ctx.restore();\n } else if (!isFillTransparent) {\n // Normal text fill. A transparent fill paints NOTHING, stroked or not —\n // Chrome hides the glyphs entirely for `-webkit-text-fill-color:\n // transparent` (or `color: transparent`) even without a stroke.\n ctx.fillStyle = textFillColor(style);\n ctx.fillText(node.text, node.x, node.y);\n }\n };\n\n const drawStroke = () => {\n if (!isStrokedText) return;\n ctx.save();\n applyTextStroke(ctx, style, effectiveStrokeGradient);\n ctx.strokeText(node.text, node.x, node.y);\n ctx.restore();\n };\n\n if (paintOrderHasStrokeFirst(style.paintOrder)) {\n drawStroke();\n drawFill();\n } else {\n drawFill();\n drawStroke();\n }\n\n // Text decorations — use font metrics for accurate positioning.\n // Each entry paints with its ORIGIN element's color/style (ancestors first,\n // so a child's own decoration lands on top), matching Chrome's non-inherited\n // decoration propagation.\n //\n // Geometry splits, measured against Chrome for `30px ABC + 80px Tale` under\n // one declaration (see tests/decorating-box-geometry.test.ts):\n // - THICKNESS is the decorating box's for all three lines — the band over\n // the 80px child stays 3px, the 30px declarer's.\n // - The UNDERLINE also takes its position from the decorating box: one flat\n // band at rows 225-227 across both runs. It hangs off the alphabetic\n // baseline, which every fragment on the line shares.\n // - The OVERLINE and the LINE-THROUGH do NOT: Chrome steps them per\n // fragment (193-195 vs 148-150, and 213-215 vs 168-170), because each\n // hangs off the crossed fragment's own ascent, not a shared line.\n //\n // `vertical-align` splits the same way: an underline declared ABOVE a\n // `super` child stays flat across it (measured: one band, x 0-228), while\n // the overline and the strike step up with the child. So the underline\n // hangs off the DECLARER's baseline — the line's own, unless the declarer\n // is the shifted element itself, which then carries the band up with it.\n const textWidth = node.width;\n // For RTL text, node.x is the right edge (textAlign='right').\n // Decoration lines need the left edge as start position.\n const decoX = style.direction === 'rtl' ? node.x - textWidth : node.x;\n\n if (style.textDecorations.length > 0) {\n for (const deco of style.textDecorations) {\n const decoWidth = bandWidthFor(deco);\n if (decoWidth <= 0) continue; // declared text-decoration-thickness: 0\n // A transparent decoration inside a background-clip:text element shows\n // the clipped background through the band (Chrome includes decorations\n // in the clip region), so paint it with the gradient — REGARDLESS of\n // this run's own glyph fill: a solid-colored span inside a gradient\n // element still gets the gradient band across it. Transparent with no\n // gradient ancestor paints nothing.\n let color: string | CanvasGradient = deco.color;\n if (isTransparent(deco.color)) {\n if (effectiveGradient) {\n color = effectiveGradient;\n } else {\n continue;\n }\n }\n const decoStyle = deco.style || 'solid';\n\n // Chrome strokes decorations with -webkit-text-stroke, same as glyphs\n // (measured: red text + 3px blue stroke + underline adds only blue\n // pixels — the stroke swallows the thin band). Approximate the outline\n // with a thicker stroke-colored underlay; the decoration paint on top\n // keeps whatever the stroke leaves visible (decoWidth - strokeWidth).\n const strokeW = style.webkitTextStrokeWidth > 0 ? style.webkitTextStrokeWidth : 0;\n const strokeColor: string | CanvasGradient =\n effectiveStrokeGradient || style.webkitTextStrokeColor || style.color;\n const paintBand = (y: number) => {\n // A gradient stroke (CanvasGradient) is never transparent; a solid\n // stroke color still gets the transparent check below.\n const strokeIsTransparent =\n typeof strokeColor === 'string' && isTransparent(strokeColor);\n if (strokeW > 0 && !strokeIsTransparent) {\n drawDecorationLine(ctx, decoX, y, textWidth, decoWidth + strokeW, decoStyle, strokeColor);\n const inner = decoWidth - strokeW;\n if (inner > 0) {\n drawDecorationLine(ctx, decoX, y, textWidth, inner, decoStyle, color);\n }\n } else {\n drawDecorationLine(ctx, decoX, y, textWidth, decoWidth, decoStyle, color);\n }\n };\n\n if (deco.line === 'underline') {\n // The line's own baseline when this run was moved off it by\n // vertical-align and the DECLARER stayed behind; `node.lineBaselineY`\n // is set only on a shifted run.\n const baseline =\n node.lineBaselineY !== undefined && !isShiftedVAlign(deco.declarer.verticalAlign)\n ? node.lineBaselineY\n : node.y;\n const explicitDelta = explicitUnderlineDelta(deco, decoWidth);\n if (explicitDelta !== null) {\n paintBand(baseline + explicitDelta);\n } else {\n // Chrome centers the underline ~0.105em below the baseline for every\n // font tested (measured against the DOM raster sweep). The -0.2px is a\n // rounding tiebreak: at fractional baselines (line-height 1.6/1.8/2.0)\n // Chrome resolves the pixel row downward less often than plain\n // rounding; empirically this cuts row-off-by-one cases 39 → 12 across\n // the sweep without disturbing integer baselines.\n paintBand(baseline + deco.declarer.fontSize * 0.105 - 0.2);\n }\n } else if (deco.line === 'line-through') {\n // Chrome positions the strike from the font's OS/2 strikeout metric,\n // which canvas can't read. 0.33em above the baseline is the closest\n // single-formula fit (tuned against the DOM raster sweep; ±1px for\n // most fonts, ±2px worst case).\n paintBand(node.y - style.fontSize * 0.33);\n } else if (deco.line === 'overline') {\n // Chrome hangs the overline band above the ascent line: its bottom\n // edge sits on the floored ascent pixel row, growing upward. The\n // ascent is the crossed run's, not the declarer's.\n const { ascent: decoAscent } = getFontMetrics(ctx, style);\n const overlineY = Math.floor(node.y - decoAscent) - decoWidth / 2;\n paintBand(overlineY);\n }\n }\n }\n\n ctx.restore();\n}\n\n/**\n * Render a layout box and its children to canvas.\n */\nfunction renderBox(\n ctx: CanvasRenderingContext2D,\n box: LayoutBox,\n gradientFill: CanvasGradient | string | null = null,\n strokeGradient: CanvasGradient | null = null,\n): void {\n const { style } = box;\n\n // Background. With background-clip:text the background is NOT painted as a\n // box — it's clipped to descendant glyphs (threaded below as the text fill).\n if (!isTransparent(style.backgroundColor) && style.webkitBackgroundClip !== 'text') {\n ctx.fillStyle = style.backgroundColor;\n ctx.fillRect(box.x, box.y, box.width, box.height);\n }\n\n // Borders\n const borders: [side: 'Top' | 'Right' | 'Bottom' | 'Left', x1: number, y1: number, x2: number, y2: number][] = [\n ['Top', box.x, box.y + style.borderTopWidth / 2, box.x + box.width, box.y + style.borderTopWidth / 2],\n ['Right', box.x + box.width - style.borderRightWidth / 2, box.y, box.x + box.width - style.borderRightWidth / 2, box.y + box.height],\n ['Bottom', box.x, box.y + box.height - style.borderBottomWidth / 2, box.x + box.width, box.y + box.height - style.borderBottomWidth / 2],\n ['Left', box.x + style.borderLeftWidth / 2, box.y, box.x + style.borderLeftWidth / 2, box.y + box.height],\n ];\n for (const [side, x1, y1, x2, y2] of borders) {\n if (!hasBorder(style, side)) continue;\n ctx.strokeStyle = style[`border${side}Color` as keyof ResolvedStyle] as string;\n ctx.lineWidth = style[`border${side}Width` as keyof ResolvedStyle] as number;\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.lineTo(x2, y2);\n ctx.stroke();\n }\n\n // Pre-compute the paint for background-clip: text elements — a gradient\n // (background-image) or a solid color (background-color). It spans the\n // declaring box and threads through descendant boxes (browsers clip the\n // ancestor's background to ALL descendant glyphs, so text inside block\n // children like <p>/<li> keeps it — the background properties themselves\n // don't inherit); a box declaring its own clipping background overrides it.\n // (Inline declarers are resolved in layout via node.clip, not here.)\n if (hasTextClip(style)) {\n const grad = style.backgroundImage && style.backgroundImage !== 'none'\n ? parseLinearGradient(ctx, style.backgroundImage, box.x, box.width, box.y, box.height)\n : null;\n const solid = !isTransparent(style.backgroundColor) ? style.backgroundColor : null;\n // An unparseable image with no solid color keeps the ancestor's paint.\n gradientFill = grad ?? solid ?? gradientFill;\n }\n\n // Pre-compute the stroke gradient the same way: it spans the declaring box\n // and threads through descendants (a box declaring its own overrides it).\n // -webkit-text-stroke-image isn't inherited as a value; the computed gradient\n // is threaded down instead — exactly like the background-clip:text fill.\n if (style.webkitTextStrokeImage && style.webkitTextStrokeImage !== 'none') {\n strokeGradient = parseLinearGradient(ctx, style.webkitTextStrokeImage, box.x, box.width, box.y, box.height);\n }\n\n // Children\n for (const child of box.children) {\n renderNode(ctx, child, gradientFill, strokeGradient);\n }\n}\n\n/**\n * Render any layout node.\n */\nexport function renderNode(\n ctx: CanvasRenderingContext2D,\n node: LayoutNode,\n gradientFill?: CanvasGradient | string | null,\n strokeGradient?: CanvasGradient | null,\n): void {\n if (node.type === 'text') {\n renderText(ctx, node, gradientFill, strokeGradient);\n } else {\n renderBox(ctx, node, gradientFill, strokeGradient);\n }\n}\n","import type {\n RenderConfig, RenderResult,\n LayoutConfig, LayoutResult, DrawConfig,\n LayoutLine, AnyCanvas, AnyContext,\n} from './types.js';\nimport { parseHTML } from './parse.js';\nimport { resolveStylesFromCSS } from './css-resolver.js';\nimport { buildLayoutTree } from './layout.js';\nimport { renderNode } from './render.js';\n\nexport type { RenderConfig, RenderResult, LayoutConfig, LayoutResult, DrawConfig, LayoutLine };\nexport { setDOMParser, type DOMParserLike } from './dom.js';\nexport { lineBaselineOffset } from './layout.js';\nimport { createFallbackMeasureCtx } from './dom.js';\n\n// Default measurement context, created lazily and reused across layout()\n// calls — safe because font/letterSpacing state is set before every\n// measurement anyway. Browser-first source keeps measurement identical to\n// previous releases.\nlet defaultMeasureCtx: CanvasRenderingContext2D | null = null;\n\n// ─── layout() ────────────────────────────────────────────────────────\n\n/**\n * Compute layout for an HTML string without rendering.\n * Returns a reusable LayoutResult that can be drawn onto multiple targets via drawLayout().\n */\nexport function layout(config: LayoutConfig): LayoutResult {\n const {\n html,\n width,\n height,\n accuracy = 'performance',\n debug,\n } = config;\n\n if (!width || width <= 0 || Number.isNaN(width)) {\n throw new TypeError(`layout: width must be a positive number, got ${width}`);\n }\n\n const useDomMeasurements = accuracy === 'balanced';\n\n const { fragment, css } = parseHTML(html);\n const { tree, cleanup } = resolveStylesFromCSS(fragment, css, width);\n\n // Caller-provided ctx is mutated (font, fontKerning) and intentionally NOT\n // save/restored — save/restore is not free on all contexts (e.g. PDF\n // proxies emit stream operators for it).\n const measureCtx =\n (config.ctx as CanvasRenderingContext2D | undefined) ??\n (defaultMeasureCtx ??= createFallbackMeasureCtx(true));\n measureCtx.fontKerning = 'normal';\n\n const { root, height: contentHeight, lines } = buildLayoutTree(measureCtx, tree, width, useDomMeasurements, debug);\n const finalHeight = height || contentHeight;\n\n cleanup();\n\n return { layoutRoot: root, height: finalHeight, lines };\n}\n\n// ─── drawLayout() ────────────────────────────────────────────────────\n\n/**\n * Draw a pre-computed layout onto a canvas or context.\n * Use with layout() to render the same content onto multiple targets.\n */\nexport function drawLayout(config: DrawConfig): { canvas: AnyCanvas } {\n const {\n layout: layoutResult,\n width,\n pixelRatio = globalThis.devicePixelRatio ?? 1,\n } = config;\n\n if (config.ctx && config.canvas) {\n throw new TypeError('drawLayout: ctx and canvas are mutually exclusive — provide one or neither');\n }\n\n const finalHeight = layoutResult.height;\n let canvas: AnyCanvas;\n let renderCtx: AnyContext;\n\n if (config.ctx) {\n renderCtx = config.ctx;\n canvas = config.ctx.canvas;\n } else {\n if (!config.canvas && typeof document === 'undefined') {\n throw new Error(\n 'render-tag: drawLayout cannot create a canvas in a non-browser environment — pass ctx or canvas.'\n );\n }\n canvas = config.canvas ?? document.createElement('canvas');\n canvas.width = Math.ceil(width * pixelRatio);\n canvas.height = Math.ceil(finalHeight * pixelRatio);\n if ('style' in canvas) {\n (canvas as HTMLCanvasElement).style.width = `${width}px`;\n (canvas as HTMLCanvasElement).style.height = `${finalHeight}px`;\n }\n renderCtx = canvas.getContext('2d')! as AnyContext;\n renderCtx.scale(pixelRatio, pixelRatio);\n }\n\n renderNode(renderCtx as CanvasRenderingContext2D, layoutResult.layoutRoot);\n\n return { canvas };\n}\n\n// ─── render() ────────────────────────────────────────────────────────\n\n/**\n * Render an HTML string onto a canvas using pure 2D canvas API.\n * Convenience function combining layout() + drawLayout().\n * Fonts must already be loaded before calling this function.\n */\nexport function render(config: RenderConfig): RenderResult {\n if (config.ctx && config.canvas) {\n throw new TypeError('render: ctx and canvas are mutually exclusive — provide one or neither');\n }\n\n // The output ctx doubles as the measurement ctx (same font resolution for\n // measuring and drawing — required in non-browser environments).\n const layoutResult = layout({\n html: config.html,\n width: config.width,\n height: config.height,\n accuracy: config.accuracy,\n debug: config.debug,\n ctx: config.ctx,\n });\n\n const { canvas } = drawLayout({\n layout: layoutResult,\n width: config.width,\n ctx: config.ctx,\n canvas: config.canvas,\n pixelRatio: config.pixelRatio,\n });\n\n return {\n canvas,\n height: layoutResult.height,\n layoutRoot: layoutResult.layoutRoot,\n lines: layoutResult.lines,\n };\n}\n\n"],"mappings":"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"}