import type { Specificity } from "./selector"; import type { CompiledDeclaration, CompiledMediaQuery, CompiledStyleRule, ResolvedNodeStyle, ResolvedPageDimensions, ResolvedPageSize, ResolveStylesheetContext, ResolveStylesheetResult, SemanticCssDiagnostic, SemanticNode, SourceRange, StructuralPresentation, StyleProgram, } from "./types"; import { createDiagnostic } from "./diagnostics"; import { SEMANTIC_CSS_LIMITS_V1 } from "./limits"; import { PROPERTY_REGISTRY_V1 } from "./registry/properties"; import { createSystemVariables } from "./registry/system-variables"; import { createSelectorMatcher } from "./selector"; import { cssFunctionDepth, decodeCssEscapes, expandShorthand, SEMANTIC_CSS_LENGTH_PROPERTIES, valueSyntaxError, } from "./values"; type FlatNode = { node: SemanticNode; parent: FlatNode | null; pageKey: string | null; }; type MatchedRule = { rule: CompiledStyleRule; specificity: Specificity; }; type Winner = { declaration: CompiledDeclaration; specificity: Specificity; }; type LengthContext = { page: ResolvedPageDimensions; parentFontSize: number; fontSize: number; rootFontSize: number; }; type CssWideKeyword = "inherit" | "initial" | "revert" | "unset"; type VariableExpansionBudget = { work: number; }; const absoluteUnitToPt = { pt: 1, px: 72 / 96, in: 72, mm: 72 / 25.4, cm: 72 / 2.54, } as const; const cssWideKeywords = new Set(["inherit", "initial", "revert", "unset"]); const maxVariableExpansionOutputCodeUnits = SEMANTIC_CSS_LIMITS_V1.maxSourceBytes; const maxVariableExpansionWorkCodeUnits = SEMANTIC_CSS_LIMITS_V1.maxSourceBytes * 4; const structuralKeys: Readonly> = { "break-before": "breakBefore", "break-inside": "breakInside", "-resume-fixed": "fixed", "-resume-min-presence-ahead": "minPresenceAhead", orphans: "orphans", widows: "widows", size: "pageSize", }; function cssWideKeyword(value: string): CssWideKeyword | null { const keyword = value.toLowerCase(); return cssWideKeywords.has(keyword) ? (keyword as CssWideKeyword) : null; } function compareSpecificity(left: Specificity, right: Specificity): number { return left[0] - right[0] || left[1] - right[1] || left[2] - right[2]; } function candidateWins(candidate: Winner, current: Winner | undefined): boolean { if (!current) return true; if (candidate.declaration.important !== current.declaration.important) return candidate.declaration.important; return ( compareSpecificity(candidate.specificity, current.specificity) > 0 || (compareSpecificity(candidate.specificity, current.specificity) === 0 && candidate.declaration.sourceOrder > current.declaration.sourceOrder) ); } function flattenTree(root: SemanticNode): FlatNode[] | null { const result: FlatNode[] = []; const stack: { node: SemanticNode; parent: FlatNode | null; pageKey: string | null }[] = [ { node: root, parent: null, pageKey: null }, ]; while (stack.length > 0) { const next = stack.pop(); if (!next) break; if (result.length >= SEMANTIC_CSS_LIMITS_V1.maxSemanticNodes) return null; const pageKey = next.node.kind === "page" ? next.node.key : next.pageKey; const flat = { node: next.node, parent: next.parent, pageKey }; result.push(flat); const childCount = next.node.children.length; if (childCount > SEMANTIC_CSS_LIMITS_V1.maxSemanticNodes - result.length - stack.length) return null; for (let index = childCount - 1; index >= 0; index--) { const child = next.node.children[index]; if (child) stack.push({ node: child, parent: flat, pageKey }); } } return result; } function matchingSpecificity( rule: CompiledStyleRule, nodeKey: string, matches: ReturnType, ): Specificity | null { let best: Specificity | null = null; for (const selector of rule.selector.selectors) { if (!matches({ selectors: [selector] }, nodeKey)) continue; if (!best || compareSpecificity(selector.specificity, best) > 0) best = selector.specificity; } return best; } function defaultPageDimensions( format: ResolveStylesheetContext["baseSettings"]["page"]["format"], ): ResolvedPageDimensions { if (format === "letter") return { width: 612, height: 792 }; return { width: 595.28, height: 841.89 }; } function initialPages( flatNodes: readonly FlatNode[], context: ResolveStylesheetContext, ): Map { const authored = new Map(context.pages.map((page) => [page.pageKey, { width: page.width, height: page.height }])); const fallback = defaultPageDimensions(context.baseSettings.page.format); const pages = new Map(); for (const { node } of flatNodes) { if (node.kind === "page") pages.set(node.key, authored.get(node.key) ?? fallback); } return pages; } function pageFor( node: FlatNode, pages: ReadonlyMap, fallback: ResolvedPageDimensions, ): ResolvedPageDimensions { return (node.pageKey ? pages.get(node.pageKey) : undefined) ?? pages.values().next().value ?? fallback; } function toPoints(value: string, property: string, context: LengthContext): number | string | null { const match = value .trim() .match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*(pt|px|in|mm|cm|%|vw|vh|em|rem)?$/i); if (!match) return null; const number = Number(match[1]); if (!Number.isFinite(number)) return null; const unit = match[2]?.toLowerCase() ?? "pt"; if (unit === "%") return property === "font-size" ? (number / 100) * context.parentFontSize : `${number}%`; if (unit === "vw") return (number / 100) * context.page.width; if (unit === "vh") return (number / 100) * context.page.height; if (unit === "rem") return number * context.rootFontSize; if (unit === "em") { return number * (property === "font-size" ? context.parentFontSize : context.fontSize); } return number * absoluteUnitToPt[unit as keyof typeof absoluteUnitToPt]; } function mediaMatches(query: CompiledMediaQuery, dimensions: ResolvedPageDimensions, rootFontSize: number): boolean { const lengthContext = { page: dimensions, parentFontSize: rootFontSize, fontSize: rootFontSize, rootFontSize, }; return query.features.every((feature) => { if (feature.name === "orientation") { return feature.value === (dimensions.width > dimensions.height ? "landscape" : "portrait"); } const expected = toPoints(feature.value, feature.name, lengthContext); if (typeof expected !== "number") return false; const actual = dimensions[feature.name]; if (feature.comparison === "min") return actual >= expected; if (feature.comparison === "max") return actual <= expected; return actual === expected; }); } function ruleApplies( rule: CompiledStyleRule, dimensions: ResolvedPageDimensions, rootFontSize: number, includeMedia: boolean, ): boolean { if (rule.media.length === 0) return true; return includeMedia && rule.media.some((query) => mediaMatches(query, dimensions, rootFontSize)); } function winnersFor( matches: readonly MatchedRule[], dimensions: ResolvedPageDimensions, rootFontSize: number, includeMedia: boolean, ): Map { const winners = new Map(); for (const { rule, specificity } of matches) { if (!ruleApplies(rule, dimensions, rootFontSize, includeMedia)) continue; for (const declaration of rule.declarations) { const candidate = { declaration, specificity }; if (candidateWins(candidate, winners.get(declaration.property))) { winners.set(declaration.property, candidate); } } } return winners; } function splitVariable(value: string): [name: string, fallback: string | null] { let depth = 0; for (let index = 0; index < value.length; index++) { if (value[index] === "(") depth++; if (value[index] === ")") depth--; if (value[index] === "," && depth === 0) { return [value.slice(0, index).trim(), value.slice(index + 1).trim()]; } } return [value.trim(), null]; } function matchingParenthesis(value: string, opening: number): number { let depth = 0; for (let index = opening; index < value.length; index++) { if (value[index] === "(") depth++; if (value[index] === ")" && --depth === 0) return index; } return -1; } function expandVariables( source: string, custom: ReadonlyMap, system: Readonly>, diagnostics: SemanticCssDiagnostic[], range: SourceRange, stack: readonly string[] = [], depth = 0, budget: VariableExpansionBudget = { work: 0 }, ): string | null { if (depth > SEMANTIC_CSS_LIMITS_V1.maxVariableExpansionDepth) { diagnostics.push( createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range), ); return null; } if ( source.length > maxVariableExpansionOutputCodeUnits || source.length > maxVariableExpansionWorkCodeUnits - budget.work ) { diagnostics.push( createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range), ); return null; } budget.work += source.length; let value = decodeCssEscapes(source); if (value.length > maxVariableExpansionOutputCodeUnits) { diagnostics.push( createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range), ); return null; } while (true) { const match = /var\s*\(/i.exec(value); if (!match) break; const opening = (match.index ?? 0) + match[0].lastIndexOf("("); const closing = matchingParenthesis(value, opening); if (closing < 0) { diagnostics.push(createDiagnostic("INVALID_VALUE", "error", "Malformed var() expression.", range)); return null; } const [name, fallback] = splitVariable(value.slice(opening + 1, closing)); if (!name.startsWith("--")) { diagnostics.push(createDiagnostic("INVALID_VALUE", "error", "var() requires a custom-property name.", range)); return null; } if (stack.includes(name)) { diagnostics.push(createDiagnostic("VARIABLE_CYCLE", "error", `Variable cycle detected at ${name}.`, range)); return null; } const replacement = custom.get(name) ?? system[name] ?? fallback; if (replacement === null || replacement === undefined) { diagnostics.push( createDiagnostic("UNRESOLVED_VARIABLE", "error", `No value or fallback exists for ${name}.`, range), ); return null; } const expanded = expandVariables( replacement, custom, system, diagnostics, range, [...stack, name], depth + 1, budget, ); if (expanded === null) return null; const nextLength = (match.index ?? 0) + expanded.length + value.length - closing - 1; if ( nextLength > maxVariableExpansionOutputCodeUnits || nextLength > maxVariableExpansionWorkCodeUnits - budget.work ) { diagnostics.push( createDiagnostic("RESOURCE_LIMIT", "error", "Variable expansion exceeds the Semantic CSS limit.", range), ); return null; } budget.work += nextLength; value = `${value.slice(0, match.index)}${expanded}${value.slice(closing + 1)}`; } if (/\burl\s*\(/i.test(value)) { diagnostics.push( createDiagnostic("FORBIDDEN_CSS_VALUE", "error", "External CSS resources are not supported.", range), ); return null; } if (cssFunctionDepth(value) > SEMANTIC_CSS_LIMITS_V1.maxFunctionDepth) { diagnostics.push( createDiagnostic("RESOURCE_LIMIT", "error", "CSS function nesting exceeds the Semantic CSS limit.", range), ); return null; } return value.trim(); } function expandedWinnersFor( matches: readonly MatchedRule[], dimensions: ResolvedPageDimensions, rootFontSize: number, custom: ReadonlyMap, system: Readonly>, diagnostics: SemanticCssDiagnostic[], ): Map { const cascaded = new Map(); for (const { rule, specificity } of matches) { if (!ruleApplies(rule, dimensions, rootFontSize, true)) continue; for (const declaration of rule.declarations) { if (declaration.property.startsWith("--")) continue; const targets = expandShorthand(declaration.property, "inherit") ?? [[declaration.property, "inherit"]]; for (const [property] of targets) { const candidate = { declaration, specificity }; if (candidateWins(candidate, cascaded.get(property))) cascaded.set(property, candidate); } } } const expansions = new Map(); const winners = new Map(); for (const [property, winner] of cascaded) { if (property === "size") { winners.set(property, winner); continue; } let declarations = expansions.get(winner.declaration); if (declarations === undefined) { const expandedValue = expandVariables( winner.declaration.value, custom, system, diagnostics, winner.declaration.range, ); declarations = expandedValue === null ? null : expandShorthand(winner.declaration.property, expandedValue); if (expandedValue !== null && !declarations) { diagnostics.push( createDiagnostic( "INVALID_VALUE", "error", `Invalid ${winner.declaration.property} shorthand.`, winner.declaration.range, ), ); } expansions.set(winner.declaration, declarations); } const value = declarations?.find(([expandedProperty]) => expandedProperty === property)?.[1]; if (value === undefined) continue; winners.set(property, { declaration: { ...winner.declaration, property, value }, specificity: winner.specificity, }); } return winners; } function customProperties( winners: ReadonlyMap, parent: ReadonlyMap | undefined, ): Map { let custom: Map | undefined; for (const [property, { declaration }] of winners) { if (!property.startsWith("--")) continue; const keyword = cssWideKeyword(declaration.value); if (keyword === "inherit" || keyword === "revert" || keyword === "unset") continue; if (keyword === "initial") { if (parent?.has(property)) { custom ??= new Map(parent); custom.delete(property); } } else if (parent?.get(property) !== declaration.value) { custom ??= new Map(parent); custom.set(property, declaration.value); } } return custom ?? (parent as Map | undefined) ?? new Map(); } function parsePageSize(value: string, lengthContext: LengthContext): ResolvedPageSize | null { if (value.toLowerCase() === "a4") return "A4"; if (value.toLowerCase() === "letter") return "LETTER"; const parts = value.trim().split(/\s+/); if (parts.length < 1 || parts.length > 2) return null; const width = toPoints(parts[0] ?? "", "size", lengthContext); const height = parts[1] ? toPoints(parts[1], "size", lengthContext) : undefined; if (typeof width !== "number" || (height !== undefined && typeof height !== "number")) return null; if ( width <= 0 || Math.abs(width) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt || (height !== undefined && (height <= 0 || Math.abs(height) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt)) ) { return null; } return height === undefined ? { width } : { width, height }; } function dimensionsForSize(size: ResolvedPageSize, authored: ResolvedPageDimensions): ResolvedPageDimensions { if (size === "A4") return { width: 595.28, height: 841.89 }; if (size === "LETTER") return { width: 612, height: 792 }; return { width: size.width, height: size.height ?? authored.height }; } function builderPageSize(context: ResolveStylesheetContext, nodeKey: string): ResolvedPageSize { return ( context.baseStyles[nodeKey]?.structural.pageSize ?? (context.baseSettings.page.format === "letter" ? "LETTER" : "A4") ); } function cssWideValue( keyword: CssWideKeyword, property: string, base: Readonly>, parent: Readonly> | undefined, ): string | number | undefined { const definition = PROPERTY_REGISTRY_V1[property]; if (keyword === "inherit") return parent?.[property]; if (keyword === "initial") return; if (keyword === "unset") return definition?.inheritable ? parent?.[property] : undefined; return base[property] ?? (definition?.inheritable ? parent?.[property] : undefined); } function normalizeValue(property: string, value: string, context: LengthContext): string | number | null { if (SEMANTIC_CSS_LENGTH_PROPERTIES.has(property)) { const length = toPoints(value, property, context); if (length !== null) return length; if (/^(auto|none|normal|max-content|min-content|fit-content|thin|medium|thick)$/i.test(value)) { return value.toLowerCase(); } return null; } if (property === "line-height") { if (value.toLowerCase() === "normal") return "normal"; return toPoints(value, property, context); } if (/^(opacity|flex-grow|flex-shrink|order|orphans|widows|z-index|max-lines)$/i.test(property)) { const number = Number(value); if (!Number.isFinite(number)) return null; if (property === "opacity" && (number < 0 || number > 1)) return null; return number; } return value; } function applyStructuralCssWide( keyword: CssWideKeyword, property: string, structural: StructuralPresentation, base: ResolvedNodeStyle, parent: ResolvedNodeStyle | undefined, revertPageSize: ResolvedPageSize, ): boolean { const key = structuralKeys[property]; if (!key) return false; const value = keyword === "inherit" ? parent?.structural[key] : keyword === "revert" ? (base.structural[key] ?? (key === "pageSize" ? revertPageSize : undefined)) : undefined; if (value === undefined) delete structural[key]; else Object.assign(structural, { [key]: value }); return true; } function structuralValue( property: string, value: string | number, structural: StructuralPresentation, pageSize?: ResolvedPageSize, ): boolean { if (property === "break-before") { if (value === "page") structural.breakBefore = "page"; else if (value === "auto") delete structural.breakBefore; else return false; } if (property === "break-inside") { if (value === "avoid") structural.breakInside = "avoid"; else if (value === "auto") delete structural.breakInside; else return false; } if (property === "-resume-fixed") { if (value === "true" || value === "1" || value === 1) structural.fixed = true; else if (value === "false" || value === "0" || value === 0) structural.fixed = false; else return false; } if (property === "-resume-min-presence-ahead") { if (typeof value === "number" && value >= 0) structural.minPresenceAhead = value; else return false; } if (property === "orphans" || property === "widows") { if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return false; if (property === "orphans") structural.orphans = value; else structural.widows = value; } if (property === "size") { if (!pageSize) return false; structural.pageSize = pageSize; } return true; } function createRenderTree( tree: SemanticNode, flatNodes: readonly FlatNode[], nodes: Readonly>, ): SemanticNode { const rendered = new Map(); for (let index = flatNodes.length - 1; index >= 0; index--) { const source = flatNodes[index]?.node; if (!source) continue; const children = source.children .map((child, sourceIndex) => ({ child: rendered.get(child.key), sourceIndex })) .filter((entry): entry is { child: SemanticNode; sourceIndex: number } => entry.child !== undefined) .filter(({ child }) => !nodes[child.key]?.hidden) .sort((left, right) => { const order = (nodes[left.child.key]?.order ?? 0) - (nodes[right.child.key]?.order ?? 0); return order || left.sourceIndex - right.sourceIndex; }) .map(({ child }) => child); rendered.set(source.key, { ...source, attributes: { ...source.attributes }, roles: [...source.roles], children }); } return rendered.get(tree.key) ?? tree; } export function resolveStylesheet( program: StyleProgram, tree: SemanticNode, context: ResolveStylesheetContext, ): ResolveStylesheetResult { const flatNodes = flattenTree(tree); if (!flatNodes) { return { nodes: {}, renderTree: tree, diagnostics: [ createDiagnostic("RESOURCE_LIMIT", "error", "The semantic tree exceeds the Semantic CSS node limit."), ], }; } if ( context.pages.some( ({ width, height }) => !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt || height > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt, ) ) { return { nodes: {}, renderTree: tree, diagnostics: [ createDiagnostic("INVALID_VALUE", "error", "Authored page dimensions must be finite and positive."), ], }; } const diagnostics: SemanticCssDiagnostic[] = []; const matched = new Map(); const selectorMatches = createSelectorMatcher(tree); for (const { node } of flatNodes) { const rules: MatchedRule[] = []; for (const rule of program.rules) { const specificity = matchingSpecificity(rule, node.key, selectorMatches); if (specificity) rules.push({ rule, specificity }); } matched.set(node.key, rules); } const nodeKinds = new Map(flatNodes.map(({ node }) => [node.key, node.kind])); const cascadeKinds = new Map>(); for (const [canonicalKey, aliasKeys] of Object.entries(context.aliases ?? {})) { const canonicalKind = nodeKinds.get(canonicalKey); if (!canonicalKind) continue; const kinds = new Set([canonicalKind]); const rules = [...(matched.get(canonicalKey) ?? [])]; for (const aliasKey of aliasKeys) { const aliasKind = nodeKinds.get(aliasKey); if (!aliasKind) continue; kinds.add(aliasKind); rules.push(...(matched.get(aliasKey) ?? [])); } matched.set(canonicalKey, rules); cascadeKinds.set(canonicalKey, kinds); } for (const rule of program.rules) { const matchingNodes = flatNodes.filter(({ node }) => matched.get(node.key)?.some((matchedRule) => matchedRule.rule === rule), ); if (matchingNodes.length === 0) { diagnostics.push( createDiagnostic("SELECTOR_NO_MATCH", "warning", "This selector matches no semantic resume node.", rule.range), ); continue; } for (const declaration of rule.declarations) { if (declaration.property.startsWith("--")) continue; const definition = PROPERTY_REGISTRY_V1[declaration.property]; if (definition && !matchingNodes.some(({ node }) => definition.appliesTo.includes(node.kind))) { diagnostics.push( createDiagnostic( "PROPERTY_NOT_APPLICABLE", "warning", `${declaration.property} cannot apply to the matched semantic node kinds.`, declaration.range, ), ); } } } const rootFontSize = context.baseSettings.typography.body.fontSize; const fallbackDimensions = defaultPageDimensions(context.baseSettings.page.format); const pages = initialPages(flatNodes, context); const resolvedPageSizes = new Map(); const resolvedPageSizeValues = new Map(); const preliminaryCustom = new Map>(); for (const node of flatNodes) { const dimensions = pageFor(node, pages, fallbackDimensions); const rawWinners = winnersFor(matched.get(node.node.key) ?? [], dimensions, rootFontSize, false); const custom = customProperties(rawWinners, node.parent ? preliminaryCustom.get(node.parent.node.key) : undefined); preliminaryCustom.set(node.node.key, custom); if (node.node.kind !== "page") continue; const size = rawWinners.get("size"); if (!size) continue; const system = createSystemVariables(context.baseSettings, dimensions); const expanded = expandVariables(size.declaration.value, custom, system, diagnostics, size.declaration.range); if (!expanded) continue; const keyword = cssWideKeyword(expanded); const parsed = keyword === "revert" ? builderPageSize(context, node.node.key) : keyword === "inherit" ? node.parent ? context.baseStyles[node.parent.node.key]?.structural.pageSize : undefined : keyword ? undefined : parsePageSize(expanded, { page: dimensions, parentFontSize: rootFontSize, fontSize: rootFontSize, rootFontSize, }); if (keyword && !parsed) { resolvedPageSizeValues.set(node.node.key, expanded); continue; } if (!parsed) { diagnostics.push(createDiagnostic("INVALID_VALUE", "error", "Invalid page size.", size.declaration.range)); continue; } resolvedPageSizeValues.set(node.node.key, expanded); resolvedPageSizes.set(node.node.key, parsed); pages.set(node.node.key, dimensionsForSize(parsed, dimensions)); } const customByNode = new Map>(); const resolved: Record = {}; for (const node of flatNodes) { const dimensions = pageFor(node, pages, fallbackDimensions); const rawWinners = winnersFor(matched.get(node.node.key) ?? [], dimensions, rootFontSize, true); const custom = customProperties(rawWinners, node.parent ? customByNode.get(node.parent.node.key) : undefined); customByNode.set(node.node.key, custom); const system = createSystemVariables(context.baseSettings, dimensions); const winners = expandedWinnersFor( matched.get(node.node.key) ?? [], dimensions, rootFontSize, custom, system, diagnostics, ); const base = context.baseStyles[node.node.key] ?? { style: {}, structural: {}, hidden: false, order: 0 }; const parent = node.parent ? resolved[node.parent.node.key] : undefined; const style: Record = { ...base.style }; const specifiedStyleProperties = new Set(); const hostBaseStyleProperties = new Set(); for (const [property, definition] of Object.entries(PROPERTY_REGISTRY_V1)) { if (!definition?.inheritable) continue; const inheritedFromAuthoredRule = parent?.specifiedStyleProperties?.includes(property) && !winners.has(property); if (inheritedFromAuthoredRule) { specifiedStyleProperties.add(property); if (parent?.hostBaseStyleProperties?.includes(property)) hostBaseStyleProperties.add(property); if (parent?.style[property] === undefined) delete style[property]; else style[property] = parent.style[property]; } else if (style[property] === undefined && parent?.style[property] !== undefined) { style[property] = parent.style[property]; } } const fontSizeWinner = winners.get("font-size"); const parentFontSize = typeof parent?.style["font-size"] === "number" ? parent.style["font-size"] : context.baseSettings.typography.body.fontSize; const applicableKinds = cascadeKinds.get(node.node.key) ?? new Set([node.node.kind]); if (fontSizeWinner && PROPERTY_REGISTRY_V1["font-size"]?.appliesTo.some((kind) => applicableKinds.has(kind))) { const expanded = fontSizeWinner.declaration.value; const keyword = cssWideKeyword(expanded); if (keyword) { specifiedStyleProperties.add("font-size"); if (keyword === "revert") hostBaseStyleProperties.add("font-size"); const wide = cssWideValue(keyword, "font-size", base.style, parent?.style); if (wide === undefined) delete style["font-size"]; else style["font-size"] = wide; } else { const syntaxError = valueSyntaxError("font-size", expanded); if (syntaxError) { diagnostics.push(createDiagnostic("INVALID_VALUE", "error", syntaxError, fontSizeWinner.declaration.range)); } else { const normalized = normalizeValue("font-size", expanded, { page: dimensions, parentFontSize, fontSize: parentFontSize, rootFontSize, }); if (normalized === null) { diagnostics.push( createDiagnostic("INVALID_VALUE", "error", "Invalid font-size value.", fontSizeWinner.declaration.range), ); } else if ( typeof normalized === "number" && (normalized < 0 || normalized > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt) ) { diagnostics.push( createDiagnostic( "INVALID_VALUE", "error", "font-size exceeds the Semantic CSS length limit.", fontSizeWinner.declaration.range, ), ); } else { specifiedStyleProperties.add("font-size"); style["font-size"] = normalized; if (typeof normalized === "number" && (normalized < 4 || normalized > 72)) { diagnostics.push( createDiagnostic( "EXTREME_VALUE", "warning", "This font size is renderable but unusually extreme.", fontSizeWinner.declaration.range, ), ); } } } } } const fontSize = typeof style["font-size"] === "number" ? style["font-size"] : parentFontSize; const structural: StructuralPresentation = { ...base.structural }; let hidden = base.hidden; let order = base.order; for (const [property, winner] of winners) { if (property.startsWith("--") || property === "font-size") continue; const definition = PROPERTY_REGISTRY_V1[property]; if (!definition?.appliesTo.some((kind) => applicableKinds.has(kind))) continue; const expanded = winner.declaration.value; if (property === "size") { const resolvedValue = resolvedPageSizeValues.get(node.node.key); if (resolvedValue === undefined) continue; const sizeKeyword = cssWideKeyword(resolvedValue); if (sizeKeyword) { applyStructuralCssWide( sizeKeyword, property, structural, base, parent, builderPageSize(context, node.node.key), ); } else { const pageSize = resolvedPageSizes.get(node.node.key); if (pageSize) structuralValue(property, resolvedValue, structural, pageSize); } continue; } const keyword = cssWideKeyword(expanded); if (keyword) { if (property === "display") { if (keyword === "inherit") { hidden = parent?.hidden ?? false; if (!hidden && parent?.style.display !== undefined) style.display = parent.style.display; else delete style.display; } else if (keyword === "revert") { hidden = base.hidden; if (base.style.display === undefined) delete style.display; else style.display = base.style.display; } else { hidden = false; delete style.display; } } else if (property === "order") { order = keyword === "inherit" ? (parent?.order ?? 0) : keyword === "revert" ? base.order : 0; } else if (definition.category === "structural") { applyStructuralCssWide(keyword, property, structural, base, parent, builderPageSize(context, node.node.key)); } else { specifiedStyleProperties.add(property); if (keyword === "revert") hostBaseStyleProperties.add(property); const wide = cssWideValue(keyword, property, base.style, parent?.style); if (wide === undefined) delete style[property]; else style[property] = wide; } continue; } const syntaxError = valueSyntaxError(property, expanded); if (syntaxError) { diagnostics.push(createDiagnostic("INVALID_VALUE", "error", syntaxError, winner.declaration.range)); continue; } const normalized = normalizeValue(property, expanded, { page: dimensions, parentFontSize, fontSize, rootFontSize, }); if (normalized === null) { diagnostics.push( createDiagnostic("INVALID_VALUE", "error", `Invalid ${property} value.`, winner.declaration.range), ); continue; } if (typeof normalized === "number" && Math.abs(normalized) > SEMANTIC_CSS_LIMITS_V1.maxAbsoluteLengthPt) { diagnostics.push( createDiagnostic( "INVALID_VALUE", "error", `${property} exceeds the Semantic CSS length limit.`, winner.declaration.range, ), ); continue; } if (property === "display") { hidden = normalized === "none"; if (hidden) delete style.display; else style.display = normalized; continue; } if (property === "order") { const nextOrder = typeof normalized === "number" ? normalized : Number(normalized); if (!Number.isInteger(nextOrder)) { diagnostics.push( createDiagnostic("INVALID_VALUE", "error", "order must be an integer.", winner.declaration.range), ); } else order = nextOrder; continue; } if (definition.category === "structural") { if (!structuralValue(property, normalized, structural)) { diagnostics.push( createDiagnostic("INVALID_VALUE", "error", `Invalid ${property} value.`, winner.declaration.range), ); } continue; } specifiedStyleProperties.add(property); style[property] = normalized; } resolved[node.node.key] = { style, specifiedStyleProperties: [...specifiedStyleProperties], hostBaseStyleProperties: [...hostBaseStyleProperties], structural, hidden, order, }; } if (diagnostics.some(({ severity }) => severity === "error")) { return { nodes: {}, renderTree: tree, diagnostics }; } return { nodes: resolved, renderTree: createRenderTree(tree, flatNodes, resolved), diagnostics }; }