render-tag 0.1.0 → 0.1.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-tag.umd.js","names":[],"sources":["../src/parse.ts","../src/style-resolver.ts","../src/layout.ts","../src/render.ts","../src/index.ts"],"sourcesContent":["/**\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 = new DOMParser();\n const doc = parser.parseFromString(html, 'text/html');\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 // Move body children into a fragment\n const fragment = document.createDocumentFragment();\n while (doc.body.firstChild) {\n fragment.appendChild(document.adoptNode(doc.body.firstChild));\n }\n\n return { fragment, css };\n}\n","import type { ResolvedStyle, StyledNode } from './types.js';\n\nlet containerId = 0;\n\nfunction parsePixels(value: string): number {\n if (!value || value === 'normal' || value === 'auto' || value === 'none') return 0;\n const num = parseFloat(value);\n return isNaN(num) ? 0 : num;\n}\n\nfunction parseFontWeight(value: string): number {\n const num = parseInt(value, 10);\n if (!isNaN(num)) return num;\n if (value === 'bold') return 700;\n if (value === 'normal') return 400;\n return 400;\n}\n\n/**\n * Resolve line-height to a pixel value.\n * \"normal\" is resolved using font metrics via a canvas context.\n */\nfunction resolveLineHeight(cs: CSSStyleDeclaration, fontSize: number): number {\n const raw = cs.lineHeight;\n if (raw && raw !== 'normal') {\n const px = parseFloat(raw);\n if (!isNaN(px)) return px;\n }\n // \"normal\" — approximate as fontSize * 1.2 (will be refined in layout with actual font metrics)\n return 0; // 0 signals \"normal\" — layout will compute from font metrics\n}\n\nfunction extractStyle(cs: CSSStyleDeclaration, el: Element | null = null): ResolvedStyle {\n const fontSize = parsePixels(cs.fontSize);\n return {\n fontFamily: cs.fontFamily,\n fontSize,\n fontWeight: parseFontWeight(cs.fontWeight),\n fontStyle: cs.fontStyle || 'normal',\n color: cs.color,\n textAlign: cs.textAlign || 'left',\n textTransform: cs.textTransform || 'none',\n textDecorationLine: cs.textDecorationLine || 'none',\n textDecorationStyle: cs.textDecorationStyle || 'solid',\n textDecorationColor: cs.textDecorationColor || cs.color,\n textShadow: cs.textShadow || 'none',\n webkitTextStrokeWidth: parsePixels((cs as any).webkitTextStrokeWidth),\n webkitTextStrokeColor: (cs as any).webkitTextStrokeColor || '',\n webkitTextFillColor: (cs as any).webkitTextFillColor || '',\n webkitBackgroundClip: (cs as any).webkitBackgroundClip || cs.backgroundClip || '',\n backgroundImage: cs.backgroundImage || 'none',\n letterSpacing: cs.letterSpacing === 'normal' ? 0 : parsePixels(cs.letterSpacing),\n fontKerning: cs.fontKerning || 'auto',\n lineHeight: resolveLineHeight(cs, fontSize),\n verticalAlign: cs.verticalAlign || 'baseline',\n whiteSpace: cs.whiteSpace || 'normal',\n wordBreak: cs.wordBreak || 'normal',\n overflowWrap: cs.overflowWrap || 'normal',\n direction: cs.direction || 'ltr',\n\n display: cs.display || 'block',\n // Only use width if explicitly set (inline style or stylesheet).\n // getComputedStyle resolves 'auto' to a pixel value for block elements,\n // which we must NOT treat as an explicit width constraint.\n width: el && el instanceof HTMLElement && el.style.width ? parsePixels(cs.width) : 0,\n minHeight: parsePixels(cs.minHeight),\n paddingTop: parsePixels(cs.paddingTop),\n paddingRight: parsePixels(cs.paddingRight),\n paddingBottom: parsePixels(cs.paddingBottom),\n paddingLeft: parsePixels(cs.paddingLeft),\n marginTop: parsePixels(cs.marginTop),\n marginRight: parsePixels(cs.marginRight),\n marginBottom: parsePixels(cs.marginBottom),\n marginLeft: parsePixels(cs.marginLeft),\n backgroundColor: cs.backgroundColor,\n\n borderTopWidth: parsePixels(cs.borderTopWidth),\n borderTopColor: cs.borderTopColor,\n borderTopStyle: cs.borderTopStyle || 'none',\n borderRightWidth: parsePixels(cs.borderRightWidth),\n borderRightColor: cs.borderRightColor,\n borderRightStyle: cs.borderRightStyle || 'none',\n borderBottomWidth: parsePixels(cs.borderBottomWidth),\n borderBottomColor: cs.borderBottomColor,\n borderBottomStyle: cs.borderBottomStyle || 'none',\n borderLeftWidth: parsePixels(cs.borderLeftWidth),\n borderLeftColor: cs.borderLeftColor,\n borderLeftStyle: cs.borderLeftStyle || 'none',\n\n flexDirection: cs.flexDirection || 'row',\n gap: parsePixels(cs.gap),\n flexGrow: parseFloat(cs.flexGrow) || 0,\n flexShrink: parseFloat(cs.flexShrink) || 1,\n\n listStyleType: cs.listStyleType || 'disc',\n listStylePosition: cs.listStylePosition || 'outside',\n };\n}\n\n/**\n * Detect list marker text for a <li> element.\n */\nfunction getListMarker(el: Element, cs: CSSStyleDeclaration): string | undefined {\n const tag = el.tagName.toLowerCase();\n if (tag !== 'li') return undefined;\n\n const beforeStyle = window.getComputedStyle(el, '::before');\n const content = beforeStyle.content;\n\n if (content && content !== 'none' && content !== 'normal') {\n if (!content.includes('counter(')) {\n const cleaned = content.replace(/^[\"']|[\"']$/g, '');\n if (cleaned) return cleaned;\n }\n }\n\n const parent = el.parentElement;\n if (!parent) return '•';\n\n const parentTag = parent.tagName.toLowerCase();\n if (parentTag === 'ol') {\n let index = 0;\n for (const child of parent.children) {\n if (child.tagName.toLowerCase() === 'li') {\n index++;\n if (child === el) break;\n }\n }\n return `${index}.`;\n }\n\n if (parentTag === 'ul') {\n // Use computed list-style-type for correct nesting level markers\n const listStyle = cs.listStyleType;\n if (listStyle === 'circle') return '○';\n if (listStyle === 'square') return '■';\n if (listStyle === 'none') return undefined;\n return '•';\n }\n\n return undefined;\n}\n\n/**\n * Rewrite CSS so that `body` and `html` selectors target our container instead.\n */\nfunction scopeCSS(css: string, containerId: string): string {\n const selector = `#${containerId}`;\n return css.replace(\n /(^|[},;\\s])(\\s*)(html|body)\\b/gm,\n (match, before, space, _tag) => `${before}${space}${selector}`,\n );\n}\n\n/**\n * Insert HTML into a hidden offscreen container, walk the DOM tree,\n * and extract computed styles for every element. No measurements — only CSS values.\n */\nexport function resolveStyles(\n fragment: DocumentFragment,\n css: string,\n width: number,\n height?: number,\n): { tree: StyledNode; cleanup: () => void } {\n const id = `__html_canvas_${containerId++}__`;\n\n const container = document.createElement('div');\n container.id = id;\n container.style.position = 'absolute';\n container.style.left = '-99999px';\n container.style.top = '-99999px';\n container.style.visibility = 'hidden';\n container.style.width = `${width}px`;\n container.style.margin = '0';\n container.style.padding = '0';\n\n if (css) {\n const styleEl = document.createElement('style');\n styleEl.textContent = scopeCSS(css, id);\n container.appendChild(styleEl);\n }\n\n container.appendChild(fragment);\n document.body.appendChild(container);\n\n // Force reflow so getComputedStyle returns resolved values\n container.getBoundingClientRect();\n\n function walkNode(node: Node): StyledNode | null {\n if (node.nodeType === Node.TEXT_NODE) {\n const text = node.textContent;\n if (!text) return null;\n if (text.trim() === '' && !text.includes('\\u00A0')) {\n // Whitespace-only text node. Decide whether to keep or drop.\n const parentEl = node.parentElement;\n const ws = parentEl ? window.getComputedStyle(parentEl).whiteSpace : '';\n\n const prev = node.previousSibling;\n const next = node.nextSibling;\n const isInlineSibling = (n: Node | null) => {\n if (!n || n.nodeType !== Node.ELEMENT_NODE) return n?.nodeType === Node.TEXT_NODE;\n const d = window.getComputedStyle(n as Element).display;\n return d === 'inline' || d === 'inline-block';\n };\n\n // Between block elements: drop in normal mode (HTML formatting).\n // But in pre-wrap, the \\n IS content — creates a visible line break.\n if (prev && next && !isInlineSibling(prev) && !isInlineSibling(next)) {\n if (ws === 'pre' || ws === 'pre-wrap' || ws === 'pre-line') {\n // Keep — pre-wrap makes this whitespace visible\n } else {\n return null;\n }\n }\n\n if (ws !== 'pre' && ws !== 'pre-wrap' && ws !== 'pre-line') {\n // Skip whitespace that contains newlines (HTML source indentation)\n if (text.includes('\\n')) return null;\n }\n }\n\n const parent = node.parentElement;\n if (!parent) return null;\n\n const parentCS = window.getComputedStyle(parent);\n\n return {\n element: null,\n tagName: '#text',\n style: extractStyle(parentCS),\n children: [],\n textContent: text,\n };\n }\n\n if (node.nodeType !== Node.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> → emit as a text node with newline content\n if (tag === 'br') {\n const parent = el.parentElement;\n const cs = parent ? window.getComputedStyle(parent) : window.getComputedStyle(el);\n return {\n element: null,\n tagName: '#text',\n style: extractStyle(cs),\n children: [],\n textContent: '\\n',\n };\n }\n\n const cs = window.getComputedStyle(el);\n const style = extractStyle(cs, el);\n const marker = getListMarker(el, cs);\n\n const children: StyledNode[] = [];\n for (const child of el.childNodes) {\n const childNode = walkNode(child);\n if (childNode) {\n children.push(childNode);\n }\n }\n\n return {\n element: el,\n tagName: tag,\n style,\n children,\n textContent: null,\n listMarker: marker,\n };\n }\n\n const tree = walkNode(container)!;\n\n const cleanup = () => {\n container.remove();\n };\n\n return { tree, cleanup };\n}\n","import type { StyledNode, LayoutNode, LayoutBox, LayoutText, ResolvedStyle } 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// ─── DOM-based line width measurement ──────────────────────────────────\n\n/**\n * Reusable hidden DOM element for measuring mixed-font line widths.\n * Only used when canvas measureText precision is insufficient\n * (mixed fonts near the wrap boundary).\n */\nlet _measureContainer: HTMLDivElement | null = null;\nlet _measureSpanPool: HTMLSpanElement[] = [];\n\nfunction getMeasureContainer(): HTMLDivElement {\n if (_measureContainer && _measureContainer.parentNode) return _measureContainer;\n _measureContainer = document.createElement('div');\n _measureContainer.style.cssText =\n 'position:absolute;top:-9999px;left:-9999px;visibility:hidden;white-space:nowrap;height:auto;width:auto;';\n document.body.appendChild(_measureContainer);\n return _measureContainer;\n}\n\nfunction getMeasureSpan(index: number): HTMLSpanElement {\n if (!_measureSpanPool[index]) {\n _measureSpanPool[index] = document.createElement('span');\n }\n return _measureSpanPool[index];\n}\n\n/**\n * Measure the exact width of a sequence of styled words using the DOM.\n * This is the ground truth — the browser's own text layout engine handles\n * kerning, shaping, and sub-pixel positioning across font boundaries.\n *\n * Only called when canvas measureText suggests a line is near the wrap\n * boundary and fonts are mixed (different weight/style/family on the line).\n */\nlet _domMeasureCount = 0;\nfunction measureLineWidthViaDom(words: Word[]): number {\n _domMeasureCount++;\n const container = getMeasureContainer();\n const textWords = words.filter(w => w.text && w.text !== '\\n');\n if (textWords.length === 0) return 0;\n\n // Build spans — group consecutive words with the same font\n let spanIdx = 0;\n let lastFont = '';\n\n for (const word of textWords) {\n const font = buildCanvasFont(word.style);\n if (font !== lastFont || spanIdx === 0) {\n const span = getMeasureSpan(spanIdx);\n span.style.font = font;\n span.textContent = word.text;\n if (!span.parentNode) container.appendChild(span);\n lastFont = font;\n spanIdx++;\n } else {\n // Same font as previous span — append text\n _measureSpanPool[spanIdx - 1].textContent += word.text;\n }\n }\n\n // Hide unused spans\n for (let i = spanIdx; i < _measureSpanPool.length; i++) {\n if (_measureSpanPool[i].parentNode) {\n _measureSpanPool[i].textContent = '';\n }\n }\n\n return container.getBoundingClientRect().width;\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 */\nfunction applyFont(ctx: CanvasRenderingContext2D, style: ResolvedStyle): void {\n ctx.font = buildCanvasFont(style);\n ctx.fontKerning = style.fontKerning === 'none' ? 'none' : 'normal';\n}\n\n/**\n * Build a canvas font string from resolved style.\n */\nexport function buildCanvasFont(style: ResolvedStyle): string {\n const parts: string[] = [];\n if (style.fontStyle !== 'normal') parts.push(style.fontStyle);\n if (style.fontWeight !== 400) parts.push(String(style.fontWeight));\n parts.push(`${style.fontSize}px`);\n parts.push(style.fontFamily);\n return parts.join(' ');\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 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 metrics\n ctx.font = buildCanvasFont(style);\n const metrics = ctx.measureText('M');\n const ascent = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent;\n const descent = metrics.fontBoundingBoxDescent ?? metrics.actualBoundingBoxDescent;\n return (ascent + descent) * 1.2;\n}\n\n/**\n * Compute the baseline Y offset within a line.\n * Uses the Konva approach: center (ascent - descent) within lineHeight.\n */\nfunction computeBaselineY(ctx: CanvasRenderingContext2D, style: ResolvedStyle, lineHeight: number): number {\n ctx.font = buildCanvasFont(style);\n const metrics = ctx.measureText('M');\n const ascent = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent;\n const descent = metrics.fontBoundingBoxDescent ?? metrics.actualBoundingBoxDescent;\n return (ascent - descent) / 2 + lineHeight / 2;\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 case 'capitalize': return text.replace(/\\b\\w/g, c => 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\nfunction isTransparent(color: string): boolean {\n return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';\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// ─── Inline text run types ─────────────────────────────────────────────\n\ninterface TextRun {\n text: string;\n style: 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}\n\ninterface Word {\n text: string;\n width: number;\n style: 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 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}\n\ninterface PositionedLine {\n words: Word[];\n totalWidth: number;\n lineHeight: number;\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: StyledNode, boxStyle?: ResolvedStyle) {\n if (n.tagName === '#text' && n.textContent) {\n runs.push({ text: n.textContent, style: n.style, boxStyle });\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 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 boxStyle: newBoxStyle,\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 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(child, isBox ? newBoxStyle : boxStyle);\n }\n\n if (hasHorizSpacing) {\n runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxClose: n.style });\n }\n }\n\n for (const child of node.children) {\n walk(child);\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[]): void {\n // Split on zero-width spaces and soft hyphens (break opportunities)\n if (text.includes('\\u200B') || text.includes('\\u00AD')) {\n // Split but keep delimiters to distinguish soft hyphens from zero-width spaces\n const parts = text.split(/(\\u200B|\\u00AD)/);\n let nextIsSoftHyphen = false;\n for (const part of parts) {\n if (part === '\\u00AD') {\n // Mark the PREVIOUS word as a soft-hyphen break point\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);\n // If the previous delimiter was a soft hyphen, mark the word\n // just before this part as having a soft-hyphen break opportunity\n if (nextIsSoftHyphen && prevLen > 0) {\n allWords[prevLen - 1].isSoftHyphenBreak = true;\n }\n nextIsSoftHyphen = false;\n }\n // If the text ends with a soft hyphen, mark the last word\n if (nextIsSoftHyphen && allWords.length > 0) {\n allWords[allWords.length - 1].isSoftHyphenBreak = true;\n }\n return;\n }\n\n const isPreserve = run.style.whiteSpace === 'pre' ||\n run.style.whiteSpace === 'pre-wrap' ||\n run.style.whiteSpace === 'pre-line';\n\n if (isPreserve) {\n // Split on spaces and tabs, keeping delimiters\n const words = text.split(/( +|\\t)/);\n const tabStopInterval = ctx.measureText(' ').width * 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 isSpace: true,\n isTab: true,\n boxStyle: run.boxStyle,\n });\n continue;\n }\n const isSpace = /^ +$/.test(w);\n allWords.push({\n text: w,\n width: ctx.measureText(w).width,\n style: run.style,\n isSpace,\n boxStyle: run.boxStyle,\n });\n }\n } else {\n // Split on whitespace but NOT on non-breaking spaces (\\u00A0)\n const words = text.split(/([ \\t\\n\\r\\f\\v]+)/);\n\n // Use cumulative measurement to avoid rounding error accumulation\n // within a single text run.\n let cumText = '';\n let 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 allWords.push({\n text: ' ',\n width: cumWidth - prevCum,\n style: run.style,\n isSpace: true,\n boxStyle: run.boxStyle,\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 isSpace: false,\n boxStyle: run.boxStyle,\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 = ctx.measureText(w).width;\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 isSpace: false,\n boxStyle: run.boxStyle,\n });\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 = run.style.letterSpacing > 0 ? `${run.style.letterSpacing}px` : '0px';\n const text = applyTextTransform(run.text, run.style.textTransform);\n const s = run.style;\n const textWidth = ctx.measureText(text).width;\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 isSpace: false,\n boxStyle: run.boxStyle,\n boxOpen: run.boxOpen,\n boxClose: run.boxClose,\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 = run.style.letterSpacing > 0 ? `${run.style.letterSpacing}px` : '0px';\n const text = applyTextTransform(run.text, run.style.textTransform);\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 tokenizeString(ctx, parts[i], run, allWords);\n }\n }\n } else {\n tokenizeString(ctx, text, run, allWords);\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\n/**\n * Break a word into character-level pieces if it contains CJK 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 // 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 && !needsBreak) return [word];\n\n // Split into characters\n ctx.font = buildCanvasFont(word.style);\n const chars = [...word.text];\n const pieces: Word[] = [];\n\n let current = '';\n let currentWidth = 0;\n\n for (const char of chars) {\n const charWidth = ctx.measureText(char).width;\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 pieces.push({ ...word, text: char, width: charWidth });\n continue;\n }\n\n // For break-word: break when adding this char would exceed container\n if (needsBreak && currentWidth + charWidth > contentWidth && current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n current = '';\n currentWidth = 0;\n }\n\n current += char;\n currentWidth += charWidth;\n }\n\n if (current) {\n pieces.push({ ...word, text: current, width: currentWidth });\n }\n\n return pieces;\n}\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): PositionedLine[] {\n const lines: PositionedLine[] = [];\n let currentLine: PositionedLine = { words: [], totalWidth: 0, lineHeight: 0 };\n const noWrap = whiteSpace === 'nowrap' || whiteSpace === 'pre';\n\n const isPreWrap = whiteSpace === 'pre-wrap' || whiteSpace === 'pre' || whiteSpace === 'pre-line';\n\n function pushLine(isSoftWrap = false) {\n const hadWords = currentLine.words.length > 0;\n // Trim trailing spaces\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 // 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 = ctx.measureText('-').width;\n currentLine.words.push({\n text: '-',\n width: hyphenWidth,\n style: lastWord.style,\n isSpace: false,\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 = { words: [], totalWidth: 0, lineHeight: 0 };\n }\n\n let afterHardBreak = true; // start of content is like after a hard break\n\n for (const word of words) {\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 const bs = word.boxStyle;\n wordLineHeight = Math.max(wordLineHeight,\n wordLineHeight + bs.paddingTop + bs.paddingBottom + bs.marginTop + bs.marginBottom\n + bs.borderTopWidth + bs.borderBottomWidth);\n }\n\n if (word.text === '\\n') {\n if (currentLine.words.length === 0) {\n currentLine.lineHeight = wordLineHeight;\n lines.push(currentLine);\n currentLine = { words: [], totalWidth: 0, lineHeight: 0 };\n } else {\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 // Break long words / CJK characters if needed\n const pieces = (!word.isSpace && word.text.length > 1)\n ? breakWordIfNeeded(ctx, word, contentWidth, currentLine.totalWidth)\n : [word];\n\n for (const piece of pieces) {\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 /^[,.\\;:!?\\)\\]\\}'\"»›]+$/.test(piece.text) &&\n currentLine.words.length > 0 &&\n !currentLine.words[currentLine.words.length - 1].isSpace;\n\n // Would this piece overflow?\n if (!piece.isSpace && !isTrailingPunct && currentLine.words.length > 0 &&\n currentLine.totalWidth + piece.width > contentWidth) {\n const overflow = currentLine.totalWidth + piece.width - contentWidth;\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 const fullWidth = ctx.measureText(fullText).width;\n // Allow tiny sub-pixel overflow — canvas measureText and DOM\n // text layout can differ by fractions of a pixel.\n if (fullWidth <= contentWidth + 0.1) {\n reallyOverflows = false;\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 after soft wraps, but preserve after hard breaks (\\n)\n if (piece.isSpace && currentLine.words.length === 0 && !afterHardBreak) continue;\n\n // Reverse check: canvas says it fits, but with mixed fonts near boundary,\n // DOM might say it doesn't fit. Verify before committing.\n // Only check when line is >80% full to avoid excessive DOM measurements.\n if (_useDomMeasurements && !piece.isSpace && currentLine.words.length > 0 &&\n currentLine.totalWidth > contentWidth * 0.8) {\n const remaining = contentWidth - (currentLine.totalWidth + piece.width);\n if (remaining >= 0 && remaining < 5 && hasMixedFonts(currentLine.words)) {\n const candidateWords = [...currentLine.words, piece];\n const domWidth = measureLineWidthViaDom(candidateWords);\n if (domWidth > contentWidth) {\n if (_debug) {\n _debug({\n type: 'line-wrap',\n message: `REVERSE WRAP: \"${piece.text}\" canvas says fits (remaining=${remaining.toFixed(2)}) but DOM says overflow (domWidth=${domWidth.toFixed(2)})`,\n data: { text: piece.text, remaining, domWidth, contentWidth },\n });\n }\n pushLine(true);\n afterHardBreak = false;\n }\n }\n }\n\n // Tab: snap to next tab stop based on current position\n let pieceWidth = piece.width;\n if (piece.isTab) {\n const tabStop = piece.width; // tabStopInterval stored as width\n const currentPos = currentLine.totalWidth;\n const nextStop = Math.ceil((currentPos + 0.1) / tabStop) * tabStop;\n pieceWidth = nextStop - currentPos;\n piece.width = pieceWidth;\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\n return lines;\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): { nodes: LayoutNode[]; height: number } {\n const results: LayoutNode[] = [];\n const runs = collectTextRuns(node);\n if (runs.length === 0) return { nodes: results, height: 0 };\n\n const words = tokenizeRuns(ctx, runs);\n const lines = flowWordsIntoLines(ctx, words, contentWidth, node.style.whiteSpace, useBulletProbe);\n const isRTL = node.style.direction === 'rtl';\n let textAlign = node.style.textAlign;\n // In RTL, default alignment is right; 'start'='right', 'end'='left'\n if (textAlign === 'start') textAlign = isRTL ? 'right' : 'left';\n if (textAlign === 'end') textAlign = isRTL ? 'left' : 'right';\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 lineHeight = line.lineHeight;\n const isLastLine = lineIdx === lines.length - 1;\n\n // Justify: expand spaces to fill the line (except last line)\n let justifyExtraPerSpace = 0;\n if (textAlign === 'justify' && !isLastLine && line.totalWidth < contentWidth) {\n const spaceCount = line.words.filter(w => w.isSpace).length;\n if (spaceCount > 0) {\n justifyExtraPerSpace = (contentWidth - line.totalWidth) / spaceCount;\n }\n }\n\n // text-align\n let curX = x;\n if (textAlign === 'center') {\n curX = x + (contentWidth - line.totalWidth) / 2;\n } else if (textAlign === 'right' || (textAlign !== 'justify' && isRTL)) {\n curX = x + contentWidth - line.totalWidth;\n } else if (isRTL) {\n curX = x + contentWidth - line.totalWidth;\n }\n\n // Two-pass: first collect inline background boxes, then text.\n // This ensures backgrounds are rendered before (behind) text.\n\n // Pass 1: find inline background box regions\n {\n let scanX = curX;\n let boxStartX = scanX;\n let currentBoxStyle: ResolvedStyle | undefined;\n let boxHasText = false; // track if region has visible text (not just padding/spaces)\n\n const emitBox = (style: ResolvedStyle, startX: number, endX: number) => {\n // Don't emit background box if this line segment has no visible text\n // (e.g. only a boxOpen padding marker + trailing space before wrap)\n if (!boxHasText) return;\n\n ctx.font = buildCanvasFont(style);\n const metrics = ctx.measureText('Mgy');\n const ascent = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent;\n const descent = metrics.fontBoundingBoxDescent ?? metrics.actualBoundingBoxDescent;\n const emHeight = ascent + descent;\n const boxHeight = emHeight + style.paddingTop + style.paddingBottom\n + style.borderTopWidth + style.borderBottomWidth;\n // For inline-block: position with margin offset. For regular inline: center.\n let boxY: number;\n if (style.display === 'inline-block') {\n boxY = curY + style.marginTop;\n } else {\n boxY = curY + (lineHeight - boxHeight) / 2;\n }\n\n results.push({\n type: 'box',\n style,\n x: startX,\n y: boxY,\n width: endX - startX,\n height: boxHeight,\n tagName: 'span',\n children: [],\n });\n };\n\n for (const word of line.words) {\n // Atomic inline-block: emit box with margin offset\n if (word.boxOpen && word.boxClose && word.text) {\n if (currentBoxStyle) {\n emitBox(currentBoxStyle, boxStartX, scanX);\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 boxHasText = true;\n emitBox(s, boxX, boxX + boxW);\n boxHasText = false;\n scanX += word.width;\n continue;\n }\n\n if (word.boxStyle !== currentBoxStyle) {\n if (currentBoxStyle) {\n emitBox(currentBoxStyle, boxStartX, scanX);\n }\n currentBoxStyle = word.boxStyle;\n boxStartX = scanX;\n boxHasText = false;\n }\n // Track if we've seen actual text content (not spaces or empty padding markers)\n if (word.text && !word.isSpace) {\n boxHasText = true;\n }\n scanX += word.width + (word.isSpace ? justifyExtraPerSpace : 0);\n }\n if (currentBoxStyle) {\n emitBox(currentBoxStyle, boxStartX, scanX);\n }\n }\n\n // Compute a single shared baseline for the entire line.\n // Exclude sub/sup words — they sit above/below the baseline and\n // shouldn't influence where the baseline is positioned.\n let maxAscent = 0;\n let maxDescent = 0;\n for (const word of line.words) {\n if (word.text === '') continue;\n const va = word.style.verticalAlign;\n if (va === 'super' || va === 'sub') continue; // skip sub/sup for baseline calc\n ctx.font = buildCanvasFont(word.style);\n const m = ctx.measureText('M');\n const a = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;\n const d = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;\n if (a > maxAscent) maxAscent = a;\n if (d > maxDescent) maxDescent = d;\n }\n // If only sub/sup words on the line, use the first word's metrics\n if (maxAscent === 0) {\n for (const word of line.words) {\n if (word.text === '') continue;\n ctx.font = buildCanvasFont(word.style);\n const m = ctx.measureText('M');\n maxAscent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;\n maxDescent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;\n break;\n }\n }\n // Center the text block (ascent + descent) within the lineHeight\n const textBlockHeight = maxAscent + maxDescent;\n let lineBaselineY = curY + (lineHeight - textBlockHeight) / 2 + maxAscent;\n\n // Expand line height if sub/sup extends beyond the line box.\n // Browsers grow the line box to fit all content, but keep\n // the normal text baseline position unchanged.\n let effectiveLineHeight = lineHeight;\n {\n const lineNormalWords = line.words.filter(w =>\n w.text !== '' && w.style.verticalAlign !== 'super' && w.style.verticalAlign !== 'sub');\n const parentFontSize = lineNormalWords.length > 0\n ? Math.max(...lineNormalWords.map(w => w.style.fontSize)) : 0;\n\n let minTop = curY;\n let maxBottom = curY + lineHeight;\n\n for (const word of line.words) {\n if (word.text === '') continue;\n const va = word.style.verticalAlign;\n if (va !== 'super' && va !== 'sub') continue;\n if (parentFontSize === 0) break;\n\n ctx.font = buildCanvasFont(word.style);\n const m = ctx.measureText('M');\n const wAscent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;\n const wDescent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;\n\n let shiftedBaseline = lineBaselineY;\n if (va === 'super') {\n shiftedBaseline -= parentFontSize * 0.4;\n } else {\n shiftedBaseline += parentFontSize * 0.26;\n }\n\n const wordTop = shiftedBaseline - wAscent;\n const wordBottom = shiftedBaseline + wDescent;\n if (wordTop < minTop) minTop = wordTop;\n if (wordBottom > maxBottom) maxBottom = wordBottom;\n }\n\n effectiveLineHeight = maxBottom - minTop;\n }\n\n // Pass 2: emit text\n // For RTL lines with uniform style, emit as a single text node\n // so the canvas can handle BiDi glyph shaping and connected letters.\n const textWords = line.words.filter(w => w.text !== '');\n const allSameStyle = textWords.length > 0 && textWords.every(w =>\n w.style.fontFamily === textWords[0].style.fontFamily &&\n w.style.fontSize === textWords[0].style.fontSize &&\n w.style.fontWeight === textWords[0].style.fontWeight &&\n w.style.fontStyle === textWords[0].style.fontStyle &&\n w.style.color === textWords[0].style.color\n );\n\n if (isRTL) {\n // RTL: render words right-to-left\n // Join consecutive words with same style into groups for proper glyph shaping\n let rtlX = curX + line.totalWidth; // start from right edge\n\n interface StyledGroup { text: string; style: ResolvedStyle; width: number; }\n const groups: StyledGroup[] = [];\n let currentGroup: StyledGroup | null = null;\n\n for (const word of line.words) {\n if (word.text === '') {\n // Padding marker — flush current group and add spacing\n if (currentGroup) { groups.push(currentGroup); currentGroup = null; }\n rtlX -= word.width;\n continue;\n }\n if (currentGroup &&\n currentGroup.style.fontFamily === word.style.fontFamily &&\n currentGroup.style.fontSize === word.style.fontSize &&\n currentGroup.style.fontWeight === word.style.fontWeight &&\n currentGroup.style.fontStyle === word.style.fontStyle &&\n currentGroup.style.color === word.style.color) {\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 };\n }\n }\n if (currentGroup) groups.push(currentGroup);\n\n // Emit groups right-to-left\n for (const group of groups) {\n ctx.font = buildCanvasFont(group.style);\n const measuredWidth = ctx.measureText(group.text).width;\n rtlX -= measuredWidth;\n\n results.push({\n type: 'text',\n text: group.text,\n x: rtlX + measuredWidth, // x = right edge for RTL textAlign\n y: lineBaselineY,\n width: measuredWidth,\n style: { ...group.style, direction: 'rtl' },\n });\n }\n } else {\n // LTR: 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 results.push({\n type: 'text',\n text: word.text,\n x: textX,\n y: lineBaselineY,\n width: ctx.measureText(word.text).width,\n style: word.style,\n });\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 (va === 'super' || va === 'sub') {\n // Find parent font size (the normal-sized text on this line)\n const normalWords = textWords.filter(w =>\n w.style.verticalAlign !== 'super' && w.style.verticalAlign !== 'sub');\n const parentFontSize = normalWords.length > 0\n ? Math.max(...normalWords.map(w => w.style.fontSize))\n : word.style.fontSize;\n if (va === 'super') {\n // Chrome raises super baseline by ~0.4em of parent font size\n baselineY -= parentFontSize * 0.4;\n } else {\n // Chrome lowers sub baseline by ~0.26em of parent font size\n baselineY += parentFontSize * 0.26;\n }\n }\n const effectiveWidth = word.width + (word.isSpace ? justifyExtraPerSpace : 0);\n\n results.push({\n type: 'text',\n text: word.text,\n x: curX,\n y: baselineY,\n width: effectiveWidth,\n style: word.style,\n });\n\n curX += effectiveWidth;\n }\n }\n\n curY += effectiveLineHeight;\n }\n\n return { nodes: results, height: curY - y };\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): { box: LayoutBox; height: number; marginBottomOut: number } {\n const style = node.style;\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);\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 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);\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,\n );\n box.children.push(childBox);\n curY += childTotalHeight;\n prevMarginBottom = 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\n const style = node.style;\n ctx.font = buildCanvasFont(style);\n const lineHeight = getLineHeight(ctx, style);\n const baselineY = box.y + style.borderTopWidth + style.paddingTop +\n computeBaselineY(ctx, style, lineHeight);\n\n const markerWidth = ctx.measureText(node.listMarker).width;\n // Content starts at box.x + borderLeft + paddingLeft\n // Place marker right-aligned within the padding area, with a small gap before content\n const contentStartX = box.x + style.borderLeftWidth + style.paddingLeft;\n const gap = style.fontSize * 0.15; // small gap between marker and content\n const markerX = contentStartX - markerWidth - gap;\n\n box.children.unshift({\n type: 'text',\n text: node.listMarker,\n x: markerX,\n y: baselineY,\n width: markerWidth,\n style: { ...style, textDecorationLine: 'none', fontWeight: 400, fontStyle: 'normal' },\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 getDomMeasureCount() { return _domMeasureCount; }\nexport function resetDomMeasureCount() { _domMeasureCount = 0; }\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 } {\n _useDomMeasurements = useDomMeasurements;\n _debug = debug;\n\n // Clear line height cache — fonts may have loaded since last call\n _lineHeightCache.clear();\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 return { root: box, height };\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 { LayoutNode, LayoutBox, LayoutText, ResolvedStyle } from './types.js';\nimport { buildCanvasFont } from './layout.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 */\nfunction 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 background color is transparent.\n */\nfunction isTransparent(color: string): boolean {\n return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';\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 */\nfunction drawDecorationLine(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n width: number,\n lineWidth: number,\n decoStyle: string,\n color: string,\n): void {\n ctx.save();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n\n if (decoStyle === 'dotted') {\n ctx.setLineDash([lineWidth, lineWidth * 2]);\n ctx.beginPath();\n ctx.moveTo(x, y);\n ctx.lineTo(x + width, y);\n ctx.stroke();\n } else if (decoStyle === 'dashed') {\n ctx.setLineDash([lineWidth * 3, lineWidth * 2]);\n ctx.beginPath();\n ctx.moveTo(x, y);\n ctx.lineTo(x + width, y);\n ctx.stroke();\n } else 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 (default)\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 */\nfunction 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/**\n * Render a single text node to canvas.\n * @param gradientFill — pre-computed gradient for background-clip:text spanning full element\n */\nfunction renderText(ctx: CanvasRenderingContext2D, node: LayoutText, gradientFill?: CanvasGradient | null): 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 (style.letterSpacing > 0) {\n ctx.letterSpacing = `${style.letterSpacing}px`;\n }\n if (style.direction === 'rtl') {\n ctx.direction = 'rtl';\n ctx.textAlign = 'right';\n }\n\n const isGradientText = style.webkitBackgroundClip === 'text' &&\n style.backgroundImage && style.backgroundImage !== 'none';\n const isStrokedText = style.webkitTextStrokeWidth > 0;\n const isFillTransparent = style.webkitTextFillColor === 'transparent' ||\n style.color === 'transparent';\n\n // Text shadow (draw before main text)\n const shadows = parseTextShadows(style.textShadow);\n if (shadows.length > 0) {\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 ctx.fillStyle = style.color;\n ctx.fillText(node.text, node.x, node.y);\n ctx.restore();\n }\n }\n\n // Main text fill\n if (isGradientText) {\n ctx.save();\n if (gradientFill) {\n ctx.fillStyle = gradientFill;\n } else {\n // Fallback: per-word gradient (shouldn't normally reach here)\n const metrics = ctx.measureText(node.text);\n const ascent = metrics.fontBoundingBoxAscent ?? metrics.actualBoundingBoxAscent;\n const descent = metrics.fontBoundingBoxDescent ?? metrics.actualBoundingBoxDescent;\n const gradient = parseLinearGradient(\n ctx, style.backgroundImage,\n node.x, node.width,\n node.y - ascent, ascent + descent,\n );\n ctx.fillStyle = gradient || style.color;\n }\n ctx.fillText(node.text, node.x, node.y);\n ctx.restore();\n } else if (!isFillTransparent || !isStrokedText) {\n // Normal text fill (skip if transparent + stroked, stroke handles it)\n ctx.fillStyle = style.webkitTextFillColor && style.webkitTextFillColor !== 'transparent'\n ? style.webkitTextFillColor : style.color;\n\n // letterSpacing is set on ctx above — fillText handles it natively\n ctx.fillText(node.text, node.x, node.y);\n }\n\n // Text stroke (outline text)\n if (isStrokedText) {\n ctx.save();\n ctx.strokeStyle = style.webkitTextStrokeColor || style.color;\n ctx.lineWidth = style.webkitTextStrokeWidth;\n ctx.lineJoin = 'round';\n ctx.strokeText(node.text, node.x, node.y);\n ctx.restore();\n }\n\n // Text decorations — use font metrics for accurate positioning\n const textWidth = node.width;\n const fontSize = style.fontSize;\n const decoColor = style.textDecorationColor || style.color;\n const decoStyle = style.textDecorationStyle || 'solid';\n const decoWidth = Math.max(1, fontSize / 15);\n\n if (style.textDecorationLine !== 'none') {\n ctx.font = buildCanvasFont(style);\n const decoMetrics = ctx.measureText('x');\n const decoAscent = decoMetrics.fontBoundingBoxAscent ?? decoMetrics.actualBoundingBoxAscent;\n const xHeight = decoMetrics.actualBoundingBoxAscent;\n\n if (style.textDecorationLine.includes('underline')) {\n // Underline sits just below the baseline\n const yOffset = fontSize * 0.1;\n drawDecorationLine(ctx, node.x, node.y + yOffset, textWidth, decoWidth, decoStyle, decoColor);\n }\n\n if (style.textDecorationLine.includes('line-through')) {\n // Strikethrough at ~40% of x-height above baseline\n const yOffset = -(xHeight * 0.5);\n drawDecorationLine(ctx, node.x, node.y + yOffset, textWidth, decoWidth, decoStyle, decoColor);\n }\n\n if (style.textDecorationLine.includes('overline')) {\n drawDecorationLine(ctx, node.x, node.y - decoAscent, textWidth, decoWidth, decoStyle, decoColor);\n }\n }\n\n ctx.restore();\n}\n\n/**\n * Render a layout box and its children to canvas.\n */\nfunction renderBox(ctx: CanvasRenderingContext2D, box: LayoutBox): void {\n const { style } = box;\n\n // Background\n if (!isTransparent(style.backgroundColor)) {\n ctx.fillStyle = style.backgroundColor;\n ctx.fillRect(box.x, box.y, box.width, box.height);\n }\n\n // Borders\n if (hasBorder(style, 'Top')) {\n ctx.strokeStyle = style.borderTopColor;\n ctx.lineWidth = style.borderTopWidth;\n const y = box.y + style.borderTopWidth / 2;\n ctx.beginPath();\n ctx.moveTo(box.x, y);\n ctx.lineTo(box.x + box.width, y);\n ctx.stroke();\n }\n if (hasBorder(style, 'Right')) {\n ctx.strokeStyle = style.borderRightColor;\n ctx.lineWidth = style.borderRightWidth;\n const x = box.x + box.width - style.borderRightWidth / 2;\n ctx.beginPath();\n ctx.moveTo(x, box.y);\n ctx.lineTo(x, box.y + box.height);\n ctx.stroke();\n }\n if (hasBorder(style, 'Bottom')) {\n ctx.strokeStyle = style.borderBottomColor;\n ctx.lineWidth = style.borderBottomWidth;\n const y = box.y + box.height - style.borderBottomWidth / 2;\n ctx.beginPath();\n ctx.moveTo(box.x, y);\n ctx.lineTo(box.x + box.width, y);\n ctx.stroke();\n }\n if (hasBorder(style, 'Left')) {\n ctx.strokeStyle = style.borderLeftColor;\n ctx.lineWidth = style.borderLeftWidth;\n const x = box.x + style.borderLeftWidth / 2;\n ctx.beginPath();\n ctx.moveTo(x, box.y);\n ctx.lineTo(x, box.y + box.height);\n ctx.stroke();\n }\n\n // Pre-compute gradient for background-clip: text elements\n let gradientFill: CanvasGradient | null = null;\n if (style.webkitBackgroundClip === 'text' && style.backgroundImage && style.backgroundImage !== 'none') {\n gradientFill = parseLinearGradient(ctx, style.backgroundImage, box.x, box.width, box.y, box.height);\n }\n\n // Children\n for (const child of box.children) {\n renderNode(ctx, child, gradientFill);\n }\n}\n\n/**\n * Render any layout node.\n */\nexport function renderNode(ctx: CanvasRenderingContext2D, node: LayoutNode, gradientFill?: CanvasGradient | null): void {\n if (node.type === 'text') {\n renderText(ctx, node, gradientFill);\n } else {\n renderBox(ctx, node);\n }\n}\n","import type {\n RenderConfig, RenderOptions, RenderResult,\n LayoutConfig, LayoutResult, DrawConfig,\n LayoutLine, LayoutNode, AnyCanvas, AnyContext,\n} from './types.js';\nimport { parseHTML } from './parse.js';\nimport { resolveStyles } from './style-resolver.js';\nimport { buildLayoutTree } from './layout.js';\nimport { renderNode } from './render.js';\n\nexport type { RenderConfig, RenderOptions, RenderResult, LayoutConfig, LayoutResult, DrawConfig, LayoutLine };\n\n// ─── Line extraction ─────────────────────────────────────────────────\n\nfunction extractLines(root: LayoutNode): LayoutLine[] {\n const wordPositions: { y: number; fontSize: number; text: string }[] = [];\n function walk(node: LayoutNode) {\n if (node.type === 'text' && node.text.trim()) {\n wordPositions.push({ y: node.y, fontSize: node.style.fontSize, text: node.text });\n }\n if (node.type === 'box') {\n for (const child of node.children) walk(child);\n }\n }\n walk(root);\n wordPositions.sort((a, b) => a.y - b.y);\n\n const lines: LayoutLine[] = [];\n let lineMaxFontSize = 0;\n for (const wp of wordPositions) {\n const lastLine = lines[lines.length - 1];\n const tolerance = Math.max(lineMaxFontSize, wp.fontSize) * 0.5;\n if (lastLine && Math.abs(wp.y - lastLine.y) < tolerance) {\n lastLine.text += wp.text;\n lineMaxFontSize = Math.max(lineMaxFontSize, wp.fontSize);\n } else {\n lines.push({ y: Math.round(wp.y), text: wp.text });\n lineMaxFontSize = wp.fontSize;\n }\n }\n return lines;\n}\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 = 'balanced',\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 } = resolveStyles(fragment, css, width, height);\n\n const tmpCanvas = document.createElement('canvas');\n const measureCtx = tmpCanvas.getContext('2d')!;\n measureCtx.fontKerning = 'normal';\n\n const { root, height: contentHeight } = buildLayoutTree(measureCtx, tree, width, useDomMeasurements, debug);\n const finalHeight = height || contentHeight;\n const lines = extractLines(root);\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 if (config.canvas) {\n canvas = config.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 } else {\n canvas = document.createElement('canvas');\n canvas.width = Math.ceil(width * pixelRatio);\n canvas.height = Math.ceil(finalHeight * pixelRatio);\n canvas.style.width = `${width}px`;\n canvas.style.height = `${finalHeight}px`;\n renderCtx = canvas.getContext('2d')!;\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 const layoutResult = layout({\n html: config.html,\n width: config.width,\n height: config.height,\n accuracy: config.accuracy,\n debug: config.debug,\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// ─── Deprecated ──────────────────────────────────────────────────────\n\n/**\n * @deprecated Use `render()` instead.\n */\nexport function renderHTML(\n html: string,\n options: RenderOptions,\n): RenderResult {\n return render({\n html: options.css ? `<style>${options.css}</style>${html}` : html,\n width: options.width,\n height: options.height,\n canvas: options.canvas,\n pixelRatio: options.pixelRatio ?? 1,\n accuracy: options.useDomMeasurements === false ? 'performance' : 'balanced',\n debug: options.debug,\n });\n}\n"],"mappings":"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"}
package/lib/types.d.ts CHANGED
@@ -1,28 +1,91 @@
1
- export interface RenderOptions {
2
- /** Target canvas element (created if not provided) */
3
- canvas?: HTMLCanvasElement;
1
+ export type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
2
+ export type AnyContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
3
+ export interface RenderConfig {
4
+ /** HTML string to render (include <style> tags for CSS) */
5
+ html: string;
4
6
  /** Width of the rendering area in CSS pixels */
5
7
  width: number;
6
- /** Height of the rendering area in CSS pixels (auto-sized if not set) */
8
+ /** Height of the rendering area in CSS pixels (auto-sized from content if omitted) */
7
9
  height?: number;
8
- /** Additional CSS to apply */
9
- css?: string;
10
- /** Device pixel ratio (default: 1) */
10
+ /**
11
+ * Existing 2D rendering context to draw onto.
12
+ * When provided, render-tag draws directly onto this context without resizing the canvas.
13
+ * Mutually exclusive with `canvas`.
14
+ */
15
+ ctx?: AnyContext;
16
+ /**
17
+ * Target canvas element (created if not provided).
18
+ * Mutually exclusive with `ctx`.
19
+ */
20
+ canvas?: AnyCanvas;
21
+ /** Device pixel ratio (default: globalThis.devicePixelRatio ?? 1) */
11
22
  pixelRatio?: number;
12
23
  /**
13
- * Use DOM measurements for improved cross-browser consistency (default: true).
14
- * When enabled, uses hidden DOM elements to measure line heights and verify
15
- * text wrapping at font boundaries. When disabled, uses pure canvas API
16
- * measurements only faster and DOM-free, but may have slight differences
17
- * across browsers (e.g. Firefox list item heights).
24
+ * Measurement accuracy mode (default: 'balanced').
25
+ * - 'balanced' uses DOM probes for cross-browser consistent line heights
26
+ * and wrap verification. Best quality.
27
+ * - 'performance' — pure canvas API measurements only. Faster and DOM-free,
28
+ * but may have slight differences across browsers (e.g. Firefox list item heights).
18
29
  */
19
- useDomMeasurements?: boolean;
30
+ accuracy?: 'balanced' | 'performance';
20
31
  /**
21
32
  * Debug callback for layout diagnostics. Receives structured log entries
22
33
  * during text measurement, wrapping decisions, and positioning.
23
34
  */
24
35
  debug?: (entry: DebugEntry) => void;
25
36
  }
37
+ export interface LayoutConfig {
38
+ /** HTML string to render (include <style> tags for CSS) */
39
+ html: string;
40
+ /** Width of the rendering area in CSS pixels */
41
+ width: number;
42
+ /** Height override in CSS pixels (auto-sized from content if omitted) */
43
+ height?: number;
44
+ /**
45
+ * Measurement accuracy mode (default: 'balanced').
46
+ * - 'balanced' — uses DOM probes for cross-browser consistent line heights.
47
+ * - 'performance' — pure canvas API measurements only.
48
+ */
49
+ accuracy?: 'balanced' | 'performance';
50
+ /** Debug callback for layout diagnostics */
51
+ debug?: (entry: DebugEntry) => void;
52
+ }
53
+ export interface LayoutResult {
54
+ /** The layout tree root */
55
+ layoutRoot: LayoutBox;
56
+ /** Content height in CSS pixels */
57
+ height: number;
58
+ /** Text lines grouped by Y coordinate */
59
+ lines: LayoutLine[];
60
+ }
61
+ export interface DrawConfig {
62
+ /** Layout result from layout() */
63
+ layout: LayoutResult;
64
+ /** Width used during layout (must match) */
65
+ width: number;
66
+ /**
67
+ * Existing 2D rendering context to draw onto.
68
+ * No resizing or scaling applied. Mutually exclusive with `canvas`.
69
+ */
70
+ ctx?: AnyContext;
71
+ /**
72
+ * Target canvas element (created if not provided).
73
+ * Mutually exclusive with `ctx`.
74
+ */
75
+ canvas?: AnyCanvas;
76
+ /** Device pixel ratio (default: globalThis.devicePixelRatio ?? 1) */
77
+ pixelRatio?: number;
78
+ }
79
+ /** @deprecated Use RenderConfig instead */
80
+ export interface RenderOptions {
81
+ canvas?: HTMLCanvasElement;
82
+ width: number;
83
+ height?: number;
84
+ css?: string;
85
+ pixelRatio?: number;
86
+ useDomMeasurements?: boolean;
87
+ debug?: (entry: DebugEntry) => void;
88
+ }
26
89
  export interface DebugEntry {
27
90
  type: 'measure-word' | 'line-wrap' | 'line-commit' | 'position-text';
28
91
  /** Human-readable description */
@@ -38,12 +101,13 @@ export interface LayoutLine {
38
101
  text: string;
39
102
  }
40
103
  export interface RenderResult {
41
- canvas: HTMLCanvasElement;
42
- /** Actual content height after layout */
104
+ /** The canvas that was rendered onto */
105
+ canvas: AnyCanvas;
106
+ /** Content height in CSS pixels after layout */
43
107
  height: number;
44
- /** The layout tree root (for debugging/comparison) */
108
+ /** The layout tree root — stable API for inspection and testing */
45
109
  layoutRoot: LayoutBox;
46
- /** Text lines extracted from the layout tree, grouped by Y coordinate */
110
+ /** Text lines grouped by Y coordinate — stable API */
47
111
  lines: LayoutLine[];
48
112
  }
49
113
  /** Resolved style for a single element — all values in px / concrete strings */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "render-tag",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Render HTML rich text onto canvas using pure 2D API",
6
6
  "main": "./lib/index.js",
@@ -9,7 +9,8 @@
9
9
  ".": {
10
10
  "import": "./lib/index.js",
11
11
  "types": "./lib/index.d.ts"
12
- }
12
+ },
13
+ "./umd": "./lib/render-tag.umd.js"
13
14
  },
14
15
  "homepage": "https://polotno.com/render-tag/",
15
16
  "author": "Anton Lavrenov",
@@ -18,12 +19,12 @@
18
19
  "README.md"
19
20
  ],
20
21
  "scripts": {
21
- "dev": "vite",
22
+ "dev": "vite --config docs/vite.config.ts",
22
23
  "test": "vitest run tests/render.test.ts",
24
+ "test:update-baselines": "vitest run tests/generate-baselines.test.ts",
23
25
  "test:stress": "vitest run tests/stress.test.ts",
24
26
  "test:firefox": "vitest run tests/render.test.ts -c vitest.firefox.config.ts",
25
- "build": "tsc",
26
- "docs": "vite --config docs/vite.config.ts",
27
+ "build": "tsc && vite build --config vite.umd.config.ts",
27
28
  "docs:build": "vite build --config docs/vite.config.ts"
28
29
  },
29
30
  "devDependencies": {