nuclo 0.2.37 → 0.3.0
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.
- package/dist/nuclo.cjs +1 -1
- package/dist/nuclo.cjs.map +1 -1
- package/dist/nuclo.mjs +1 -1
- package/dist/nuclo.mjs.map +1 -1
- package/dist/ssr/nuclo.ssr.cjs.map +1 -1
- package/dist/ssr/nuclo.ssr.mjs.map +1 -1
- package/package.json +1 -1
- package/types/features/render.d.ts +26 -2
- package/types/index.d.ts +7 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nuclo.ssr.cjs","names":["NucloElement"],"sources":["../../src/shared/strings.ts","../../src/hydration.ts","../../src/ssr/render-to-string.ts","../../src/style/engine.ts"],"sourcesContent":["/**\n * camelCase → kebab-case for CSS property (and attribute) names. A leading\n * capital becomes a leading dash, as vendor prefixes need\n * (WebkitTransition → -webkit-transition). Char-code loop with a\n * no-uppercase fast path: SSR runs this per style declaration.\n */\nexport function camelToKebab(str: string): string {\n let firstUpper = 0;\n for (; firstUpper < str.length; firstUpper++) {\n const code = str.charCodeAt(firstUpper);\n if (code >= 65 && code <= 90) break;\n }\n if (firstUpper === str.length) return str;\n\n let result = str.slice(0, firstUpper);\n for (let i = firstUpper; i < str.length; i++) {\n const code = str.charCodeAt(i);\n // A-Z is 65-90, a-z is 97-122 (difference of 32)\n result += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];\n }\n return result;\n}\n\nconst ATTR_ESCAPE_RE = /[&<>\"']/;\nconst TEXT_ESCAPE_RE = /[&<>]/;\n\n/**\n * Escapes HTML special characters in attribute values (includes \" and ')\n */\nexport function escapeHtml(text: string): string {\n // Fast path: most strings contain no special characters at all.\n const first = ATTR_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n case 34: escaped = '"'; break; // \"\n case 39: escaped = '''; break; // '\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n\n/**\n * Escapes HTML special characters in text node content.\n * Only &, < and > need escaping — quotes are safe inside text nodes.\n */\nexport function escapeText(text: string): string {\n const first = TEXT_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n","/**\n * Hydration context — tracks global hydration state and per-parent cursor positions.\n *\n * During hydration the component tree is built against an existing DOM tree\n * (produced by SSR). Instead of creating new nodes, factories claim existing\n * children from their parent element by advancing a cursor.\n *\n * The cursor is a *node pointer* (the next unclaimed child), not an index.\n * Indexed childNodes access is O(n) after any tree/attribute mutation in\n * jsdom (live NodeList re-materialization) which made hydration O(n²);\n * firstChild/nextSibling pointers are O(1) in every DOM implementation.\n */\n\nlet _hydrating = false;\n// True while renderToString() is building a tree destined for serialization.\nlet _serializing = false;\n// Missing entry = cursor at parent.firstChild; null = past the last child.\nlet _cursors = new WeakMap<Node, Node | null>();\n\nexport function isHydrating(): boolean {\n return _hydrating;\n}\n\n/**\n * True while a tree is being built for SSR serialization (inside\n * renderToString). Text children must keep their `<!-- text-N -->` markers in\n * this mode so the emitted HTML stays hydratable — even when `isBrowser` is\n * true (SSR running under jsdom, or an isomorphic worker that provides a DOM).\n * A pure client render leaves this false and skips the markers.\n */\nexport function isSerializing(): boolean {\n return _serializing;\n}\n\n/**\n * Runs `fn` with serialization mode enabled, restoring the previous state\n * afterwards (so nested/re-entrant renderToString calls behave correctly).\n */\nexport function runSerializing<T>(fn: () => T): T {\n const previous = _serializing;\n _serializing = true;\n try {\n return fn();\n } finally {\n _serializing = previous;\n }\n}\n\nexport function startHydration(): void {\n _hydrating = true;\n // Each hydration pass starts with a clean cursor slate. Stale cursors from\n // a previous pass would desynchronize claims when a container's content is\n // replaced and re-hydrated (HMR, islands re-mounting).\n _cursors = new WeakMap<Node, Node | null>();\n}\n\nexport function endHydration(): void {\n _hydrating = false;\n}\n\n/**\n * Runs a callback with hydration temporarily disabled, restoring the previous\n * state afterwards. Used when a server/client mismatch forces a subtree to be\n * rendered fresh in the middle of a hydration pass — fresh rendering must not\n * claim nodes from the surrounding SSR DOM.\n */\nexport function runWithoutHydration<T>(fn: () => T): T {\n const wasHydrating = _hydrating;\n _hydrating = false;\n try {\n return fn();\n } finally {\n _hydrating = wasHydrating;\n }\n}\n\n/**\n * Returns the next unclaimed child of a parent node (or null at the end)\n * without advancing the cursor.\n */\nexport function peekChild(parent: Node): Node | null {\n const entry = _cursors.get(parent);\n return entry === undefined ? parent.firstChild : entry;\n}\n\n/**\n * Sets the cursor to a specific child node (or null for \"past the end\").\n */\nexport function setCursor(parent: Node, node: Node | null): void {\n _cursors.set(parent, node);\n}\n\nfunction isWhitespaceText(node: Node): boolean {\n return node.nodeType === 3 && !/\\S/.test(node.textContent || '');\n}\n\n/**\n * Advances the cursor past whitespace-only text nodes.\n *\n * Nuclo's own SSR output never produces bare whitespace text nodes (text\n * children are always preceded by a `<!-- text-N -->` marker), so unmarked\n * whitespace comes from the surrounding template or an HTML\n * formatter/minifier. Skipping (without removing) keeps the cursor aligned\n * while leaving the document's visual whitespace untouched.\n */\nexport function skipWhitespaceText(parent: Node): void {\n let child = peekChild(parent);\n let advanced = false;\n while (child && isWhitespaceText(child)) {\n child = child.nextSibling;\n advanced = true;\n }\n if (advanced) {\n setCursor(parent, child);\n }\n}\n\n/**\n * Claims the next child node from parent at the current cursor position.\n * Advances the cursor by one.\n */\nexport function claimChild(parent: Node): Node | null {\n const child = peekChild(parent);\n if (child) {\n setCursor(parent, child.nextSibling);\n }\n return child;\n}\n\n/**\n * Claims a server-rendered `<!--{prefix}-start-…-->` … `<!--{prefix}-end-->`\n * block (list()/when()) at the cursor, leaving the cursor just after the start\n * marker. Returns null, claiming nothing, when the next child isn't a start\n * marker. Pairs are depth-counted because directly nested blocks share a\n * host. A missing end marker (corrupt/truncated SSR output) is recreated right\n * after the start marker and reported as `recreated`.\n */\nexport function claimMarkerPair(\n parent: Node,\n prefix: \"list\" | \"when\",\n): { start: Comment; end: Comment; recreated: boolean } | null {\n skipWhitespaceText(parent);\n const start = peekChild(parent);\n const startPrefix = prefix + \"-start-\";\n if (!start || start.nodeType !== 8 || !start.textContent?.startsWith(startPrefix)) return null;\n claimChild(parent);\n const endText = prefix + \"-end\";\n let depth = 0;\n for (let node = start.nextSibling; node; node = node.nextSibling) {\n if (node.nodeType !== 8) continue;\n const text = node.textContent;\n if (text === endText) {\n if (depth === 0) return { start: start as Comment, end: node as Comment, recreated: false };\n depth--;\n } else if (text?.startsWith(startPrefix)) {\n depth++;\n }\n }\n const end = document.createComment(endText);\n parent.insertBefore(end, start.nextSibling);\n return { start: start as Comment, end, recreated: true };\n}\n\n/**\n * Attempts to claim an existing element from the parent during hydration.\n * Returns the claimed element if the next child matches the expected tag, or null.\n */\nexport function claimElement(parent: Node, tagName: string): Element | null {\n if (!_hydrating) return null;\n skipWhitespaceText(parent);\n const candidate = peekChild(parent);\n if (candidate && candidate.nodeType === 1 && (candidate as Element).tagName.toLowerCase() === tagName) {\n return claimChild(parent) as Element;\n }\n return null;\n}\n\n/**\n * Removes unclaimed SSR children from a hydrated element.\n * Must be called after modifiers have been applied to a claimed element.\n * Removes nodes from the cursor through `lastOriginalChild` (the element's\n * last child before modifiers ran) — nodes appended by modifiers come after\n * that boundary and are preserved.\n */\nexport function cleanupUnclaimedChildren(node: Node, lastOriginalChild: Node | null): void {\n if (!lastOriginalChild) return;\n let current = peekChild(node);\n while (current) {\n const next = current === lastOriginalChild ? null : current.nextSibling;\n node.removeChild(current);\n current = next;\n }\n setCursor(node, null);\n}\n","/**\n * Server-Side Rendering (SSR) utilities for Nuclo\n * Renders Nuclo components to HTML strings in Node.js environment\n */\n\nimport { escapeHtml, escapeText, camelToKebab } from '../shared/strings';\nimport { runSerializing } from '../hydration';\nimport { NucloElement } from '../polyfill/Element';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\n\n/**\n * SVG attributes that are natively camelCase and must NOT be converted to kebab-case.\n * Most SVG attributes are already kebab-case (stroke-width, fill-rule, etc.), but a\n * handful are defined as camelCase in the SVG spec and must be preserved.\n */\nconst SVG_PRESERVE_CASE_ATTRS = new Set([\n 'viewBox', 'preserveAspectRatio', 'markerWidth', 'markerHeight',\n 'gradientTransform', 'patternTransform', 'clipPathUnits', 'gradientUnits',\n 'patternUnits', 'pathLength', 'refX', 'refY', 'stdDeviation',\n 'baseFrequency', 'numOctaves', 'kernelMatrix', 'tableValues',\n 'targetX', 'targetY', 'specularExponent', 'specularConstant',\n 'diffuseConstant', 'surfaceScale', 'xChannelSelector', 'yChannelSelector',\n 'edgeMode', 'stitchTiles', 'spreadMethod', 'patternContentUnits',\n 'markerUnits', 'startOffset', 'textLength', 'lengthAdjust',\n]);\n\n/**\n * HTML boolean attributes — presence means true, absence means false.\n * When the stored value is the string \"true\" or \"false\" (from setAttribute),\n * we must re-apply boolean semantics instead of outputting the raw string.\n */\nconst HTML_BOOLEAN_ATTRIBUTES = new Set([\n 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',\n 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'ismap', 'loop',\n 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'readonly', 'required',\n 'reversed', 'selected',\n]);\n\n/**\n * Serializes a DOM attribute value\n */\nfunction serializeAttribute(name: string, value: unknown): string {\n if (value === null || value === undefined || value === false) {\n return '';\n }\n\n if (value === true) {\n return ` ${name}`;\n }\n\n // Boolean attributes stored as strings via setAttribute() need special handling\n if (HTML_BOOLEAN_ATTRIBUTES.has(name)) {\n if (value === 'false') return '';\n if (value === 'true' || value === '' || value === name) return ` ${name}`;\n }\n\n // A style object (e.g. renderToStringWithContainer's containerAttrs from\n // untyped JS): serialize its declarations, skipping nullish/empty values.\n if (name === 'style' && typeof value === 'object') {\n let styleStr = '';\n for (const key in value as Record<string, unknown>) {\n const val = (value as Record<string, unknown>)[key];\n if (val == null || val === '') continue;\n styleStr += (styleStr ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n return styleStr ? ` style=\"${escapeHtml(styleStr)}\"` : '';\n }\n\n return ` ${name}=\"${escapeHtml(String(value))}\"`;\n}\n\n/**\n * Serializes DOM element attributes to HTML string\n *\n * `isPolyfill` is passed in by the caller (serializeNode already computed it\n * to decide whether tagName needs lowercasing) so this doesn't repeat the\n * same `Array.isArray` discriminator check per element.\n */\nfunction serializeAttributes(element: Element, isPolyfill: boolean): string {\n let result = '';\n\n // Handle polyfill elements. NucloElement keeps every child in a plain Array\n // (`children`), which a real DOM element never does (HTMLCollection) — this is\n // the same allocation-free discriminator serializeNode() uses. Reading\n // `el.attributes` directly would lazily allocate an empty Map for every\n // element being serialized, so the backing `_attributes` field is read instead.\n if (isPolyfill) {\n const el = element as any;\n const attrs = el._attributes as Map<string, string> | undefined;\n\n // id — may live on the property rather than in the Map\n if (el.id && !attrs?.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // class — kept on .className, mirrored to Map only when setAttribute is used\n if (el.className && !attrs?.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // style — lives on the backing _style object (its own properties are the\n // declarations), not in the attributes Map. Read the field directly (never\n // `el.style`) so elements that never set a style are not forced to lazily\n // allocate an empty SSRStyle just to serialize. Empty values (e.g. reactive\n // styles that resolved to undefined) are skipped.\n const styleObj = el._style as Record<string, unknown> | undefined;\n if (styleObj && !attrs?.has('style')) {\n let css = '';\n for (const key in styleObj) {\n const val = styleObj[key];\n if (val == null || val === '') continue;\n css += (css ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n if (css) result += ` style=\"${escapeHtml(css)}\"`;\n }\n\n // All remaining attributes from the Map\n // Convert camelCase ARIA/HTML attribute names (e.g. ariaLabel → aria-label) that the\n // polyfill stores as-is because NucloElement lacks the browser property mappings.\n // SVG attributes that are natively camelCase (e.g. viewBox, preserveAspectRatio) must\n // NOT be converted — they are stored via setAttribute() as-is.\n if (attrs) {\n for (const [name, value] of attrs) {\n const htmlName = SVG_PRESERVE_CASE_ATTRS.has(name) ? name : camelToKebab(name);\n result += serializeAttribute(htmlName, value);\n }\n }\n return result;\n }\n\n // Handle browser elements with NamedNodeMap attributes\n if (element.attributes && element.attributes.length) {\n for (let i = 0; i < element.attributes.length; i++) {\n const attr = element.attributes[i];\n if (attr && attr.name) {\n result += serializeAttribute(attr.name, attr.value);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Tag-category lookup, keyed by lowercase tag name. Every element in a tree\n * hits this once, so void + raw-text elements are folded into a single\n * dictionary lookup instead of two separate `Set.has()` probes (one of which\n * would always miss for the other category).\n */\nconst TAG_VOID = 1;\nconst TAG_RAW_TEXT = 2;\nconst TAG_CATEGORY: Record<string, 1 | 2> = Object.assign(Object.create(null), {\n // Self-closing HTML tags that don't have closing tags\n area: TAG_VOID, base: TAG_VOID, br: TAG_VOID, col: TAG_VOID, embed: TAG_VOID,\n hr: TAG_VOID, img: TAG_VOID, input: TAG_VOID, link: TAG_VOID, meta: TAG_VOID,\n param: TAG_VOID, source: TAG_VOID, track: TAG_VOID, wbr: TAG_VOID,\n // Raw-text elements — browsers never decode entities inside them, so their\n // text content must be emitted verbatim (escaping would corrupt inline\n // JS/CSS, e.g. `a < b` becoming `a < b` inside a script). A closing-tag\n // sequence in the content would terminate the element early, so it is\n // neutralized (see escapeRawText).\n script: TAG_RAW_TEXT, style: TAG_RAW_TEXT,\n});\n\nfunction escapeRawText(tagName: string, text: string): string {\n // \"</script\" (any case) inside a script would close it — break the sequence\n // the same way JSON serializers do (\"<\\/script\").\n return text.replace(new RegExp('</(' + tagName + ')', 'gi'), '<\\\\/$1');\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node — only & < > need escaping; quotes are safe in text content\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeText(node.textContent || '');\n }\n\n // Comment node\n if (node.nodeType === 8) { // Node.COMMENT_NODE\n return `<!--${node.textContent || ''}-->`;\n }\n\n // Element node\n if (node.nodeType === 1) { // Node.ELEMENT_NODE\n const element = node as Element;\n const rawTagName = element.tagName;\n // Duck-typed polyfill discriminator (also used by serializeAttributes): a\n // plain Array `children` field never occurs on a real\n // DOM element (HTMLCollection), only on NucloElement — and on hand-rolled\n // polyfill-shaped test doubles, which is why this alone isn't enough to\n // skip re-lowercasing tagName below.\n const isPolyfillShape = Array.isArray((element as any).children);\n // Only a *real* NucloElement is guaranteed to have pre-lowercased tagName\n // (done once in its constructor) — re-lowercasing it here on every\n // element would be pure waste, since this is the hottest function in\n // renderToString. Anything else (browser Element, or a polyfill-shaped\n // object that didn't go through `new NucloElement()`) still needs it.\n const tagName = element instanceof NucloElement ? rawTagName : rawTagName.toLowerCase();\n const attributes = serializeAttributes(element, isPolyfillShape);\n const category = TAG_CATEGORY[tagName];\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as any).children : element.childNodes;\n\n // Raw-text elements: emit text verbatim (no entity escaping, no Nuclo\n // text markers — `<!--` would act as a line comment inside a script).\n if (category === TAG_RAW_TEXT) {\n let rawContent = '';\n const rawChildren = childNodes;\n if (rawChildren && rawChildren.length > 0) {\n for (let i = 0; i < rawChildren.length; i++) {\n const child = rawChildren[i];\n if (child && child.nodeType === 3) {\n rawContent += child.textContent || '';\n }\n }\n } else {\n const tc = (node as { textContent?: unknown }).textContent;\n if (typeof tc === 'string') rawContent = tc;\n }\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n if (childNodes && childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n childrenHtml += serializeNode(child);\n }\n }\n } else {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as any).textContent;\n if (typeof tc === 'string' && tc) {\n childrenHtml = escapeText(tc);\n }\n }\n\n return `<${tagName}${attributes}>${childrenHtml}</${tagName}>`;\n }\n\n // Document fragment\n if (node.nodeType === 11) { // Node.DOCUMENT_FRAGMENT_NODE\n let result = '';\n const childNodes: ArrayLike<Node> = (node as any).childNodes ?? [];\n if (childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n result += serializeNode(child);\n }\n }\n }\n return result;\n }\n\n return '';\n}\n\n/**\n * Renders a Nuclo component to an HTML string for server-side rendering\n *\n * @param input - A Nuclo component function, DOM element, or node\n * @returns HTML string representation of the component\n *\n * @example\n * ```ts\n * import { renderToString } from 'nuclo/ssr';\n * import { div } from 'nuclo';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div>Hello, World!</div>'\n * ```\n */\nexport function renderToString(input: RenderableInput): string {\n if (!input) return '';\n\n if (typeof input === 'function') {\n try {\n // Build the tree in serialization mode so text children keep their\n // <!-- text-N --> markers (needed for hydration) even when isBrowser is\n // true, e.g. SSR running under jsdom.\n const element = runSerializing(() => {\n if (typeof document === 'undefined') throw new Error('Document is not available. Make sure polyfills are loaded.');\n return input(document.createElement('div') as unknown as ExpandedElement<ElementTagName>, 0);\n });\n return element && typeof element === 'object' && 'nodeType' in element ? serializeNode(element as Node) : '';\n } catch (error) {\n console.error('Error rendering component to string:', error);\n return '';\n }\n }\n\n if ('nodeType' in input) {\n return serializeNode(input as Node);\n }\n\n return '';\n}\n\n/**\n * Renders multiple Nuclo components to HTML strings\n *\n * @param inputs - Array of Nuclo components\n * @returns Array of HTML strings\n */\nexport function renderManyToString(inputs: RenderableInput[]): string[] {\n return inputs.map(input => renderToString(input));\n}\n\n/**\n * Renders a Nuclo component and wraps it in a container element\n *\n * @param input - A Nuclo component\n * @param containerTag - The tag name for the container (default: 'div')\n * @param containerAttrs - Attributes for the container element\n * @returns HTML string with container wrapper\n */\nexport function renderToStringWithContainer(\n input: RenderableInput,\n containerTag: string = 'div',\n containerAttrs: Record<string, string> = {}\n): string {\n const content = renderToString(input);\n const attrs = Object.entries(containerAttrs)\n .map(([key, value]) => serializeAttribute(key, value))\n .join('');\n\n return `<${containerTag}${attrs}>${content}</${containerTag}>`;\n}\n","/**\n * Shared generated-CSS engine state.\n *\n * One stylesheet, one rule registry, shared by every `createCss` instance so\n * identical declarations dedupe globally and cascade order stays deterministic:\n * base rules first (insertion order), then one grouping rule per at-rule query\n * in registration order (theme `screens` order).\n *\n * Browser: rules are injected through CSSOM with O(1) appends — no rescans.\n * SSR: rules accumulate in the registry; `getCssText()` serializes them.\n *\n * The state below lives on `globalThis`, keyed by a `Symbol.for(...)` (the\n * global symbol registry, shared by every realm-local copy of this module).\n * `nuclo` publishes `.`, `./ssr`, and `./polyfill` as independently built\n * bundle files (see tsdown.config.ts) — a consumer that resolves them\n * through normal package resolution (any bundler/runtime that doesn't\n * alias every subpath to this same source file) loads two or three\n * separate copies of this module, each with its own module-scope\n * variables. Without a realm-wide singleton, `css()` (from `nuclo`) would\n * mint rules into one copy's registry while `getCssText()` (from\n * `nuclo/ssr`) reads an entirely different, always-empty one — SSR output\n * would render the right classes with no matching CSS at all. Storing the registry on `globalThis` instead makes\n * every copy of this module share the same state regardless of how many\n * times it ends up bundled.\n *\n * The registry never shrinks on its own: atomCache/atomMeta/rawKeys/\n * baseRules/queryRules retain every rule for the life of the process —\n * content-addressed dedup needs a durable record of what each class means,\n * and the engine has no way to know whether a minted class still backs a\n * live DOM node (it's deliberately decoupled from element/list lifecycle),\n * so it can't safely evict. Only resetStyles() (a manual test helper)\n * clears it. This is fine for the intended use — a bounded design-token\n * vocabulary — but a caller piping truly per-instance dynamic values\n * through css({ raw: {...} }) (e.g. per-row data in a list) mints one\n * permanent class per unique value; prefer the `style` prop\n * (element/inline-style.ts) for values that vary per instance rather than\n * per design token. See test/style/registry-growth.test.ts.\n */\n\nexport interface BlockMeta {\n\treadonly query: string | undefined;\n\treadonly suffix: string;\n\treadonly decls: ReadonlyArray<readonly [string, string]>;\n\t/** Author-supplied identity from `css(name, …)`; inherited by cx() merges. */\n\treadonly name: string | undefined;\n}\n\ninterface EngineState {\n\t// Registry — the source of truth for all generated CSS.\n\t// atomCache: declaration-block key -> class name (dedup)\n\t// atomMeta: class name -> what the class means (its selector context and its\n\t// declarations). This is what lets cx() merge two same-context classes at the\n\t// declaration level instead of guessing: the engine can always recover the\n\t// exact content of any class it minted.\n\tatomCache: Map<string, string>;\n\tatomMeta: Map<string, BlockMeta>;\n\trawKeys: Set<string>;\n\tbaseRules: string[];\n\tqueryRules: Map<string, string[]>;\n\tstyleEpoch: number;\n\n\t// Memoized getCssText() output. SSR calls getCssText() once per request, but the\n\t// rule set is stable after warmup — so the serialized sheet is recomputed only\n\t// when a new rule is actually recorded. Invalidated in record() and resetStyles().\n\tcssTextCache: string | null;\n\n\t// Browser CSSOM state. Base rules occupy [0, baseRuleCount); grouping rules\n\t// (one per query, created in registration order) live after them. CSSRule\n\t// references stay live when indices shift, so insertion never rescans.\n\tsheet: CSSStyleSheet | null;\n\tsheetDocument: Document | null;\n\tbaseRuleCount: number;\n\tgroupRules: Map<string, CSSGroupingRule>;\n\n\t// Normalized rules already sitting in a freshly-adopted stylesheet that this\n\t// module didn't insert itself — e.g. server-rendered CSS being hydrated.\n\t// Rebuilt on every fresh bind in ensureSheet(); null when there is nothing\n\t// external to guard against.\n\texternalRules: Map<string, Set<string>> | null;\n\thasExternalGroups: boolean;\n}\n\nfunction createState(): EngineState {\n\treturn {\n\t\tatomCache: new Map(),\n\t\tatomMeta: new Map(),\n\t\trawKeys: new Set(),\n\t\tbaseRules: [],\n\t\tqueryRules: new Map(),\n\t\tstyleEpoch: 0,\n\t\tcssTextCache: null,\n\t\tsheet: null,\n\t\tsheetDocument: null,\n\t\tbaseRuleCount: 0,\n\t\tgroupRules: new Map(),\n\t\texternalRules: null,\n\t\thasExternalGroups: false,\n\t};\n}\n\nconst ENGINE_STATE_KEY = Symbol.for(\"nuclo.style.engine.v1\");\nconst globalScope = globalThis as unknown as Record<symbol, EngineState>;\n// Importing the runtime does not allocate the registry until styling is used.\nlet cachedState: EngineState | undefined;\nfunction getState(): EngineState {\n\treturn cachedState ??= globalScope[ENGINE_STATE_KEY] ?? (globalScope[ENGINE_STATE_KEY] = createState());\n}\n\n/** Pre-register at-rule queries so their cascade order follows theme order. */\nexport function registerQueries(queries: Iterable<string>): void {\n\tfor (const query of queries) {\n\t\tconst state = getState();\n\t\tif (!state.queryRules.has(query)) state.queryRules.set(query, []);\n\t}\n}\n\nfunction createGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\ttry {\n\t\tconst idx = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", idx);\n\t\tconst group = s.cssRules[idx] as CSSGroupingRule;\n\t\tstate.groupRules.set(query, group);\n\t\tstate.groupRules.set(groupQueryOf(group), group);\n\t\treturn group;\n\t} catch {\n\t\treturn null; // unsupported at-rule in this environment\n\t}\n}\n\n// A group prelude is stable while its children grow. Reading cssText on each\n// insertion would serialize all previous children and make loading quadratic.\n// Weak keys allow detached stylesheets to be collected after reset/rebinding.\nconst groupQueries = new WeakMap<CSSRule, string>();\nfunction groupQueryOf(rule: CSSRule): string {\n\tconst cached = groupQueries.get(rule);\n\tif (cached !== undefined) return cached;\n\tconst text = rule.cssText;\n\tconst brace = text.indexOf(\"{\");\n\tconst query = (brace === -1 ? text : text.slice(0, brace)).trim();\n\tgroupQueries.set(rule, query);\n\treturn query;\n}\n\nfunction ruleIdentity(rule: CSSRule): string {\n\treturn rule instanceof CSSStyleRule ? rule.selectorText : groupQueryOf(rule);\n}\n\nfunction rememberExternalRule(rule: CSSRule, query: string | undefined): void {\n\tconst state = getState();\n\tif (!state.externalRules) return;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tlet texts = state.externalRules.get(key);\n\tif (!texts) {\n\t\ttexts = new Set();\n\t\tstate.externalRules.set(key, texts);\n\t}\n\ttexts.add(rule.cssText);\n}\n\nfunction consumeExternalRule(rule: CSSRule, query: string | undefined): boolean {\n\tconst state = getState();\n\tif (!state.externalRules) return false;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tconst texts = state.externalRules.get(key);\n\tif (!texts?.delete(rule.cssText)) return false;\n\tif (texts.size === 0) state.externalRules.delete(key);\n\tif (state.externalRules.size === 0) state.externalRules = null;\n\treturn true;\n}\n\n/**\n * Index an already-populated stylesheet found on adopt (SSR output, most\n * commonly): every top-level conditional grouping rule (@media/@container/…)\n * is registered into groupRules (by its reconstructed at-rule text) so later\n * record() calls append into it instead of creating a sibling duplicate. The\n * other rules are indexed by scope, selector/prelude, and normalized cssText\n * so exact server/client matches can be skipped without hiding a changed\n * named or global rule that happens to reuse the same selector.\n */\nfunction indexExternalRules(rules: CSSRuleList): void {\n\tconst state = getState();\n\tfor (let i = 0; i < rules.length; i++) {\n\t\tconst rule = rules[i];\n\t\tif (rule instanceof CSSStyleRule) {\n\t\t\trememberExternalRule(rule, undefined);\n\t\t\tcontinue;\n\t\t}\n\t\tconst prelude = groupQueryOf(rule);\n\t\tif (\"cssRules\" in rule && !prelude.startsWith(\"@keyframes\")) {\n\t\t\tconst group = rule as CSSGroupingRule;\n\t\t\tstate.hasExternalGroups = true;\n\t\t\tstate.groupRules.set(prelude, group);\n\t\t\tfor (let j = 0; j < group.cssRules.length; j++) {\n\t\t\t\trememberExternalRule(group.cssRules[j], prelude);\n\t\t\t}\n\t\t} else {\n\t\t\t// @keyframes, @font-face, … — one-shot rules matched by prelude.\n\t\t\trememberExternalRule(rule, undefined);\n\t\t}\n\t}\n}\n\nfunction insertBase(s: CSSStyleSheet, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\ts.insertRule(rule, state.baseRuleCount);\n\t\tconst inserted = s.cssRules[state.baseRuleCount];\n\t\tif (consumeExternalRule(inserted, undefined)) {\n\t\t\ts.deleteRule(state.baseRuleCount);\n\t\t\treturn;\n\t\t}\n\t\tstate.baseRuleCount++;\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction insertGrouped(group: CSSGroupingRule, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\tconst index = group.cssRules.length;\n\t\tgroup.insertRule(rule, index);\n\t\tconst inserted = group.cssRules[index];\n\t\tif (state.externalRules && consumeExternalRule(inserted, groupQueryOf(group))) group.deleteRule(index);\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction getOrCreateGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\tconst direct = state.groupRules.get(query);\n\tif (direct) return direct;\n\tif (!state.hasExternalGroups) return createGroup(s, query);\n\n\t// CSSOM may normalize whitespace in an at-rule prelude. Normalize the\n\t// authored query through a temporary empty group before deciding that an\n\t// adopted SSR group is missing.\n\ttry {\n\t\tconst index = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", index);\n\t\tconst normalized = groupQueryOf(s.cssRules[index]);\n\t\ts.deleteRule(index);\n\t\tconst existing = state.groupRules.get(normalized);\n\t\tif (existing) {\n\t\t\tstate.groupRules.set(query, existing);\n\t\t\treturn existing;\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n\treturn createGroup(s, query);\n}\n\n/**\n * Returns the live stylesheet, (re)binding when the document changes — e.g.\n * fresh jsdom documents between tests. Rebinding replays the whole registry,\n * which replaces v1's per-call classExistsInDOM() DOM scans with a single\n * replay per document.\n */\nexport function ensureSheet(): CSSStyleSheet | null {\n\tconst state = getState();\n\tif (typeof document === \"undefined\") return null;\n\t// The nuclo SSR polyfill provides a document without getElementById — in\n\t// that context rules accumulate in the registry for getCssText() only.\n\tif (typeof document.getElementById !== \"function\") return null;\n\tif (\n\t\tstate.sheet &&\n\t\tstate.sheetDocument === document &&\n\t\tstate.sheet.ownerNode &&\n\t\t(state.sheet.ownerNode as Element).isConnected\n\t) {\n\t\treturn state.sheet;\n\t}\n\tconst existing = document.getElementById(\"nuclo-styles\");\n\tlet el: HTMLStyleElement;\n\tif (existing?.tagName === \"STYLE\") {\n\t\tel = existing as HTMLStyleElement;\n\t} else {\n\t\tel = document.createElement(\"style\");\n\t\tel.id = \"nuclo-styles\";\n\t\tif (existing) existing.replaceWith(el);\n\t\telse document.head.appendChild(el);\n\t}\n\tstate.sheet = el.sheet;\n\tstate.sheetDocument = document;\n\tstate.groupRules.clear();\n\tstate.baseRuleCount = 0;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tif (!state.sheet) return null;\n\tif (state.sheet.cssRules.length > 0) {\n\t\t// Leading top-level non-group rules (style rules, @keyframes — before\n\t\t// the first @media/@container group) are this element's existing\n\t\t// \"base\" region — count them so insertBase() keeps inserting new base\n\t\t// rules right after them, still ahead of every group.\n\t\tlet i = 0;\n\t\twhile (i < state.sheet.cssRules.length) {\n\t\t\tconst r = state.sheet.cssRules[i];\n\t\t\tif (!(r instanceof CSSStyleRule) && \"cssRules\" in r && !groupQueryOf(r).startsWith(\"@keyframes\")) break;\n\t\t\ti++;\n\t\t}\n\t\tstate.baseRuleCount = i;\n\t\tstate.externalRules = new Map();\n\t\tindexExternalRules(state.sheet.cssRules);\n\t}\n\tfor (const rule of state.baseRules) insertBase(state.sheet, rule);\n\tfor (const [query, rules] of state.queryRules) {\n\t\tconst group = getOrCreateGroup(state.sheet, query);\n\t\tif (group) for (const rule of rules) insertGrouped(group, rule);\n\t}\n\treturn state.sheet;\n}\n\nfunction record(rule: string, query: string | undefined): void {\n\tconst state = getState();\n\t// Bind/replay *before* this rule joins the registry, so a fresh bind's\n\t// replay only ever covers previously-known rules — never this one twice.\n\tconst s = ensureSheet();\n\n\tif (query === undefined) {\n\t\tstate.baseRules.push(rule);\n\t} else {\n\t\tlet bucket = state.queryRules.get(query);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tstate.queryRules.set(query, bucket);\n\t\t}\n\t\tbucket.push(rule);\n\t}\n\tstate.cssTextCache = null; // a new rule changes the serialized sheet\n\n\tif (s) {\n\t\tif (query === undefined) {\n\t\t\tinsertBase(s, rule);\n\t\t} else {\n\t\t\tconst group = getOrCreateGroup(s, query);\n\t\t\tif (group) insertGrouped(group, rule);\n\t\t}\n\t}\n}\n\n/**\n * Content-addressed ~42-bit hash (two interleaved FNV-1a style passes). Class\n * names must match between server and client for SSR hydration, so a counter\n * is not an option; 42 bits keeps collision odds low (~1e-5 at 10k\n * unique declaration blocks).\n */\nexport function hash(input: string): string {\n\tlet a = 0x811c9dc5;\n\tlet b = 0x7ee3623b;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst c = input.charCodeAt(i);\n\t\ta = Math.imul(a ^ c, 0x01000193);\n\t\tb = Math.imul(b ^ c, 0x85ebca6b);\n\t}\n\treturn (a >>> 0).toString(36) + ((b >>> 9) % 1296).toString(36);\n}\n\n/** Length-prefixed (query, selector-suffix) key — unambiguous for any strings. */\nexport function contextKeyOf(query: string | undefined, suffix: string): string {\n\tconst queryPart = query === undefined ? \"-\" : \"+\" + query.length + \":\" + query;\n\treturn queryPart + suffix.length + \":\" + suffix;\n}\n\nfunction appendKeyPart(key: string, value: string): string {\n\treturn key + value.length + \":\" + value;\n}\n\nexport function expandAmpersands(input: string, replacement: string): string {\n\tlet expanded = \"\";\n\tlet quote = 0;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst code = input.charCodeAt(i);\n\t\tif (code === 92 /* \\\\ */ && i + 1 < input.length) {\n\t\t\texpanded += input[i] + input[++i];\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote !== 0) {\n\t\t\texpanded += input[i];\n\t\t\tif (code === quote) quote = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tif (code === 34 /* \" */ || code === 39 /* ' */) {\n\t\t\tquote = code;\n\t\t\texpanded += input[i];\n\t\t} else {\n\t\t\texpanded += code === 38 /* & */ ? replacement : input[i];\n\t\t}\n\t}\n\treturn expanded;\n}\n\nfunction selectorFor(className: string, suffix: string): string {\n\tconst selector = \".\" + className;\n\treturn selector + expandAmpersands(suffix, selector);\n}\n\n/**\n * True for strings usable verbatim as an unescaped CSS class selector:\n * letters/digits/`-`/`_`, not starting with a digit or `-`+digit.\n * Char-code loop rather than a RegExp — this runs on the cold mint path but\n * stays allocation-free either way.\n */\nfunction isValidClassName(name: string): boolean {\n\tif (name.length === 0 || name === \"-\") return false;\n\tfor (let i = 0; i < name.length; i++) {\n\t\tconst c = name.charCodeAt(i);\n\t\tconst ok =\n\t\t\t(c >= 97 && c <= 122) || // a-z\n\t\t\t(c >= 65 && c <= 90) || // A-Z\n\t\t\tc === 45 || c === 95 || // - _\n\t\t\t(i > 0 && c >= 48 && c <= 57); // 0-9, never leading\n\t\tif (!ok) return false;\n\t}\n\t// \"-9foo\" is not a valid identifier start either.\n\tif (name.charCodeAt(0) === 45 && name.length > 1) {\n\t\tconst second = name.charCodeAt(1);\n\t\tif (second >= 48 && second <= 57) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Mint (or reuse) the class for a declaration block in a selector context.\n *\n * Declarations keep their authored order — CSS resolves shorthand/longhand\n * and repeated-property conflicts by declaration order, so reordering (e.g.\n * alphabetical sorting) would silently change what `{ pt: 20, p: 16 }` means.\n * The cost is that the same declarations authored in a different order mint a\n * second, content-identical class — harmless, and rare in practice.\n *\n * `name` is the author's identity for the style (`css(\"app-root\", …)`). It\n * becomes part of the cache key, so two different names never share a class\n * even with identical declarations. With `exactName`, this block *is* the\n * named one and takes the name verbatim; otherwise the name is only a\n * readable prefix on a content-addressed prefix (`app-root-1a2b3c`), which is\n * how a named style's non-base contexts and its cx() merges stay identifiable\n * in devtools without ever colliding with the base class.\n */\nexport function atomBlock(\n\tquery: string | undefined,\n\tsuffix: string,\n\tdecls: ReadonlyArray<readonly [string, string]>,\n\tname?: string,\n\texactName = false,\n): string {\n\tconst state = getState();\n\tlet contextKey = contextKeyOf(query, suffix);\n\tfor (const [prop, value] of decls) {\n\t\tcontextKey = appendKeyPart(appendKeyPart(contextKey, prop), value);\n\t}\n\tif (name !== undefined && !isValidClassName(name)) {\n\t\tconsole.warn(\n\t\t\t`[nuclo] css() name ${JSON.stringify(name)} is not a valid CSS class name ` +\n\t\t\t\t\"(letters, digits, - and _ only, not starting with a digit); falling back to a generated name.\",\n\t\t);\n\t\tname = undefined;\n\t}\n\tconst declKey = name === undefined ? \"a\" + contextKey : \"n\" + appendKeyPart(\"\", name) + contextKey;\n\tlet className = state.atomCache.get(declKey);\n\tif (className !== undefined) {\n\t\t// Touch the sheet so a swapped document (tests) gets the replayed rules.\n\t\tensureSheet();\n\t\treturn className;\n\t}\n\n\tif (name === undefined) {\n\t\tclassName = \"n\" + hash(declKey);\n\t} else {\n\t\tclassName = exactName ? name : name + \"-\" + hash(declKey);\n\t\tif (state.atomMeta.has(className)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[nuclo] css() name ${JSON.stringify(className)} is already used by a different style. ` +\n\t\t\t\t\t\"Both rules are emitted under the same class, so they will override each other — rename one.\",\n\t\t\t);\n\t\t}\n\t}\n\n\tlet body = \"\";\n\tfor (const [prop, value] of decls) body += (body ? \";\" : \"\") + prop + \":\" + value;\n\tstate.atomCache.set(declKey, className);\n\tstate.atomMeta.set(className, { query, suffix, decls, name });\n\trecord(selectorFor(className, suffix) + \"{\" + body + \"}\", query);\n\treturn className;\n}\n\n/**\n * Merge two same-context generated classes into one: `second`'s declarations\n * override `first`'s per exact property (shorthand/longhand interplay is left\n * to CSS order), and the combined block mints its own content-addressed class\n * — so cx(base, override) keeps base's untouched properties instead of\n * dropping the whole base class. Deterministic: the same pair always yields\n * the same class name, on server and client alike. A named base passes its\n * name down as a prefix, so `cx(appRoot, active)` reads as `app-root-1a2b3c`.\n */\nexport function mergeBlocks(first: string, second: string): string {\n\tconst state = getState();\n\tconst a = state.atomMeta.get(first);\n\tconst b = state.atomMeta.get(second);\n\tif (!a || !b) return second; // unknown input — nothing to merge with\n\tconst concat = [...a.decls, ...b.decls];\n\t// Keep only the last occurrence of each exact property, at its position —\n\t// so a later `padding` still fully resets an earlier `padding-top`.\n\tconst lastIndex = new Map<string, number>();\n\tfor (let i = 0; i < concat.length; i++) lastIndex.set(concat[i][0], i);\n\tconst merged = concat.filter((decl, i) => lastIndex.get(decl[0]) === i);\n\treturn atomBlock(a.query, a.suffix, merged, a.name ?? b.name);\n}\n\n/** Conflict key (query|suffix) for a generated class — lets cx() group classes by selector context. */\nexport function conflictKeyOf(className: string): string | undefined {\n\tconst state = getState();\n\tconst meta = state.atomMeta.get(className);\n\treturn meta === undefined ? undefined : contextKeyOf(meta.query, meta.suffix);\n}\n\n/** Current registry generation, used to invalidate per-instance WeakMap hits after resetStyles(). */\nexport function getStyleEpoch(): number {\n\treturn (cachedState ?? globalScope[ENGINE_STATE_KEY])?.styleEpoch ?? 0;\n}\n\n/** Register a non-atomic rule (keyframes, global styles) once per dedupe key. */\nexport function addRawRule(dedupeKey: string, rule: string): void {\n\tconst state = getState();\n\tif (state.rawKeys.has(dedupeKey)) {\n\t\tensureSheet();\n\t\treturn;\n\t}\n\tstate.rawKeys.add(dedupeKey);\n\trecord(rule, undefined);\n}\n\n/** All generated CSS — base rules first, then at-rule groups in registration order. */\nexport function getCssText(): string {\n\tconst state = getState();\n\tif (state.cssTextCache !== null) return state.cssTextCache;\n\tlet out = state.baseRules.join(\"\");\n\tfor (const [query, rules] of state.queryRules) {\n\t\tif (rules.length > 0) out += query + \"{\" + rules.join(\"\") + \"}\";\n\t}\n\tstate.cssTextCache = out;\n\treturn out;\n}\n\n/** Clear all engine state (test helper). Removes the injected style element. */\nexport function resetStyles(): void {\n\tconst state = getState();\n\tstate.atomCache.clear();\n\tstate.atomMeta.clear();\n\tstate.rawKeys.clear();\n\tstate.baseRules.length = 0;\n\tstate.queryRules.clear();\n\tstate.groupRules.clear();\n\tstate.cssTextCache = null;\n\tstate.baseRuleCount = 0;\n\tstate.sheet = null;\n\tstate.sheetDocument = null;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tstate.styleEpoch++;\n\tif (typeof document !== \"undefined\") {\n\t\tdocument.getElementById(\"nuclo-styles\")?.remove();\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nuclo.ssr.cjs","names":["NucloElement"],"sources":["../../src/shared/strings.ts","../../src/hydration.ts","../../src/ssr/render-to-string.ts","../../src/style/engine.ts"],"sourcesContent":["/**\n * camelCase → kebab-case for CSS property (and attribute) names. A leading\n * capital becomes a leading dash, as vendor prefixes need\n * (WebkitTransition → -webkit-transition). Char-code loop with a\n * no-uppercase fast path: SSR runs this per style declaration.\n */\nexport function camelToKebab(str: string): string {\n let firstUpper = 0;\n for (; firstUpper < str.length; firstUpper++) {\n const code = str.charCodeAt(firstUpper);\n if (code >= 65 && code <= 90) break;\n }\n if (firstUpper === str.length) return str;\n\n let result = str.slice(0, firstUpper);\n for (let i = firstUpper; i < str.length; i++) {\n const code = str.charCodeAt(i);\n // A-Z is 65-90, a-z is 97-122 (difference of 32)\n result += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];\n }\n return result;\n}\n\nconst ATTR_ESCAPE_RE = /[&<>\"']/;\nconst TEXT_ESCAPE_RE = /[&<>]/;\n\n/**\n * Escapes HTML special characters in attribute values (includes \" and ')\n */\nexport function escapeHtml(text: string): string {\n // Fast path: most strings contain no special characters at all.\n const first = ATTR_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n case 34: escaped = '"'; break; // \"\n case 39: escaped = '''; break; // '\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n\n/**\n * Escapes HTML special characters in text node content.\n * Only &, < and > need escaping — quotes are safe inside text nodes.\n */\nexport function escapeText(text: string): string {\n const first = TEXT_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n","/**\n * Hydration context — tracks global hydration state and per-parent cursor positions.\n *\n * During hydration the component tree is built against an existing DOM tree\n * (produced by SSR). Instead of creating new nodes, factories claim existing\n * children from their parent element by advancing a cursor.\n *\n * The cursor is a *node pointer* (the next unclaimed child), not an index.\n * Indexed childNodes access is O(n) after any tree/attribute mutation in\n * jsdom (live NodeList re-materialization) which made hydration O(n²);\n * firstChild/nextSibling pointers are O(1) in every DOM implementation.\n */\n\nimport { safeRemoveChild } from \"./shared/dom\";\n\nlet _hydrating = false;\n// True while forceUpdate() re-hydrates a live, client-rendered tree: text\n// claims work without `<!-- text-N -->` markers and reused elements get their\n// previous listeners/class state reset before modifiers re-apply.\nlet _force = false;\n// True while renderToString() is building a tree destined for serialization.\nlet _serializing = false;\n// Missing entry = cursor at parent.firstChild; null = past the last child.\nlet _cursors = new WeakMap<Node, Node | null>();\n\nexport function isHydrating(): boolean {\n return _hydrating;\n}\n\n/** True only inside a forceUpdate() pass (never during plain hydrate()). */\nexport function isForceHydrating(): boolean {\n return _hydrating && _force;\n}\n\n/**\n * True while a tree is being built for SSR serialization (inside\n * renderToString). Text children must keep their `<!-- text-N -->` markers in\n * this mode so the emitted HTML stays hydratable — even when `isBrowser` is\n * true (SSR running under jsdom, or an isomorphic worker that provides a DOM).\n * A pure client render leaves this false and skips the markers.\n */\nexport function isSerializing(): boolean {\n return _serializing;\n}\n\n/**\n * Runs `fn` with serialization mode enabled, restoring the previous state\n * afterwards (so nested/re-entrant renderToString calls behave correctly).\n */\nexport function runSerializing<T>(fn: () => T): T {\n const previous = _serializing;\n _serializing = true;\n try {\n return fn();\n } finally {\n _serializing = previous;\n }\n}\n\nexport function startHydration(force = false): void {\n _hydrating = true;\n _force = force;\n // Each hydration pass starts with a clean cursor slate. Stale cursors from\n // a previous pass would desynchronize claims when a container's content is\n // replaced and re-hydrated (HMR, islands re-mounting).\n _cursors = new WeakMap<Node, Node | null>();\n}\n\nexport function endHydration(): void {\n _hydrating = false;\n _force = false;\n}\n\n/**\n * Runs a callback with hydration temporarily disabled, restoring the previous\n * state afterwards. Used when a server/client mismatch forces a subtree to be\n * rendered fresh in the middle of a hydration pass — fresh rendering must not\n * claim nodes from the surrounding SSR DOM.\n */\nexport function runWithoutHydration<T>(fn: () => T): T {\n const wasHydrating = _hydrating;\n _hydrating = false;\n try {\n return fn();\n } finally {\n _hydrating = wasHydrating;\n }\n}\n\n/**\n * Returns the next unclaimed child of a parent node (or null at the end)\n * without advancing the cursor.\n */\nexport function peekChild(parent: Node): Node | null {\n const entry = _cursors.get(parent);\n return entry === undefined ? parent.firstChild : entry;\n}\n\n/**\n * Sets the cursor to a specific child node (or null for \"past the end\").\n */\nexport function setCursor(parent: Node, node: Node | null): void {\n _cursors.set(parent, node);\n}\n\nfunction isWhitespaceText(node: Node): boolean {\n return node.nodeType === 3 && !/\\S/.test(node.textContent || '');\n}\n\n/**\n * Advances the cursor past whitespace-only text nodes.\n *\n * Nuclo's own SSR output never produces bare whitespace text nodes (text\n * children are always preceded by a `<!-- text-N -->` marker), so unmarked\n * whitespace comes from the surrounding template or an HTML\n * formatter/minifier. Skipping (without removing) keeps the cursor aligned\n * while leaving the document's visual whitespace untouched.\n */\nexport function skipWhitespaceText(parent: Node): void {\n let child = peekChild(parent);\n let advanced = false;\n while (child && isWhitespaceText(child)) {\n child = child.nextSibling;\n advanced = true;\n }\n if (advanced) {\n setCursor(parent, child);\n }\n}\n\n/**\n * Claims the next child node from parent at the current cursor position.\n * Advances the cursor by one.\n */\nexport function claimChild(parent: Node): Node | null {\n const child = peekChild(parent);\n if (child) {\n setCursor(parent, child.nextSibling);\n }\n return child;\n}\n\n/**\n * forceUpdate() text claim: claims the next child when it is a bare text node\n * (client renders emit no `<!-- text-N -->` markers). Whitespace-only text\n * that is followed by a text marker is SSR template formatting, not content —\n * it is left for the marker path, which skips it. Returns null when nothing\n * claimable is at the cursor.\n */\nexport function claimBareText(parent: Node): Text | null {\n const next = peekChild(parent);\n if (!next || next.nodeType !== 3) return null;\n if (isWhitespaceText(next)) {\n let sibling = next.nextSibling;\n while (sibling && isWhitespaceText(sibling)) sibling = sibling.nextSibling;\n if (\n sibling && sibling.nodeType === 8 &&\n (sibling.textContent ?? \"\").trimStart().startsWith(\"text-\")\n ) return null;\n }\n claimChild(parent);\n return next as Text;\n}\n\n/**\n * Claims a server-rendered `<!--{prefix}-start-…-->` … `<!--{prefix}-end-->`\n * block (list()/when()) at the cursor, leaving the cursor just after the start\n * marker. Returns null, claiming nothing, when the next child isn't a start\n * marker. Pairs are depth-counted because directly nested blocks share a\n * host. A missing end marker (corrupt/truncated SSR output) is recreated right\n * after the start marker and reported as `recreated`.\n */\nexport function claimMarkerPair(\n parent: Node,\n prefix: \"list\" | \"when\",\n): { start: Comment; end: Comment; recreated: boolean } | null {\n skipWhitespaceText(parent);\n const start = peekChild(parent);\n const startPrefix = prefix + \"-start-\";\n if (!start || start.nodeType !== 8 || !start.textContent?.startsWith(startPrefix)) return null;\n claimChild(parent);\n const endText = prefix + \"-end\";\n let depth = 0;\n for (let node = start.nextSibling; node; node = node.nextSibling) {\n if (node.nodeType !== 8) continue;\n const text = node.textContent;\n if (text === endText) {\n if (depth === 0) return { start: start as Comment, end: node as Comment, recreated: false };\n depth--;\n } else if (text?.startsWith(startPrefix)) {\n depth++;\n }\n }\n const end = document.createComment(endText);\n parent.insertBefore(end, start.nextSibling);\n return { start: start as Comment, end, recreated: true };\n}\n\n/**\n * Attempts to claim an existing element from the parent during hydration.\n * Returns the claimed element if the next child matches the expected tag, or null.\n */\nexport function claimElement(parent: Node, tagName: string): Element | null {\n if (!_hydrating) return null;\n skipWhitespaceText(parent);\n const candidate = peekChild(parent);\n if (candidate && candidate.nodeType === 1 && (candidate as Element).tagName.toLowerCase() === tagName) {\n return claimChild(parent) as Element;\n }\n return null;\n}\n\n/**\n * Removes unclaimed SSR children from a hydrated element.\n * Must be called after modifiers have been applied to a claimed element.\n * Removes nodes from the cursor through `lastOriginalChild` (the element's\n * last child before modifiers ran) — nodes appended by modifiers come after\n * that boundary and are preserved.\n */\nexport function cleanupUnclaimedChildren(node: Node, lastOriginalChild: Node | null): void {\n if (!lastOriginalChild) return;\n let current = peekChild(node);\n while (current) {\n const next = current === lastOriginalChild ? null : current.nextSibling;\n // safeRemoveChild (not a bare removeChild): during forceUpdate() the\n // unclaimed nodes were live — their listeners, registrations and\n // onDestroy hooks must be released, not just detached.\n safeRemoveChild(current);\n current = next;\n }\n setCursor(node, null);\n}\n","/**\n * Server-Side Rendering (SSR) utilities for Nuclo\n * Renders Nuclo components to HTML strings in Node.js environment\n */\n\nimport { escapeHtml, escapeText, camelToKebab } from '../shared/strings';\nimport { runSerializing } from '../hydration';\nimport { NucloElement } from '../polyfill/Element';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\n\n/**\n * SVG attributes that are natively camelCase and must NOT be converted to kebab-case.\n * Most SVG attributes are already kebab-case (stroke-width, fill-rule, etc.), but a\n * handful are defined as camelCase in the SVG spec and must be preserved.\n */\nconst SVG_PRESERVE_CASE_ATTRS = new Set([\n 'viewBox', 'preserveAspectRatio', 'markerWidth', 'markerHeight',\n 'gradientTransform', 'patternTransform', 'clipPathUnits', 'gradientUnits',\n 'patternUnits', 'pathLength', 'refX', 'refY', 'stdDeviation',\n 'baseFrequency', 'numOctaves', 'kernelMatrix', 'tableValues',\n 'targetX', 'targetY', 'specularExponent', 'specularConstant',\n 'diffuseConstant', 'surfaceScale', 'xChannelSelector', 'yChannelSelector',\n 'edgeMode', 'stitchTiles', 'spreadMethod', 'patternContentUnits',\n 'markerUnits', 'startOffset', 'textLength', 'lengthAdjust',\n]);\n\n/**\n * HTML boolean attributes — presence means true, absence means false.\n * When the stored value is the string \"true\" or \"false\" (from setAttribute),\n * we must re-apply boolean semantics instead of outputting the raw string.\n */\nconst HTML_BOOLEAN_ATTRIBUTES = new Set([\n 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',\n 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'ismap', 'loop',\n 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'readonly', 'required',\n 'reversed', 'selected',\n]);\n\n/**\n * Serializes a DOM attribute value\n */\nfunction serializeAttribute(name: string, value: unknown): string {\n if (value === null || value === undefined || value === false) {\n return '';\n }\n\n if (value === true) {\n return ` ${name}`;\n }\n\n // Boolean attributes stored as strings via setAttribute() need special handling\n if (HTML_BOOLEAN_ATTRIBUTES.has(name)) {\n if (value === 'false') return '';\n if (value === 'true' || value === '' || value === name) return ` ${name}`;\n }\n\n // A style object (e.g. renderToStringWithContainer's containerAttrs from\n // untyped JS): serialize its declarations, skipping nullish/empty values.\n if (name === 'style' && typeof value === 'object') {\n let styleStr = '';\n for (const key in value as Record<string, unknown>) {\n const val = (value as Record<string, unknown>)[key];\n if (val == null || val === '') continue;\n styleStr += (styleStr ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n return styleStr ? ` style=\"${escapeHtml(styleStr)}\"` : '';\n }\n\n return ` ${name}=\"${escapeHtml(String(value))}\"`;\n}\n\n/**\n * Serializes DOM element attributes to HTML string\n *\n * `isPolyfill` is passed in by the caller (serializeNode already computed it\n * to decide whether tagName needs lowercasing) so this doesn't repeat the\n * same `Array.isArray` discriminator check per element.\n */\nfunction serializeAttributes(element: Element, isPolyfill: boolean): string {\n let result = '';\n\n // Handle polyfill elements. NucloElement keeps every child in a plain Array\n // (`children`), which a real DOM element never does (HTMLCollection) — this is\n // the same allocation-free discriminator serializeNode() uses. Reading\n // `el.attributes` directly would lazily allocate an empty Map for every\n // element being serialized, so the backing `_attributes` field is read instead.\n if (isPolyfill) {\n const el = element as any;\n const attrs = el._attributes as Map<string, string> | undefined;\n\n // id — may live on the property rather than in the Map\n if (el.id && !attrs?.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // class — kept on .className, mirrored to Map only when setAttribute is used\n if (el.className && !attrs?.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // style — lives on the backing _style object (its own properties are the\n // declarations), not in the attributes Map. Read the field directly (never\n // `el.style`) so elements that never set a style are not forced to lazily\n // allocate an empty SSRStyle just to serialize. Empty values (e.g. reactive\n // styles that resolved to undefined) are skipped.\n const styleObj = el._style as Record<string, unknown> | undefined;\n if (styleObj && !attrs?.has('style')) {\n let css = '';\n for (const key in styleObj) {\n const val = styleObj[key];\n if (val == null || val === '') continue;\n css += (css ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n if (css) result += ` style=\"${escapeHtml(css)}\"`;\n }\n\n // All remaining attributes from the Map\n // Convert camelCase ARIA/HTML attribute names (e.g. ariaLabel → aria-label) that the\n // polyfill stores as-is because NucloElement lacks the browser property mappings.\n // SVG attributes that are natively camelCase (e.g. viewBox, preserveAspectRatio) must\n // NOT be converted — they are stored via setAttribute() as-is.\n if (attrs) {\n for (const [name, value] of attrs) {\n const htmlName = SVG_PRESERVE_CASE_ATTRS.has(name) ? name : camelToKebab(name);\n result += serializeAttribute(htmlName, value);\n }\n }\n return result;\n }\n\n // Handle browser elements with NamedNodeMap attributes\n if (element.attributes && element.attributes.length) {\n for (let i = 0; i < element.attributes.length; i++) {\n const attr = element.attributes[i];\n if (attr && attr.name) {\n result += serializeAttribute(attr.name, attr.value);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Tag-category lookup, keyed by lowercase tag name. Every element in a tree\n * hits this once, so void + raw-text elements are folded into a single\n * dictionary lookup instead of two separate `Set.has()` probes (one of which\n * would always miss for the other category).\n */\nconst TAG_VOID = 1;\nconst TAG_RAW_TEXT = 2;\nconst TAG_CATEGORY: Record<string, 1 | 2> = Object.assign(Object.create(null), {\n // Self-closing HTML tags that don't have closing tags\n area: TAG_VOID, base: TAG_VOID, br: TAG_VOID, col: TAG_VOID, embed: TAG_VOID,\n hr: TAG_VOID, img: TAG_VOID, input: TAG_VOID, link: TAG_VOID, meta: TAG_VOID,\n param: TAG_VOID, source: TAG_VOID, track: TAG_VOID, wbr: TAG_VOID,\n // Raw-text elements — browsers never decode entities inside them, so their\n // text content must be emitted verbatim (escaping would corrupt inline\n // JS/CSS, e.g. `a < b` becoming `a < b` inside a script). A closing-tag\n // sequence in the content would terminate the element early, so it is\n // neutralized (see escapeRawText).\n script: TAG_RAW_TEXT, style: TAG_RAW_TEXT,\n});\n\nfunction escapeRawText(tagName: string, text: string): string {\n // \"</script\" (any case) inside a script would close it — break the sequence\n // the same way JSON serializers do (\"<\\/script\").\n return text.replace(new RegExp('</(' + tagName + ')', 'gi'), '<\\\\/$1');\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node — only & < > need escaping; quotes are safe in text content\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeText(node.textContent || '');\n }\n\n // Comment node\n if (node.nodeType === 8) { // Node.COMMENT_NODE\n return `<!--${node.textContent || ''}-->`;\n }\n\n // Element node\n if (node.nodeType === 1) { // Node.ELEMENT_NODE\n const element = node as Element;\n const rawTagName = element.tagName;\n // Duck-typed polyfill discriminator (also used by serializeAttributes): a\n // plain Array `children` field never occurs on a real\n // DOM element (HTMLCollection), only on NucloElement — and on hand-rolled\n // polyfill-shaped test doubles, which is why this alone isn't enough to\n // skip re-lowercasing tagName below.\n const isPolyfillShape = Array.isArray((element as any).children);\n // Only a *real* NucloElement is guaranteed to have pre-lowercased tagName\n // (done once in its constructor) — re-lowercasing it here on every\n // element would be pure waste, since this is the hottest function in\n // renderToString. Anything else (browser Element, or a polyfill-shaped\n // object that didn't go through `new NucloElement()`) still needs it.\n const tagName = element instanceof NucloElement ? rawTagName : rawTagName.toLowerCase();\n const attributes = serializeAttributes(element, isPolyfillShape);\n const category = TAG_CATEGORY[tagName];\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as any).children : element.childNodes;\n\n // Raw-text elements: emit text verbatim (no entity escaping, no Nuclo\n // text markers — `<!--` would act as a line comment inside a script).\n if (category === TAG_RAW_TEXT) {\n let rawContent = '';\n const rawChildren = childNodes;\n if (rawChildren && rawChildren.length > 0) {\n for (let i = 0; i < rawChildren.length; i++) {\n const child = rawChildren[i];\n if (child && child.nodeType === 3) {\n rawContent += child.textContent || '';\n }\n }\n } else {\n const tc = (node as { textContent?: unknown }).textContent;\n if (typeof tc === 'string') rawContent = tc;\n }\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n if (childNodes && childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n childrenHtml += serializeNode(child);\n }\n }\n } else {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as any).textContent;\n if (typeof tc === 'string' && tc) {\n childrenHtml = escapeText(tc);\n }\n }\n\n return `<${tagName}${attributes}>${childrenHtml}</${tagName}>`;\n }\n\n // Document fragment\n if (node.nodeType === 11) { // Node.DOCUMENT_FRAGMENT_NODE\n let result = '';\n const childNodes: ArrayLike<Node> = (node as any).childNodes ?? [];\n if (childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n result += serializeNode(child);\n }\n }\n }\n return result;\n }\n\n return '';\n}\n\n/**\n * Renders a Nuclo component to an HTML string for server-side rendering\n *\n * @param input - A Nuclo component function, DOM element, or node\n * @returns HTML string representation of the component\n *\n * @example\n * ```ts\n * import { renderToString } from 'nuclo/ssr';\n * import { div } from 'nuclo';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div>Hello, World!</div>'\n * ```\n */\nexport function renderToString(input: RenderableInput): string {\n if (!input) return '';\n\n if (typeof input === 'function') {\n try {\n // Build the tree in serialization mode so text children keep their\n // <!-- text-N --> markers (needed for hydration) even when isBrowser is\n // true, e.g. SSR running under jsdom.\n const element = runSerializing(() => {\n if (typeof document === 'undefined') throw new Error('Document is not available. Make sure polyfills are loaded.');\n return input(document.createElement('div') as unknown as ExpandedElement<ElementTagName>, 0);\n });\n return element && typeof element === 'object' && 'nodeType' in element ? serializeNode(element as Node) : '';\n } catch (error) {\n console.error('Error rendering component to string:', error);\n return '';\n }\n }\n\n if ('nodeType' in input) {\n return serializeNode(input as Node);\n }\n\n return '';\n}\n\n/**\n * Renders multiple Nuclo components to HTML strings\n *\n * @param inputs - Array of Nuclo components\n * @returns Array of HTML strings\n */\nexport function renderManyToString(inputs: RenderableInput[]): string[] {\n return inputs.map(input => renderToString(input));\n}\n\n/**\n * Renders a Nuclo component and wraps it in a container element\n *\n * @param input - A Nuclo component\n * @param containerTag - The tag name for the container (default: 'div')\n * @param containerAttrs - Attributes for the container element\n * @returns HTML string with container wrapper\n */\nexport function renderToStringWithContainer(\n input: RenderableInput,\n containerTag: string = 'div',\n containerAttrs: Record<string, string> = {}\n): string {\n const content = renderToString(input);\n const attrs = Object.entries(containerAttrs)\n .map(([key, value]) => serializeAttribute(key, value))\n .join('');\n\n return `<${containerTag}${attrs}>${content}</${containerTag}>`;\n}\n","/**\n * Shared generated-CSS engine state.\n *\n * One stylesheet, one rule registry, shared by every `createCss` instance so\n * identical declarations dedupe globally and cascade order stays deterministic:\n * base rules first (insertion order), then one grouping rule per at-rule query\n * in registration order (theme `screens` order).\n *\n * Browser: rules are injected through CSSOM with O(1) appends — no rescans.\n * SSR: rules accumulate in the registry; `getCssText()` serializes them.\n *\n * The state below lives on `globalThis`, keyed by a `Symbol.for(...)` (the\n * global symbol registry, shared by every realm-local copy of this module).\n * `nuclo` publishes `.`, `./ssr`, and `./polyfill` as independently built\n * bundle files (see tsdown.config.ts) — a consumer that resolves them\n * through normal package resolution (any bundler/runtime that doesn't\n * alias every subpath to this same source file) loads two or three\n * separate copies of this module, each with its own module-scope\n * variables. Without a realm-wide singleton, `css()` (from `nuclo`) would\n * mint rules into one copy's registry while `getCssText()` (from\n * `nuclo/ssr`) reads an entirely different, always-empty one — SSR output\n * would render the right classes with no matching CSS at all. Storing the registry on `globalThis` instead makes\n * every copy of this module share the same state regardless of how many\n * times it ends up bundled.\n *\n * The registry never shrinks on its own: atomCache/atomMeta/rawKeys/\n * baseRules/queryRules retain every rule for the life of the process —\n * content-addressed dedup needs a durable record of what each class means,\n * and the engine has no way to know whether a minted class still backs a\n * live DOM node (it's deliberately decoupled from element/list lifecycle),\n * so it can't safely evict. Only resetStyles() (a manual test helper)\n * clears it. This is fine for the intended use — a bounded design-token\n * vocabulary — but a caller piping truly per-instance dynamic values\n * through css({ raw: {...} }) (e.g. per-row data in a list) mints one\n * permanent class per unique value; prefer the `style` prop\n * (element/inline-style.ts) for values that vary per instance rather than\n * per design token. See test/style/registry-growth.test.ts.\n */\n\nexport interface BlockMeta {\n\treadonly query: string | undefined;\n\treadonly suffix: string;\n\treadonly decls: ReadonlyArray<readonly [string, string]>;\n\t/** Author-supplied identity from `css(name, …)`; inherited by cx() merges. */\n\treadonly name: string | undefined;\n}\n\ninterface EngineState {\n\t// Registry — the source of truth for all generated CSS.\n\t// atomCache: declaration-block key -> class name (dedup)\n\t// atomMeta: class name -> what the class means (its selector context and its\n\t// declarations). This is what lets cx() merge two same-context classes at the\n\t// declaration level instead of guessing: the engine can always recover the\n\t// exact content of any class it minted.\n\tatomCache: Map<string, string>;\n\tatomMeta: Map<string, BlockMeta>;\n\trawKeys: Set<string>;\n\tbaseRules: string[];\n\tqueryRules: Map<string, string[]>;\n\tstyleEpoch: number;\n\n\t// Memoized getCssText() output. SSR calls getCssText() once per request, but the\n\t// rule set is stable after warmup — so the serialized sheet is recomputed only\n\t// when a new rule is actually recorded. Invalidated in record() and resetStyles().\n\tcssTextCache: string | null;\n\n\t// Browser CSSOM state. Base rules occupy [0, baseRuleCount); grouping rules\n\t// (one per query, created in registration order) live after them. CSSRule\n\t// references stay live when indices shift, so insertion never rescans.\n\tsheet: CSSStyleSheet | null;\n\tsheetDocument: Document | null;\n\tbaseRuleCount: number;\n\tgroupRules: Map<string, CSSGroupingRule>;\n\n\t// Normalized rules already sitting in a freshly-adopted stylesheet that this\n\t// module didn't insert itself — e.g. server-rendered CSS being hydrated.\n\t// Rebuilt on every fresh bind in ensureSheet(); null when there is nothing\n\t// external to guard against.\n\texternalRules: Map<string, Set<string>> | null;\n\thasExternalGroups: boolean;\n}\n\nfunction createState(): EngineState {\n\treturn {\n\t\tatomCache: new Map(),\n\t\tatomMeta: new Map(),\n\t\trawKeys: new Set(),\n\t\tbaseRules: [],\n\t\tqueryRules: new Map(),\n\t\tstyleEpoch: 0,\n\t\tcssTextCache: null,\n\t\tsheet: null,\n\t\tsheetDocument: null,\n\t\tbaseRuleCount: 0,\n\t\tgroupRules: new Map(),\n\t\texternalRules: null,\n\t\thasExternalGroups: false,\n\t};\n}\n\nconst ENGINE_STATE_KEY = Symbol.for(\"nuclo.style.engine.v1\");\nconst globalScope = globalThis as unknown as Record<symbol, EngineState>;\n// Importing the runtime does not allocate the registry until styling is used.\nlet cachedState: EngineState | undefined;\nfunction getState(): EngineState {\n\treturn cachedState ??= globalScope[ENGINE_STATE_KEY] ?? (globalScope[ENGINE_STATE_KEY] = createState());\n}\n\n/** Pre-register at-rule queries so their cascade order follows theme order. */\nexport function registerQueries(queries: Iterable<string>): void {\n\tfor (const query of queries) {\n\t\tconst state = getState();\n\t\tif (!state.queryRules.has(query)) state.queryRules.set(query, []);\n\t}\n}\n\nfunction createGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\ttry {\n\t\tconst idx = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", idx);\n\t\tconst group = s.cssRules[idx] as CSSGroupingRule;\n\t\tstate.groupRules.set(query, group);\n\t\tstate.groupRules.set(groupQueryOf(group), group);\n\t\treturn group;\n\t} catch {\n\t\treturn null; // unsupported at-rule in this environment\n\t}\n}\n\n// A group prelude is stable while its children grow. Reading cssText on each\n// insertion would serialize all previous children and make loading quadratic.\n// Weak keys allow detached stylesheets to be collected after reset/rebinding.\nconst groupQueries = new WeakMap<CSSRule, string>();\nfunction groupQueryOf(rule: CSSRule): string {\n\tconst cached = groupQueries.get(rule);\n\tif (cached !== undefined) return cached;\n\tconst text = rule.cssText;\n\tconst brace = text.indexOf(\"{\");\n\tconst query = (brace === -1 ? text : text.slice(0, brace)).trim();\n\tgroupQueries.set(rule, query);\n\treturn query;\n}\n\nfunction ruleIdentity(rule: CSSRule): string {\n\treturn rule instanceof CSSStyleRule ? rule.selectorText : groupQueryOf(rule);\n}\n\nfunction rememberExternalRule(rule: CSSRule, query: string | undefined): void {\n\tconst state = getState();\n\tif (!state.externalRules) return;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tlet texts = state.externalRules.get(key);\n\tif (!texts) {\n\t\ttexts = new Set();\n\t\tstate.externalRules.set(key, texts);\n\t}\n\ttexts.add(rule.cssText);\n}\n\nfunction consumeExternalRule(rule: CSSRule, query: string | undefined): boolean {\n\tconst state = getState();\n\tif (!state.externalRules) return false;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tconst texts = state.externalRules.get(key);\n\tif (!texts?.delete(rule.cssText)) return false;\n\tif (texts.size === 0) state.externalRules.delete(key);\n\tif (state.externalRules.size === 0) state.externalRules = null;\n\treturn true;\n}\n\n/**\n * Index an already-populated stylesheet found on adopt (SSR output, most\n * commonly): every top-level conditional grouping rule (@media/@container/…)\n * is registered into groupRules (by its reconstructed at-rule text) so later\n * record() calls append into it instead of creating a sibling duplicate. The\n * other rules are indexed by scope, selector/prelude, and normalized cssText\n * so exact server/client matches can be skipped without hiding a changed\n * named or global rule that happens to reuse the same selector.\n */\nfunction indexExternalRules(rules: CSSRuleList): void {\n\tconst state = getState();\n\tfor (let i = 0; i < rules.length; i++) {\n\t\tconst rule = rules[i];\n\t\tif (rule instanceof CSSStyleRule) {\n\t\t\trememberExternalRule(rule, undefined);\n\t\t\tcontinue;\n\t\t}\n\t\tconst prelude = groupQueryOf(rule);\n\t\tif (\"cssRules\" in rule && !prelude.startsWith(\"@keyframes\")) {\n\t\t\tconst group = rule as CSSGroupingRule;\n\t\t\tstate.hasExternalGroups = true;\n\t\t\tstate.groupRules.set(prelude, group);\n\t\t\tfor (let j = 0; j < group.cssRules.length; j++) {\n\t\t\t\trememberExternalRule(group.cssRules[j], prelude);\n\t\t\t}\n\t\t} else {\n\t\t\t// @keyframes, @font-face, … — one-shot rules matched by prelude.\n\t\t\trememberExternalRule(rule, undefined);\n\t\t}\n\t}\n}\n\nfunction insertBase(s: CSSStyleSheet, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\ts.insertRule(rule, state.baseRuleCount);\n\t\tconst inserted = s.cssRules[state.baseRuleCount];\n\t\tif (consumeExternalRule(inserted, undefined)) {\n\t\t\ts.deleteRule(state.baseRuleCount);\n\t\t\treturn;\n\t\t}\n\t\tstate.baseRuleCount++;\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction insertGrouped(group: CSSGroupingRule, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\tconst index = group.cssRules.length;\n\t\tgroup.insertRule(rule, index);\n\t\tconst inserted = group.cssRules[index];\n\t\tif (state.externalRules && consumeExternalRule(inserted, groupQueryOf(group))) group.deleteRule(index);\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction getOrCreateGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\tconst direct = state.groupRules.get(query);\n\tif (direct) return direct;\n\tif (!state.hasExternalGroups) return createGroup(s, query);\n\n\t// CSSOM may normalize whitespace in an at-rule prelude. Normalize the\n\t// authored query through a temporary empty group before deciding that an\n\t// adopted SSR group is missing.\n\ttry {\n\t\tconst index = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", index);\n\t\tconst normalized = groupQueryOf(s.cssRules[index]);\n\t\ts.deleteRule(index);\n\t\tconst existing = state.groupRules.get(normalized);\n\t\tif (existing) {\n\t\t\tstate.groupRules.set(query, existing);\n\t\t\treturn existing;\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n\treturn createGroup(s, query);\n}\n\n/**\n * Returns the live stylesheet, (re)binding when the document changes — e.g.\n * fresh jsdom documents between tests. Rebinding replays the whole registry,\n * which replaces v1's per-call classExistsInDOM() DOM scans with a single\n * replay per document.\n */\nexport function ensureSheet(): CSSStyleSheet | null {\n\tconst state = getState();\n\tif (typeof document === \"undefined\") return null;\n\t// The nuclo SSR polyfill provides a document without getElementById — in\n\t// that context rules accumulate in the registry for getCssText() only.\n\tif (typeof document.getElementById !== \"function\") return null;\n\tif (\n\t\tstate.sheet &&\n\t\tstate.sheetDocument === document &&\n\t\tstate.sheet.ownerNode &&\n\t\t(state.sheet.ownerNode as Element).isConnected\n\t) {\n\t\treturn state.sheet;\n\t}\n\tconst existing = document.getElementById(\"nuclo-styles\");\n\tlet el: HTMLStyleElement;\n\tif (existing?.tagName === \"STYLE\") {\n\t\tel = existing as HTMLStyleElement;\n\t} else {\n\t\tel = document.createElement(\"style\");\n\t\tel.id = \"nuclo-styles\";\n\t\tif (existing) existing.replaceWith(el);\n\t\telse document.head.appendChild(el);\n\t}\n\tstate.sheet = el.sheet;\n\tstate.sheetDocument = document;\n\tstate.groupRules.clear();\n\tstate.baseRuleCount = 0;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tif (!state.sheet) return null;\n\tif (state.sheet.cssRules.length > 0) {\n\t\t// Leading top-level non-group rules (style rules, @keyframes — before\n\t\t// the first @media/@container group) are this element's existing\n\t\t// \"base\" region — count them so insertBase() keeps inserting new base\n\t\t// rules right after them, still ahead of every group.\n\t\tlet i = 0;\n\t\twhile (i < state.sheet.cssRules.length) {\n\t\t\tconst r = state.sheet.cssRules[i];\n\t\t\tif (!(r instanceof CSSStyleRule) && \"cssRules\" in r && !groupQueryOf(r).startsWith(\"@keyframes\")) break;\n\t\t\ti++;\n\t\t}\n\t\tstate.baseRuleCount = i;\n\t\tstate.externalRules = new Map();\n\t\tindexExternalRules(state.sheet.cssRules);\n\t}\n\tfor (const rule of state.baseRules) insertBase(state.sheet, rule);\n\tfor (const [query, rules] of state.queryRules) {\n\t\tconst group = getOrCreateGroup(state.sheet, query);\n\t\tif (group) for (const rule of rules) insertGrouped(group, rule);\n\t}\n\treturn state.sheet;\n}\n\nfunction record(rule: string, query: string | undefined): void {\n\tconst state = getState();\n\t// Bind/replay *before* this rule joins the registry, so a fresh bind's\n\t// replay only ever covers previously-known rules — never this one twice.\n\tconst s = ensureSheet();\n\n\tif (query === undefined) {\n\t\tstate.baseRules.push(rule);\n\t} else {\n\t\tlet bucket = state.queryRules.get(query);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tstate.queryRules.set(query, bucket);\n\t\t}\n\t\tbucket.push(rule);\n\t}\n\tstate.cssTextCache = null; // a new rule changes the serialized sheet\n\n\tif (s) {\n\t\tif (query === undefined) {\n\t\t\tinsertBase(s, rule);\n\t\t} else {\n\t\t\tconst group = getOrCreateGroup(s, query);\n\t\t\tif (group) insertGrouped(group, rule);\n\t\t}\n\t}\n}\n\n/**\n * Content-addressed ~42-bit hash (two interleaved FNV-1a style passes). Class\n * names must match between server and client for SSR hydration, so a counter\n * is not an option; 42 bits keeps collision odds low (~1e-5 at 10k\n * unique declaration blocks).\n */\nexport function hash(input: string): string {\n\tlet a = 0x811c9dc5;\n\tlet b = 0x7ee3623b;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst c = input.charCodeAt(i);\n\t\ta = Math.imul(a ^ c, 0x01000193);\n\t\tb = Math.imul(b ^ c, 0x85ebca6b);\n\t}\n\treturn (a >>> 0).toString(36) + ((b >>> 9) % 1296).toString(36);\n}\n\n/** Length-prefixed (query, selector-suffix) key — unambiguous for any strings. */\nexport function contextKeyOf(query: string | undefined, suffix: string): string {\n\tconst queryPart = query === undefined ? \"-\" : \"+\" + query.length + \":\" + query;\n\treturn queryPart + suffix.length + \":\" + suffix;\n}\n\nfunction appendKeyPart(key: string, value: string): string {\n\treturn key + value.length + \":\" + value;\n}\n\nexport function expandAmpersands(input: string, replacement: string): string {\n\tlet expanded = \"\";\n\tlet quote = 0;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst code = input.charCodeAt(i);\n\t\tif (code === 92 /* \\\\ */ && i + 1 < input.length) {\n\t\t\texpanded += input[i] + input[++i];\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote !== 0) {\n\t\t\texpanded += input[i];\n\t\t\tif (code === quote) quote = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tif (code === 34 /* \" */ || code === 39 /* ' */) {\n\t\t\tquote = code;\n\t\t\texpanded += input[i];\n\t\t} else {\n\t\t\texpanded += code === 38 /* & */ ? replacement : input[i];\n\t\t}\n\t}\n\treturn expanded;\n}\n\nfunction selectorFor(className: string, suffix: string): string {\n\tconst selector = \".\" + className;\n\treturn selector + expandAmpersands(suffix, selector);\n}\n\n/**\n * True for strings usable verbatim as an unescaped CSS class selector:\n * letters/digits/`-`/`_`, not starting with a digit or `-`+digit.\n * Char-code loop rather than a RegExp — this runs on the cold mint path but\n * stays allocation-free either way.\n */\nfunction isValidClassName(name: string): boolean {\n\tif (name.length === 0 || name === \"-\") return false;\n\tfor (let i = 0; i < name.length; i++) {\n\t\tconst c = name.charCodeAt(i);\n\t\tconst ok =\n\t\t\t(c >= 97 && c <= 122) || // a-z\n\t\t\t(c >= 65 && c <= 90) || // A-Z\n\t\t\tc === 45 || c === 95 || // - _\n\t\t\t(i > 0 && c >= 48 && c <= 57); // 0-9, never leading\n\t\tif (!ok) return false;\n\t}\n\t// \"-9foo\" is not a valid identifier start either.\n\tif (name.charCodeAt(0) === 45 && name.length > 1) {\n\t\tconst second = name.charCodeAt(1);\n\t\tif (second >= 48 && second <= 57) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Mint (or reuse) the class for a declaration block in a selector context.\n *\n * Declarations keep their authored order — CSS resolves shorthand/longhand\n * and repeated-property conflicts by declaration order, so reordering (e.g.\n * alphabetical sorting) would silently change what `{ pt: 20, p: 16 }` means.\n * The cost is that the same declarations authored in a different order mint a\n * second, content-identical class — harmless, and rare in practice.\n *\n * `name` is the author's identity for the style (`css(\"app-root\", …)`). It\n * becomes part of the cache key, so two different names never share a class\n * even with identical declarations. With `exactName`, this block *is* the\n * named one and takes the name verbatim; otherwise the name is only a\n * readable prefix on a content-addressed prefix (`app-root-1a2b3c`), which is\n * how a named style's non-base contexts and its cx() merges stay identifiable\n * in devtools without ever colliding with the base class.\n */\nexport function atomBlock(\n\tquery: string | undefined,\n\tsuffix: string,\n\tdecls: ReadonlyArray<readonly [string, string]>,\n\tname?: string,\n\texactName = false,\n): string {\n\tconst state = getState();\n\tlet contextKey = contextKeyOf(query, suffix);\n\tfor (const [prop, value] of decls) {\n\t\tcontextKey = appendKeyPart(appendKeyPart(contextKey, prop), value);\n\t}\n\tif (name !== undefined && !isValidClassName(name)) {\n\t\tconsole.warn(\n\t\t\t`[nuclo] css() name ${JSON.stringify(name)} is not a valid CSS class name ` +\n\t\t\t\t\"(letters, digits, - and _ only, not starting with a digit); falling back to a generated name.\",\n\t\t);\n\t\tname = undefined;\n\t}\n\tconst declKey = name === undefined ? \"a\" + contextKey : \"n\" + appendKeyPart(\"\", name) + contextKey;\n\tlet className = state.atomCache.get(declKey);\n\tif (className !== undefined) {\n\t\t// Touch the sheet so a swapped document (tests) gets the replayed rules.\n\t\tensureSheet();\n\t\treturn className;\n\t}\n\n\tif (name === undefined) {\n\t\tclassName = \"n\" + hash(declKey);\n\t} else {\n\t\tclassName = exactName ? name : name + \"-\" + hash(declKey);\n\t\tif (state.atomMeta.has(className)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[nuclo] css() name ${JSON.stringify(className)} is already used by a different style. ` +\n\t\t\t\t\t\"Both rules are emitted under the same class, so they will override each other — rename one.\",\n\t\t\t);\n\t\t}\n\t}\n\n\tlet body = \"\";\n\tfor (const [prop, value] of decls) body += (body ? \";\" : \"\") + prop + \":\" + value;\n\tstate.atomCache.set(declKey, className);\n\tstate.atomMeta.set(className, { query, suffix, decls, name });\n\trecord(selectorFor(className, suffix) + \"{\" + body + \"}\", query);\n\treturn className;\n}\n\n/**\n * Merge two same-context generated classes into one: `second`'s declarations\n * override `first`'s per exact property (shorthand/longhand interplay is left\n * to CSS order), and the combined block mints its own content-addressed class\n * — so cx(base, override) keeps base's untouched properties instead of\n * dropping the whole base class. Deterministic: the same pair always yields\n * the same class name, on server and client alike. A named base passes its\n * name down as a prefix, so `cx(appRoot, active)` reads as `app-root-1a2b3c`.\n */\nexport function mergeBlocks(first: string, second: string): string {\n\tconst state = getState();\n\tconst a = state.atomMeta.get(first);\n\tconst b = state.atomMeta.get(second);\n\tif (!a || !b) return second; // unknown input — nothing to merge with\n\tconst concat = [...a.decls, ...b.decls];\n\t// Keep only the last occurrence of each exact property, at its position —\n\t// so a later `padding` still fully resets an earlier `padding-top`.\n\tconst lastIndex = new Map<string, number>();\n\tfor (let i = 0; i < concat.length; i++) lastIndex.set(concat[i][0], i);\n\tconst merged = concat.filter((decl, i) => lastIndex.get(decl[0]) === i);\n\treturn atomBlock(a.query, a.suffix, merged, a.name ?? b.name);\n}\n\n/** Conflict key (query|suffix) for a generated class — lets cx() group classes by selector context. */\nexport function conflictKeyOf(className: string): string | undefined {\n\tconst state = getState();\n\tconst meta = state.atomMeta.get(className);\n\treturn meta === undefined ? undefined : contextKeyOf(meta.query, meta.suffix);\n}\n\n/** Current registry generation, used to invalidate per-instance WeakMap hits after resetStyles(). */\nexport function getStyleEpoch(): number {\n\treturn (cachedState ?? globalScope[ENGINE_STATE_KEY])?.styleEpoch ?? 0;\n}\n\n/** Register a non-atomic rule (keyframes, global styles) once per dedupe key. */\nexport function addRawRule(dedupeKey: string, rule: string): void {\n\tconst state = getState();\n\tif (state.rawKeys.has(dedupeKey)) {\n\t\tensureSheet();\n\t\treturn;\n\t}\n\tstate.rawKeys.add(dedupeKey);\n\trecord(rule, undefined);\n}\n\n/** All generated CSS — base rules first, then at-rule groups in registration order. */\nexport function getCssText(): string {\n\tconst state = getState();\n\tif (state.cssTextCache !== null) return state.cssTextCache;\n\tlet out = state.baseRules.join(\"\");\n\tfor (const [query, rules] of state.queryRules) {\n\t\tif (rules.length > 0) out += query + \"{\" + rules.join(\"\") + \"}\";\n\t}\n\tstate.cssTextCache = out;\n\treturn out;\n}\n\n/** Clear all engine state (test helper). Removes the injected style element. */\nexport function resetStyles(): void {\n\tconst state = getState();\n\tstate.atomCache.clear();\n\tstate.atomMeta.clear();\n\tstate.rawKeys.clear();\n\tstate.baseRules.length = 0;\n\tstate.queryRules.clear();\n\tstate.groupRules.clear();\n\tstate.cssTextCache = null;\n\tstate.baseRuleCount = 0;\n\tstate.sheet = null;\n\tstate.sheetDocument = null;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tstate.styleEpoch++;\n\tif (typeof document !== \"undefined\") {\n\t\tdocument.getElementById(\"nuclo-styles\")?.remove();\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nuclo.ssr.mjs","names":[],"sources":["../../src/shared/strings.ts","../../src/hydration.ts","../../src/ssr/render-to-string.ts","../../src/style/engine.ts"],"sourcesContent":["/**\n * camelCase → kebab-case for CSS property (and attribute) names. A leading\n * capital becomes a leading dash, as vendor prefixes need\n * (WebkitTransition → -webkit-transition). Char-code loop with a\n * no-uppercase fast path: SSR runs this per style declaration.\n */\nexport function camelToKebab(str: string): string {\n let firstUpper = 0;\n for (; firstUpper < str.length; firstUpper++) {\n const code = str.charCodeAt(firstUpper);\n if (code >= 65 && code <= 90) break;\n }\n if (firstUpper === str.length) return str;\n\n let result = str.slice(0, firstUpper);\n for (let i = firstUpper; i < str.length; i++) {\n const code = str.charCodeAt(i);\n // A-Z is 65-90, a-z is 97-122 (difference of 32)\n result += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];\n }\n return result;\n}\n\nconst ATTR_ESCAPE_RE = /[&<>\"']/;\nconst TEXT_ESCAPE_RE = /[&<>]/;\n\n/**\n * Escapes HTML special characters in attribute values (includes \" and ')\n */\nexport function escapeHtml(text: string): string {\n // Fast path: most strings contain no special characters at all.\n const first = ATTR_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n case 34: escaped = '"'; break; // \"\n case 39: escaped = '''; break; // '\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n\n/**\n * Escapes HTML special characters in text node content.\n * Only &, < and > need escaping — quotes are safe inside text nodes.\n */\nexport function escapeText(text: string): string {\n const first = TEXT_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n","/**\n * Hydration context — tracks global hydration state and per-parent cursor positions.\n *\n * During hydration the component tree is built against an existing DOM tree\n * (produced by SSR). Instead of creating new nodes, factories claim existing\n * children from their parent element by advancing a cursor.\n *\n * The cursor is a *node pointer* (the next unclaimed child), not an index.\n * Indexed childNodes access is O(n) after any tree/attribute mutation in\n * jsdom (live NodeList re-materialization) which made hydration O(n²);\n * firstChild/nextSibling pointers are O(1) in every DOM implementation.\n */\n\nlet _hydrating = false;\n// True while renderToString() is building a tree destined for serialization.\nlet _serializing = false;\n// Missing entry = cursor at parent.firstChild; null = past the last child.\nlet _cursors = new WeakMap<Node, Node | null>();\n\nexport function isHydrating(): boolean {\n return _hydrating;\n}\n\n/**\n * True while a tree is being built for SSR serialization (inside\n * renderToString). Text children must keep their `<!-- text-N -->` markers in\n * this mode so the emitted HTML stays hydratable — even when `isBrowser` is\n * true (SSR running under jsdom, or an isomorphic worker that provides a DOM).\n * A pure client render leaves this false and skips the markers.\n */\nexport function isSerializing(): boolean {\n return _serializing;\n}\n\n/**\n * Runs `fn` with serialization mode enabled, restoring the previous state\n * afterwards (so nested/re-entrant renderToString calls behave correctly).\n */\nexport function runSerializing<T>(fn: () => T): T {\n const previous = _serializing;\n _serializing = true;\n try {\n return fn();\n } finally {\n _serializing = previous;\n }\n}\n\nexport function startHydration(): void {\n _hydrating = true;\n // Each hydration pass starts with a clean cursor slate. Stale cursors from\n // a previous pass would desynchronize claims when a container's content is\n // replaced and re-hydrated (HMR, islands re-mounting).\n _cursors = new WeakMap<Node, Node | null>();\n}\n\nexport function endHydration(): void {\n _hydrating = false;\n}\n\n/**\n * Runs a callback with hydration temporarily disabled, restoring the previous\n * state afterwards. Used when a server/client mismatch forces a subtree to be\n * rendered fresh in the middle of a hydration pass — fresh rendering must not\n * claim nodes from the surrounding SSR DOM.\n */\nexport function runWithoutHydration<T>(fn: () => T): T {\n const wasHydrating = _hydrating;\n _hydrating = false;\n try {\n return fn();\n } finally {\n _hydrating = wasHydrating;\n }\n}\n\n/**\n * Returns the next unclaimed child of a parent node (or null at the end)\n * without advancing the cursor.\n */\nexport function peekChild(parent: Node): Node | null {\n const entry = _cursors.get(parent);\n return entry === undefined ? parent.firstChild : entry;\n}\n\n/**\n * Sets the cursor to a specific child node (or null for \"past the end\").\n */\nexport function setCursor(parent: Node, node: Node | null): void {\n _cursors.set(parent, node);\n}\n\nfunction isWhitespaceText(node: Node): boolean {\n return node.nodeType === 3 && !/\\S/.test(node.textContent || '');\n}\n\n/**\n * Advances the cursor past whitespace-only text nodes.\n *\n * Nuclo's own SSR output never produces bare whitespace text nodes (text\n * children are always preceded by a `<!-- text-N -->` marker), so unmarked\n * whitespace comes from the surrounding template or an HTML\n * formatter/minifier. Skipping (without removing) keeps the cursor aligned\n * while leaving the document's visual whitespace untouched.\n */\nexport function skipWhitespaceText(parent: Node): void {\n let child = peekChild(parent);\n let advanced = false;\n while (child && isWhitespaceText(child)) {\n child = child.nextSibling;\n advanced = true;\n }\n if (advanced) {\n setCursor(parent, child);\n }\n}\n\n/**\n * Claims the next child node from parent at the current cursor position.\n * Advances the cursor by one.\n */\nexport function claimChild(parent: Node): Node | null {\n const child = peekChild(parent);\n if (child) {\n setCursor(parent, child.nextSibling);\n }\n return child;\n}\n\n/**\n * Claims a server-rendered `<!--{prefix}-start-…-->` … `<!--{prefix}-end-->`\n * block (list()/when()) at the cursor, leaving the cursor just after the start\n * marker. Returns null, claiming nothing, when the next child isn't a start\n * marker. Pairs are depth-counted because directly nested blocks share a\n * host. A missing end marker (corrupt/truncated SSR output) is recreated right\n * after the start marker and reported as `recreated`.\n */\nexport function claimMarkerPair(\n parent: Node,\n prefix: \"list\" | \"when\",\n): { start: Comment; end: Comment; recreated: boolean } | null {\n skipWhitespaceText(parent);\n const start = peekChild(parent);\n const startPrefix = prefix + \"-start-\";\n if (!start || start.nodeType !== 8 || !start.textContent?.startsWith(startPrefix)) return null;\n claimChild(parent);\n const endText = prefix + \"-end\";\n let depth = 0;\n for (let node = start.nextSibling; node; node = node.nextSibling) {\n if (node.nodeType !== 8) continue;\n const text = node.textContent;\n if (text === endText) {\n if (depth === 0) return { start: start as Comment, end: node as Comment, recreated: false };\n depth--;\n } else if (text?.startsWith(startPrefix)) {\n depth++;\n }\n }\n const end = document.createComment(endText);\n parent.insertBefore(end, start.nextSibling);\n return { start: start as Comment, end, recreated: true };\n}\n\n/**\n * Attempts to claim an existing element from the parent during hydration.\n * Returns the claimed element if the next child matches the expected tag, or null.\n */\nexport function claimElement(parent: Node, tagName: string): Element | null {\n if (!_hydrating) return null;\n skipWhitespaceText(parent);\n const candidate = peekChild(parent);\n if (candidate && candidate.nodeType === 1 && (candidate as Element).tagName.toLowerCase() === tagName) {\n return claimChild(parent) as Element;\n }\n return null;\n}\n\n/**\n * Removes unclaimed SSR children from a hydrated element.\n * Must be called after modifiers have been applied to a claimed element.\n * Removes nodes from the cursor through `lastOriginalChild` (the element's\n * last child before modifiers ran) — nodes appended by modifiers come after\n * that boundary and are preserved.\n */\nexport function cleanupUnclaimedChildren(node: Node, lastOriginalChild: Node | null): void {\n if (!lastOriginalChild) return;\n let current = peekChild(node);\n while (current) {\n const next = current === lastOriginalChild ? null : current.nextSibling;\n node.removeChild(current);\n current = next;\n }\n setCursor(node, null);\n}\n","/**\n * Server-Side Rendering (SSR) utilities for Nuclo\n * Renders Nuclo components to HTML strings in Node.js environment\n */\n\nimport { escapeHtml, escapeText, camelToKebab } from '../shared/strings';\nimport { runSerializing } from '../hydration';\nimport { NucloElement } from '../polyfill/Element';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\n\n/**\n * SVG attributes that are natively camelCase and must NOT be converted to kebab-case.\n * Most SVG attributes are already kebab-case (stroke-width, fill-rule, etc.), but a\n * handful are defined as camelCase in the SVG spec and must be preserved.\n */\nconst SVG_PRESERVE_CASE_ATTRS = new Set([\n 'viewBox', 'preserveAspectRatio', 'markerWidth', 'markerHeight',\n 'gradientTransform', 'patternTransform', 'clipPathUnits', 'gradientUnits',\n 'patternUnits', 'pathLength', 'refX', 'refY', 'stdDeviation',\n 'baseFrequency', 'numOctaves', 'kernelMatrix', 'tableValues',\n 'targetX', 'targetY', 'specularExponent', 'specularConstant',\n 'diffuseConstant', 'surfaceScale', 'xChannelSelector', 'yChannelSelector',\n 'edgeMode', 'stitchTiles', 'spreadMethod', 'patternContentUnits',\n 'markerUnits', 'startOffset', 'textLength', 'lengthAdjust',\n]);\n\n/**\n * HTML boolean attributes — presence means true, absence means false.\n * When the stored value is the string \"true\" or \"false\" (from setAttribute),\n * we must re-apply boolean semantics instead of outputting the raw string.\n */\nconst HTML_BOOLEAN_ATTRIBUTES = new Set([\n 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',\n 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'ismap', 'loop',\n 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'readonly', 'required',\n 'reversed', 'selected',\n]);\n\n/**\n * Serializes a DOM attribute value\n */\nfunction serializeAttribute(name: string, value: unknown): string {\n if (value === null || value === undefined || value === false) {\n return '';\n }\n\n if (value === true) {\n return ` ${name}`;\n }\n\n // Boolean attributes stored as strings via setAttribute() need special handling\n if (HTML_BOOLEAN_ATTRIBUTES.has(name)) {\n if (value === 'false') return '';\n if (value === 'true' || value === '' || value === name) return ` ${name}`;\n }\n\n // A style object (e.g. renderToStringWithContainer's containerAttrs from\n // untyped JS): serialize its declarations, skipping nullish/empty values.\n if (name === 'style' && typeof value === 'object') {\n let styleStr = '';\n for (const key in value as Record<string, unknown>) {\n const val = (value as Record<string, unknown>)[key];\n if (val == null || val === '') continue;\n styleStr += (styleStr ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n return styleStr ? ` style=\"${escapeHtml(styleStr)}\"` : '';\n }\n\n return ` ${name}=\"${escapeHtml(String(value))}\"`;\n}\n\n/**\n * Serializes DOM element attributes to HTML string\n *\n * `isPolyfill` is passed in by the caller (serializeNode already computed it\n * to decide whether tagName needs lowercasing) so this doesn't repeat the\n * same `Array.isArray` discriminator check per element.\n */\nfunction serializeAttributes(element: Element, isPolyfill: boolean): string {\n let result = '';\n\n // Handle polyfill elements. NucloElement keeps every child in a plain Array\n // (`children`), which a real DOM element never does (HTMLCollection) — this is\n // the same allocation-free discriminator serializeNode() uses. Reading\n // `el.attributes` directly would lazily allocate an empty Map for every\n // element being serialized, so the backing `_attributes` field is read instead.\n if (isPolyfill) {\n const el = element as any;\n const attrs = el._attributes as Map<string, string> | undefined;\n\n // id — may live on the property rather than in the Map\n if (el.id && !attrs?.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // class — kept on .className, mirrored to Map only when setAttribute is used\n if (el.className && !attrs?.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // style — lives on the backing _style object (its own properties are the\n // declarations), not in the attributes Map. Read the field directly (never\n // `el.style`) so elements that never set a style are not forced to lazily\n // allocate an empty SSRStyle just to serialize. Empty values (e.g. reactive\n // styles that resolved to undefined) are skipped.\n const styleObj = el._style as Record<string, unknown> | undefined;\n if (styleObj && !attrs?.has('style')) {\n let css = '';\n for (const key in styleObj) {\n const val = styleObj[key];\n if (val == null || val === '') continue;\n css += (css ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n if (css) result += ` style=\"${escapeHtml(css)}\"`;\n }\n\n // All remaining attributes from the Map\n // Convert camelCase ARIA/HTML attribute names (e.g. ariaLabel → aria-label) that the\n // polyfill stores as-is because NucloElement lacks the browser property mappings.\n // SVG attributes that are natively camelCase (e.g. viewBox, preserveAspectRatio) must\n // NOT be converted — they are stored via setAttribute() as-is.\n if (attrs) {\n for (const [name, value] of attrs) {\n const htmlName = SVG_PRESERVE_CASE_ATTRS.has(name) ? name : camelToKebab(name);\n result += serializeAttribute(htmlName, value);\n }\n }\n return result;\n }\n\n // Handle browser elements with NamedNodeMap attributes\n if (element.attributes && element.attributes.length) {\n for (let i = 0; i < element.attributes.length; i++) {\n const attr = element.attributes[i];\n if (attr && attr.name) {\n result += serializeAttribute(attr.name, attr.value);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Tag-category lookup, keyed by lowercase tag name. Every element in a tree\n * hits this once, so void + raw-text elements are folded into a single\n * dictionary lookup instead of two separate `Set.has()` probes (one of which\n * would always miss for the other category).\n */\nconst TAG_VOID = 1;\nconst TAG_RAW_TEXT = 2;\nconst TAG_CATEGORY: Record<string, 1 | 2> = Object.assign(Object.create(null), {\n // Self-closing HTML tags that don't have closing tags\n area: TAG_VOID, base: TAG_VOID, br: TAG_VOID, col: TAG_VOID, embed: TAG_VOID,\n hr: TAG_VOID, img: TAG_VOID, input: TAG_VOID, link: TAG_VOID, meta: TAG_VOID,\n param: TAG_VOID, source: TAG_VOID, track: TAG_VOID, wbr: TAG_VOID,\n // Raw-text elements — browsers never decode entities inside them, so their\n // text content must be emitted verbatim (escaping would corrupt inline\n // JS/CSS, e.g. `a < b` becoming `a < b` inside a script). A closing-tag\n // sequence in the content would terminate the element early, so it is\n // neutralized (see escapeRawText).\n script: TAG_RAW_TEXT, style: TAG_RAW_TEXT,\n});\n\nfunction escapeRawText(tagName: string, text: string): string {\n // \"</script\" (any case) inside a script would close it — break the sequence\n // the same way JSON serializers do (\"<\\/script\").\n return text.replace(new RegExp('</(' + tagName + ')', 'gi'), '<\\\\/$1');\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node — only & < > need escaping; quotes are safe in text content\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeText(node.textContent || '');\n }\n\n // Comment node\n if (node.nodeType === 8) { // Node.COMMENT_NODE\n return `<!--${node.textContent || ''}-->`;\n }\n\n // Element node\n if (node.nodeType === 1) { // Node.ELEMENT_NODE\n const element = node as Element;\n const rawTagName = element.tagName;\n // Duck-typed polyfill discriminator (also used by serializeAttributes): a\n // plain Array `children` field never occurs on a real\n // DOM element (HTMLCollection), only on NucloElement — and on hand-rolled\n // polyfill-shaped test doubles, which is why this alone isn't enough to\n // skip re-lowercasing tagName below.\n const isPolyfillShape = Array.isArray((element as any).children);\n // Only a *real* NucloElement is guaranteed to have pre-lowercased tagName\n // (done once in its constructor) — re-lowercasing it here on every\n // element would be pure waste, since this is the hottest function in\n // renderToString. Anything else (browser Element, or a polyfill-shaped\n // object that didn't go through `new NucloElement()`) still needs it.\n const tagName = element instanceof NucloElement ? rawTagName : rawTagName.toLowerCase();\n const attributes = serializeAttributes(element, isPolyfillShape);\n const category = TAG_CATEGORY[tagName];\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as any).children : element.childNodes;\n\n // Raw-text elements: emit text verbatim (no entity escaping, no Nuclo\n // text markers — `<!--` would act as a line comment inside a script).\n if (category === TAG_RAW_TEXT) {\n let rawContent = '';\n const rawChildren = childNodes;\n if (rawChildren && rawChildren.length > 0) {\n for (let i = 0; i < rawChildren.length; i++) {\n const child = rawChildren[i];\n if (child && child.nodeType === 3) {\n rawContent += child.textContent || '';\n }\n }\n } else {\n const tc = (node as { textContent?: unknown }).textContent;\n if (typeof tc === 'string') rawContent = tc;\n }\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n if (childNodes && childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n childrenHtml += serializeNode(child);\n }\n }\n } else {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as any).textContent;\n if (typeof tc === 'string' && tc) {\n childrenHtml = escapeText(tc);\n }\n }\n\n return `<${tagName}${attributes}>${childrenHtml}</${tagName}>`;\n }\n\n // Document fragment\n if (node.nodeType === 11) { // Node.DOCUMENT_FRAGMENT_NODE\n let result = '';\n const childNodes: ArrayLike<Node> = (node as any).childNodes ?? [];\n if (childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n result += serializeNode(child);\n }\n }\n }\n return result;\n }\n\n return '';\n}\n\n/**\n * Renders a Nuclo component to an HTML string for server-side rendering\n *\n * @param input - A Nuclo component function, DOM element, or node\n * @returns HTML string representation of the component\n *\n * @example\n * ```ts\n * import { renderToString } from 'nuclo/ssr';\n * import { div } from 'nuclo';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div>Hello, World!</div>'\n * ```\n */\nexport function renderToString(input: RenderableInput): string {\n if (!input) return '';\n\n if (typeof input === 'function') {\n try {\n // Build the tree in serialization mode so text children keep their\n // <!-- text-N --> markers (needed for hydration) even when isBrowser is\n // true, e.g. SSR running under jsdom.\n const element = runSerializing(() => {\n if (typeof document === 'undefined') throw new Error('Document is not available. Make sure polyfills are loaded.');\n return input(document.createElement('div') as unknown as ExpandedElement<ElementTagName>, 0);\n });\n return element && typeof element === 'object' && 'nodeType' in element ? serializeNode(element as Node) : '';\n } catch (error) {\n console.error('Error rendering component to string:', error);\n return '';\n }\n }\n\n if ('nodeType' in input) {\n return serializeNode(input as Node);\n }\n\n return '';\n}\n\n/**\n * Renders multiple Nuclo components to HTML strings\n *\n * @param inputs - Array of Nuclo components\n * @returns Array of HTML strings\n */\nexport function renderManyToString(inputs: RenderableInput[]): string[] {\n return inputs.map(input => renderToString(input));\n}\n\n/**\n * Renders a Nuclo component and wraps it in a container element\n *\n * @param input - A Nuclo component\n * @param containerTag - The tag name for the container (default: 'div')\n * @param containerAttrs - Attributes for the container element\n * @returns HTML string with container wrapper\n */\nexport function renderToStringWithContainer(\n input: RenderableInput,\n containerTag: string = 'div',\n containerAttrs: Record<string, string> = {}\n): string {\n const content = renderToString(input);\n const attrs = Object.entries(containerAttrs)\n .map(([key, value]) => serializeAttribute(key, value))\n .join('');\n\n return `<${containerTag}${attrs}>${content}</${containerTag}>`;\n}\n","/**\n * Shared generated-CSS engine state.\n *\n * One stylesheet, one rule registry, shared by every `createCss` instance so\n * identical declarations dedupe globally and cascade order stays deterministic:\n * base rules first (insertion order), then one grouping rule per at-rule query\n * in registration order (theme `screens` order).\n *\n * Browser: rules are injected through CSSOM with O(1) appends — no rescans.\n * SSR: rules accumulate in the registry; `getCssText()` serializes them.\n *\n * The state below lives on `globalThis`, keyed by a `Symbol.for(...)` (the\n * global symbol registry, shared by every realm-local copy of this module).\n * `nuclo` publishes `.`, `./ssr`, and `./polyfill` as independently built\n * bundle files (see tsdown.config.ts) — a consumer that resolves them\n * through normal package resolution (any bundler/runtime that doesn't\n * alias every subpath to this same source file) loads two or three\n * separate copies of this module, each with its own module-scope\n * variables. Without a realm-wide singleton, `css()` (from `nuclo`) would\n * mint rules into one copy's registry while `getCssText()` (from\n * `nuclo/ssr`) reads an entirely different, always-empty one — SSR output\n * would render the right classes with no matching CSS at all. Storing the registry on `globalThis` instead makes\n * every copy of this module share the same state regardless of how many\n * times it ends up bundled.\n *\n * The registry never shrinks on its own: atomCache/atomMeta/rawKeys/\n * baseRules/queryRules retain every rule for the life of the process —\n * content-addressed dedup needs a durable record of what each class means,\n * and the engine has no way to know whether a minted class still backs a\n * live DOM node (it's deliberately decoupled from element/list lifecycle),\n * so it can't safely evict. Only resetStyles() (a manual test helper)\n * clears it. This is fine for the intended use — a bounded design-token\n * vocabulary — but a caller piping truly per-instance dynamic values\n * through css({ raw: {...} }) (e.g. per-row data in a list) mints one\n * permanent class per unique value; prefer the `style` prop\n * (element/inline-style.ts) for values that vary per instance rather than\n * per design token. See test/style/registry-growth.test.ts.\n */\n\nexport interface BlockMeta {\n\treadonly query: string | undefined;\n\treadonly suffix: string;\n\treadonly decls: ReadonlyArray<readonly [string, string]>;\n\t/** Author-supplied identity from `css(name, …)`; inherited by cx() merges. */\n\treadonly name: string | undefined;\n}\n\ninterface EngineState {\n\t// Registry — the source of truth for all generated CSS.\n\t// atomCache: declaration-block key -> class name (dedup)\n\t// atomMeta: class name -> what the class means (its selector context and its\n\t// declarations). This is what lets cx() merge two same-context classes at the\n\t// declaration level instead of guessing: the engine can always recover the\n\t// exact content of any class it minted.\n\tatomCache: Map<string, string>;\n\tatomMeta: Map<string, BlockMeta>;\n\trawKeys: Set<string>;\n\tbaseRules: string[];\n\tqueryRules: Map<string, string[]>;\n\tstyleEpoch: number;\n\n\t// Memoized getCssText() output. SSR calls getCssText() once per request, but the\n\t// rule set is stable after warmup — so the serialized sheet is recomputed only\n\t// when a new rule is actually recorded. Invalidated in record() and resetStyles().\n\tcssTextCache: string | null;\n\n\t// Browser CSSOM state. Base rules occupy [0, baseRuleCount); grouping rules\n\t// (one per query, created in registration order) live after them. CSSRule\n\t// references stay live when indices shift, so insertion never rescans.\n\tsheet: CSSStyleSheet | null;\n\tsheetDocument: Document | null;\n\tbaseRuleCount: number;\n\tgroupRules: Map<string, CSSGroupingRule>;\n\n\t// Normalized rules already sitting in a freshly-adopted stylesheet that this\n\t// module didn't insert itself — e.g. server-rendered CSS being hydrated.\n\t// Rebuilt on every fresh bind in ensureSheet(); null when there is nothing\n\t// external to guard against.\n\texternalRules: Map<string, Set<string>> | null;\n\thasExternalGroups: boolean;\n}\n\nfunction createState(): EngineState {\n\treturn {\n\t\tatomCache: new Map(),\n\t\tatomMeta: new Map(),\n\t\trawKeys: new Set(),\n\t\tbaseRules: [],\n\t\tqueryRules: new Map(),\n\t\tstyleEpoch: 0,\n\t\tcssTextCache: null,\n\t\tsheet: null,\n\t\tsheetDocument: null,\n\t\tbaseRuleCount: 0,\n\t\tgroupRules: new Map(),\n\t\texternalRules: null,\n\t\thasExternalGroups: false,\n\t};\n}\n\nconst ENGINE_STATE_KEY = Symbol.for(\"nuclo.style.engine.v1\");\nconst globalScope = globalThis as unknown as Record<symbol, EngineState>;\n// Importing the runtime does not allocate the registry until styling is used.\nlet cachedState: EngineState | undefined;\nfunction getState(): EngineState {\n\treturn cachedState ??= globalScope[ENGINE_STATE_KEY] ?? (globalScope[ENGINE_STATE_KEY] = createState());\n}\n\n/** Pre-register at-rule queries so their cascade order follows theme order. */\nexport function registerQueries(queries: Iterable<string>): void {\n\tfor (const query of queries) {\n\t\tconst state = getState();\n\t\tif (!state.queryRules.has(query)) state.queryRules.set(query, []);\n\t}\n}\n\nfunction createGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\ttry {\n\t\tconst idx = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", idx);\n\t\tconst group = s.cssRules[idx] as CSSGroupingRule;\n\t\tstate.groupRules.set(query, group);\n\t\tstate.groupRules.set(groupQueryOf(group), group);\n\t\treturn group;\n\t} catch {\n\t\treturn null; // unsupported at-rule in this environment\n\t}\n}\n\n// A group prelude is stable while its children grow. Reading cssText on each\n// insertion would serialize all previous children and make loading quadratic.\n// Weak keys allow detached stylesheets to be collected after reset/rebinding.\nconst groupQueries = new WeakMap<CSSRule, string>();\nfunction groupQueryOf(rule: CSSRule): string {\n\tconst cached = groupQueries.get(rule);\n\tif (cached !== undefined) return cached;\n\tconst text = rule.cssText;\n\tconst brace = text.indexOf(\"{\");\n\tconst query = (brace === -1 ? text : text.slice(0, brace)).trim();\n\tgroupQueries.set(rule, query);\n\treturn query;\n}\n\nfunction ruleIdentity(rule: CSSRule): string {\n\treturn rule instanceof CSSStyleRule ? rule.selectorText : groupQueryOf(rule);\n}\n\nfunction rememberExternalRule(rule: CSSRule, query: string | undefined): void {\n\tconst state = getState();\n\tif (!state.externalRules) return;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tlet texts = state.externalRules.get(key);\n\tif (!texts) {\n\t\ttexts = new Set();\n\t\tstate.externalRules.set(key, texts);\n\t}\n\ttexts.add(rule.cssText);\n}\n\nfunction consumeExternalRule(rule: CSSRule, query: string | undefined): boolean {\n\tconst state = getState();\n\tif (!state.externalRules) return false;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tconst texts = state.externalRules.get(key);\n\tif (!texts?.delete(rule.cssText)) return false;\n\tif (texts.size === 0) state.externalRules.delete(key);\n\tif (state.externalRules.size === 0) state.externalRules = null;\n\treturn true;\n}\n\n/**\n * Index an already-populated stylesheet found on adopt (SSR output, most\n * commonly): every top-level conditional grouping rule (@media/@container/…)\n * is registered into groupRules (by its reconstructed at-rule text) so later\n * record() calls append into it instead of creating a sibling duplicate. The\n * other rules are indexed by scope, selector/prelude, and normalized cssText\n * so exact server/client matches can be skipped without hiding a changed\n * named or global rule that happens to reuse the same selector.\n */\nfunction indexExternalRules(rules: CSSRuleList): void {\n\tconst state = getState();\n\tfor (let i = 0; i < rules.length; i++) {\n\t\tconst rule = rules[i];\n\t\tif (rule instanceof CSSStyleRule) {\n\t\t\trememberExternalRule(rule, undefined);\n\t\t\tcontinue;\n\t\t}\n\t\tconst prelude = groupQueryOf(rule);\n\t\tif (\"cssRules\" in rule && !prelude.startsWith(\"@keyframes\")) {\n\t\t\tconst group = rule as CSSGroupingRule;\n\t\t\tstate.hasExternalGroups = true;\n\t\t\tstate.groupRules.set(prelude, group);\n\t\t\tfor (let j = 0; j < group.cssRules.length; j++) {\n\t\t\t\trememberExternalRule(group.cssRules[j], prelude);\n\t\t\t}\n\t\t} else {\n\t\t\t// @keyframes, @font-face, … — one-shot rules matched by prelude.\n\t\t\trememberExternalRule(rule, undefined);\n\t\t}\n\t}\n}\n\nfunction insertBase(s: CSSStyleSheet, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\ts.insertRule(rule, state.baseRuleCount);\n\t\tconst inserted = s.cssRules[state.baseRuleCount];\n\t\tif (consumeExternalRule(inserted, undefined)) {\n\t\t\ts.deleteRule(state.baseRuleCount);\n\t\t\treturn;\n\t\t}\n\t\tstate.baseRuleCount++;\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction insertGrouped(group: CSSGroupingRule, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\tconst index = group.cssRules.length;\n\t\tgroup.insertRule(rule, index);\n\t\tconst inserted = group.cssRules[index];\n\t\tif (state.externalRules && consumeExternalRule(inserted, groupQueryOf(group))) group.deleteRule(index);\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction getOrCreateGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\tconst direct = state.groupRules.get(query);\n\tif (direct) return direct;\n\tif (!state.hasExternalGroups) return createGroup(s, query);\n\n\t// CSSOM may normalize whitespace in an at-rule prelude. Normalize the\n\t// authored query through a temporary empty group before deciding that an\n\t// adopted SSR group is missing.\n\ttry {\n\t\tconst index = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", index);\n\t\tconst normalized = groupQueryOf(s.cssRules[index]);\n\t\ts.deleteRule(index);\n\t\tconst existing = state.groupRules.get(normalized);\n\t\tif (existing) {\n\t\t\tstate.groupRules.set(query, existing);\n\t\t\treturn existing;\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n\treturn createGroup(s, query);\n}\n\n/**\n * Returns the live stylesheet, (re)binding when the document changes — e.g.\n * fresh jsdom documents between tests. Rebinding replays the whole registry,\n * which replaces v1's per-call classExistsInDOM() DOM scans with a single\n * replay per document.\n */\nexport function ensureSheet(): CSSStyleSheet | null {\n\tconst state = getState();\n\tif (typeof document === \"undefined\") return null;\n\t// The nuclo SSR polyfill provides a document without getElementById — in\n\t// that context rules accumulate in the registry for getCssText() only.\n\tif (typeof document.getElementById !== \"function\") return null;\n\tif (\n\t\tstate.sheet &&\n\t\tstate.sheetDocument === document &&\n\t\tstate.sheet.ownerNode &&\n\t\t(state.sheet.ownerNode as Element).isConnected\n\t) {\n\t\treturn state.sheet;\n\t}\n\tconst existing = document.getElementById(\"nuclo-styles\");\n\tlet el: HTMLStyleElement;\n\tif (existing?.tagName === \"STYLE\") {\n\t\tel = existing as HTMLStyleElement;\n\t} else {\n\t\tel = document.createElement(\"style\");\n\t\tel.id = \"nuclo-styles\";\n\t\tif (existing) existing.replaceWith(el);\n\t\telse document.head.appendChild(el);\n\t}\n\tstate.sheet = el.sheet;\n\tstate.sheetDocument = document;\n\tstate.groupRules.clear();\n\tstate.baseRuleCount = 0;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tif (!state.sheet) return null;\n\tif (state.sheet.cssRules.length > 0) {\n\t\t// Leading top-level non-group rules (style rules, @keyframes — before\n\t\t// the first @media/@container group) are this element's existing\n\t\t// \"base\" region — count them so insertBase() keeps inserting new base\n\t\t// rules right after them, still ahead of every group.\n\t\tlet i = 0;\n\t\twhile (i < state.sheet.cssRules.length) {\n\t\t\tconst r = state.sheet.cssRules[i];\n\t\t\tif (!(r instanceof CSSStyleRule) && \"cssRules\" in r && !groupQueryOf(r).startsWith(\"@keyframes\")) break;\n\t\t\ti++;\n\t\t}\n\t\tstate.baseRuleCount = i;\n\t\tstate.externalRules = new Map();\n\t\tindexExternalRules(state.sheet.cssRules);\n\t}\n\tfor (const rule of state.baseRules) insertBase(state.sheet, rule);\n\tfor (const [query, rules] of state.queryRules) {\n\t\tconst group = getOrCreateGroup(state.sheet, query);\n\t\tif (group) for (const rule of rules) insertGrouped(group, rule);\n\t}\n\treturn state.sheet;\n}\n\nfunction record(rule: string, query: string | undefined): void {\n\tconst state = getState();\n\t// Bind/replay *before* this rule joins the registry, so a fresh bind's\n\t// replay only ever covers previously-known rules — never this one twice.\n\tconst s = ensureSheet();\n\n\tif (query === undefined) {\n\t\tstate.baseRules.push(rule);\n\t} else {\n\t\tlet bucket = state.queryRules.get(query);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tstate.queryRules.set(query, bucket);\n\t\t}\n\t\tbucket.push(rule);\n\t}\n\tstate.cssTextCache = null; // a new rule changes the serialized sheet\n\n\tif (s) {\n\t\tif (query === undefined) {\n\t\t\tinsertBase(s, rule);\n\t\t} else {\n\t\t\tconst group = getOrCreateGroup(s, query);\n\t\t\tif (group) insertGrouped(group, rule);\n\t\t}\n\t}\n}\n\n/**\n * Content-addressed ~42-bit hash (two interleaved FNV-1a style passes). Class\n * names must match between server and client for SSR hydration, so a counter\n * is not an option; 42 bits keeps collision odds low (~1e-5 at 10k\n * unique declaration blocks).\n */\nexport function hash(input: string): string {\n\tlet a = 0x811c9dc5;\n\tlet b = 0x7ee3623b;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst c = input.charCodeAt(i);\n\t\ta = Math.imul(a ^ c, 0x01000193);\n\t\tb = Math.imul(b ^ c, 0x85ebca6b);\n\t}\n\treturn (a >>> 0).toString(36) + ((b >>> 9) % 1296).toString(36);\n}\n\n/** Length-prefixed (query, selector-suffix) key — unambiguous for any strings. */\nexport function contextKeyOf(query: string | undefined, suffix: string): string {\n\tconst queryPart = query === undefined ? \"-\" : \"+\" + query.length + \":\" + query;\n\treturn queryPart + suffix.length + \":\" + suffix;\n}\n\nfunction appendKeyPart(key: string, value: string): string {\n\treturn key + value.length + \":\" + value;\n}\n\nexport function expandAmpersands(input: string, replacement: string): string {\n\tlet expanded = \"\";\n\tlet quote = 0;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst code = input.charCodeAt(i);\n\t\tif (code === 92 /* \\\\ */ && i + 1 < input.length) {\n\t\t\texpanded += input[i] + input[++i];\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote !== 0) {\n\t\t\texpanded += input[i];\n\t\t\tif (code === quote) quote = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tif (code === 34 /* \" */ || code === 39 /* ' */) {\n\t\t\tquote = code;\n\t\t\texpanded += input[i];\n\t\t} else {\n\t\t\texpanded += code === 38 /* & */ ? replacement : input[i];\n\t\t}\n\t}\n\treturn expanded;\n}\n\nfunction selectorFor(className: string, suffix: string): string {\n\tconst selector = \".\" + className;\n\treturn selector + expandAmpersands(suffix, selector);\n}\n\n/**\n * True for strings usable verbatim as an unescaped CSS class selector:\n * letters/digits/`-`/`_`, not starting with a digit or `-`+digit.\n * Char-code loop rather than a RegExp — this runs on the cold mint path but\n * stays allocation-free either way.\n */\nfunction isValidClassName(name: string): boolean {\n\tif (name.length === 0 || name === \"-\") return false;\n\tfor (let i = 0; i < name.length; i++) {\n\t\tconst c = name.charCodeAt(i);\n\t\tconst ok =\n\t\t\t(c >= 97 && c <= 122) || // a-z\n\t\t\t(c >= 65 && c <= 90) || // A-Z\n\t\t\tc === 45 || c === 95 || // - _\n\t\t\t(i > 0 && c >= 48 && c <= 57); // 0-9, never leading\n\t\tif (!ok) return false;\n\t}\n\t// \"-9foo\" is not a valid identifier start either.\n\tif (name.charCodeAt(0) === 45 && name.length > 1) {\n\t\tconst second = name.charCodeAt(1);\n\t\tif (second >= 48 && second <= 57) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Mint (or reuse) the class for a declaration block in a selector context.\n *\n * Declarations keep their authored order — CSS resolves shorthand/longhand\n * and repeated-property conflicts by declaration order, so reordering (e.g.\n * alphabetical sorting) would silently change what `{ pt: 20, p: 16 }` means.\n * The cost is that the same declarations authored in a different order mint a\n * second, content-identical class — harmless, and rare in practice.\n *\n * `name` is the author's identity for the style (`css(\"app-root\", …)`). It\n * becomes part of the cache key, so two different names never share a class\n * even with identical declarations. With `exactName`, this block *is* the\n * named one and takes the name verbatim; otherwise the name is only a\n * readable prefix on a content-addressed prefix (`app-root-1a2b3c`), which is\n * how a named style's non-base contexts and its cx() merges stay identifiable\n * in devtools without ever colliding with the base class.\n */\nexport function atomBlock(\n\tquery: string | undefined,\n\tsuffix: string,\n\tdecls: ReadonlyArray<readonly [string, string]>,\n\tname?: string,\n\texactName = false,\n): string {\n\tconst state = getState();\n\tlet contextKey = contextKeyOf(query, suffix);\n\tfor (const [prop, value] of decls) {\n\t\tcontextKey = appendKeyPart(appendKeyPart(contextKey, prop), value);\n\t}\n\tif (name !== undefined && !isValidClassName(name)) {\n\t\tconsole.warn(\n\t\t\t`[nuclo] css() name ${JSON.stringify(name)} is not a valid CSS class name ` +\n\t\t\t\t\"(letters, digits, - and _ only, not starting with a digit); falling back to a generated name.\",\n\t\t);\n\t\tname = undefined;\n\t}\n\tconst declKey = name === undefined ? \"a\" + contextKey : \"n\" + appendKeyPart(\"\", name) + contextKey;\n\tlet className = state.atomCache.get(declKey);\n\tif (className !== undefined) {\n\t\t// Touch the sheet so a swapped document (tests) gets the replayed rules.\n\t\tensureSheet();\n\t\treturn className;\n\t}\n\n\tif (name === undefined) {\n\t\tclassName = \"n\" + hash(declKey);\n\t} else {\n\t\tclassName = exactName ? name : name + \"-\" + hash(declKey);\n\t\tif (state.atomMeta.has(className)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[nuclo] css() name ${JSON.stringify(className)} is already used by a different style. ` +\n\t\t\t\t\t\"Both rules are emitted under the same class, so they will override each other — rename one.\",\n\t\t\t);\n\t\t}\n\t}\n\n\tlet body = \"\";\n\tfor (const [prop, value] of decls) body += (body ? \";\" : \"\") + prop + \":\" + value;\n\tstate.atomCache.set(declKey, className);\n\tstate.atomMeta.set(className, { query, suffix, decls, name });\n\trecord(selectorFor(className, suffix) + \"{\" + body + \"}\", query);\n\treturn className;\n}\n\n/**\n * Merge two same-context generated classes into one: `second`'s declarations\n * override `first`'s per exact property (shorthand/longhand interplay is left\n * to CSS order), and the combined block mints its own content-addressed class\n * — so cx(base, override) keeps base's untouched properties instead of\n * dropping the whole base class. Deterministic: the same pair always yields\n * the same class name, on server and client alike. A named base passes its\n * name down as a prefix, so `cx(appRoot, active)` reads as `app-root-1a2b3c`.\n */\nexport function mergeBlocks(first: string, second: string): string {\n\tconst state = getState();\n\tconst a = state.atomMeta.get(first);\n\tconst b = state.atomMeta.get(second);\n\tif (!a || !b) return second; // unknown input — nothing to merge with\n\tconst concat = [...a.decls, ...b.decls];\n\t// Keep only the last occurrence of each exact property, at its position —\n\t// so a later `padding` still fully resets an earlier `padding-top`.\n\tconst lastIndex = new Map<string, number>();\n\tfor (let i = 0; i < concat.length; i++) lastIndex.set(concat[i][0], i);\n\tconst merged = concat.filter((decl, i) => lastIndex.get(decl[0]) === i);\n\treturn atomBlock(a.query, a.suffix, merged, a.name ?? b.name);\n}\n\n/** Conflict key (query|suffix) for a generated class — lets cx() group classes by selector context. */\nexport function conflictKeyOf(className: string): string | undefined {\n\tconst state = getState();\n\tconst meta = state.atomMeta.get(className);\n\treturn meta === undefined ? undefined : contextKeyOf(meta.query, meta.suffix);\n}\n\n/** Current registry generation, used to invalidate per-instance WeakMap hits after resetStyles(). */\nexport function getStyleEpoch(): number {\n\treturn (cachedState ?? globalScope[ENGINE_STATE_KEY])?.styleEpoch ?? 0;\n}\n\n/** Register a non-atomic rule (keyframes, global styles) once per dedupe key. */\nexport function addRawRule(dedupeKey: string, rule: string): void {\n\tconst state = getState();\n\tif (state.rawKeys.has(dedupeKey)) {\n\t\tensureSheet();\n\t\treturn;\n\t}\n\tstate.rawKeys.add(dedupeKey);\n\trecord(rule, undefined);\n}\n\n/** All generated CSS — base rules first, then at-rule groups in registration order. */\nexport function getCssText(): string {\n\tconst state = getState();\n\tif (state.cssTextCache !== null) return state.cssTextCache;\n\tlet out = state.baseRules.join(\"\");\n\tfor (const [query, rules] of state.queryRules) {\n\t\tif (rules.length > 0) out += query + \"{\" + rules.join(\"\") + \"}\";\n\t}\n\tstate.cssTextCache = out;\n\treturn out;\n}\n\n/** Clear all engine state (test helper). Removes the injected style element. */\nexport function resetStyles(): void {\n\tconst state = getState();\n\tstate.atomCache.clear();\n\tstate.atomMeta.clear();\n\tstate.rawKeys.clear();\n\tstate.baseRules.length = 0;\n\tstate.queryRules.clear();\n\tstate.groupRules.clear();\n\tstate.cssTextCache = null;\n\tstate.baseRuleCount = 0;\n\tstate.sheet = null;\n\tstate.sheetDocument = null;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tstate.styleEpoch++;\n\tif (typeof document !== \"undefined\") {\n\t\tdocument.getElementById(\"nuclo-styles\")?.remove();\n\t}\n}\n"],"mappings":"4CAMA,SAAgB,EAAa,EAAqB,CAChD,IAAI,EAAa,EACjB,KAAO,EAAa,EAAI,OAAQ,IAAc,CAC5C,IAAM,EAAO,EAAI,WAAW,CAAU,EACtC,GAAI,GAAQ,IAAM,GAAQ,GAAI,KAChC,CACA,GAAI,IAAe,EAAI,OAAQ,OAAO,EAEtC,IAAI,EAAS,EAAI,MAAM,EAAG,CAAU,EACpC,IAAK,IAAI,EAAI,EAAY,EAAI,EAAI,OAAQ,IAAK,CAC5C,IAAM,EAAO,EAAI,WAAW,CAAC,EAE7B,GAAU,GAAQ,IAAM,GAAQ,GAAK,IAAM,OAAO,aAAa,EAAO,EAAE,EAAI,EAAI,EAClF,CACA,OAAO,CACT,CAEA,MAAM,EAAiB,UACjB,EAAiB,QAKvB,SAAgB,EAAW,EAAsB,CAE/C,IAAM,EAAQ,EAAe,KAAK,CAAI,EACtC,GAAI,CAAC,EAAO,OAAO,EAEnB,IAAI,EAAS,GACT,EAAO,EACX,IAAK,IAAI,EAAI,EAAM,MAAO,EAAI,EAAK,OAAQ,IAAK,CAC9C,IAAI,EACJ,OAAQ,EAAK,WAAW,CAAC,EAAzB,CACE,IAAK,IAAI,EAAU,QAAS,MAC5B,IAAK,IAAI,EAAU,OAAQ,MAC3B,IAAK,IAAI,EAAU,OAAQ,MAC3B,IAAK,IAAI,EAAU,SAAU,MAC7B,IAAK,IAAI,EAAU,SAAU,MAC7B,QAAS,QACX,CACA,GAAU,EAAK,MAAM,EAAM,CAAC,EAAI,EAChC,EAAO,EAAI,CACb,CACA,OAAO,EAAS,EAAK,MAAM,CAAI,CACjC,CAMA,SAAgB,EAAW,EAAsB,CAC/C,IAAM,EAAQ,EAAe,KAAK,CAAI,EACtC,GAAI,CAAC,EAAO,OAAO,EAEnB,IAAI,EAAS,GACT,EAAO,EACX,IAAK,IAAI,EAAI,EAAM,MAAO,EAAI,EAAK,OAAQ,IAAK,CAC9C,IAAI,EACJ,OAAQ,EAAK,WAAW,CAAC,EAAzB,CACE,IAAK,IAAI,EAAU,QAAS,MAC5B,IAAK,IAAI,EAAU,OAAQ,MAC3B,IAAK,IAAI,EAAU,OAAQ,MAC3B,QAAS,QACX,CACA,GAAU,EAAK,MAAM,EAAM,CAAC,EAAI,EAChC,EAAO,EAAI,CACb,CACA,OAAO,EAAS,EAAK,MAAM,CAAI,CACjC,CC3DA,IAAI,EAAe,GAuBnB,SAAgB,EAAkB,EAAgB,CAChD,IAAM,EAAW,EACjB,EAAe,GACf,GAAI,CACF,OAAO,EAAG,CACZ,QAAU,CACR,EAAe,CACjB,CACF,CCzBA,MAAM,kBAA0B,IAAI,IAAI,ucASxC,CAAC,EAOK,kBAA0B,IAAI,IAAI,CACtC,kBAAmB,QAAS,YAAa,WAAY,UAAW,WAChE,UAAW,QAAS,WAAY,iBAAkB,SAAU,QAAS,OACrE,WAAY,QAAS,WAAY,aAAc,OAAQ,WAAY,WACnE,WAAY,UACd,CAAC,EAKD,SAAS,EAAmB,EAAc,EAAwB,CAChE,GAAI,GAAU,MAA+B,IAAU,GACrD,MAAO,GAGT,GAAI,IAAU,GACZ,MAAO,IAAI,IAIb,GAAI,EAAwB,IAAI,CAAI,EAAG,CACrC,GAAI,IAAU,QAAS,MAAO,GAC9B,GAAI,IAAU,QAAU,IAAU,IAAM,IAAU,EAAM,MAAO,IAAI,GACrE,CAIA,GAAI,IAAS,SAAW,OAAO,GAAU,SAAU,CACjD,IAAI,EAAW,GACf,IAAK,IAAM,KAAO,EAAkC,CAClD,IAAM,EAAO,EAAkC,GAC3C,GAAO,MAAQ,IAAQ,KAC3B,IAAa,EAAW,IAAM,IAAM,EAAa,CAAG,EAAI,KAAO,OAAO,CAAG,EAAI,IAC/E,CACA,OAAO,EAAW,WAAW,EAAW,CAAQ,EAAE,GAAK,EACzD,CAEA,MAAO,IAAI,EAAK,IAAI,EAAW,OAAO,CAAK,CAAC,EAAE,EAChD,CASA,SAAS,EAAoB,EAAkB,EAA6B,CAC1E,IAAI,EAAS,GAOb,GAAI,EAAY,CACd,IAAM,EAAK,EACL,EAAQ,EAAG,YAGb,EAAG,IAAM,CAAC,GAAO,IAAI,IAAI,IAC3B,GAAU,EAAmB,KAAM,EAAG,EAAE,GAItC,EAAG,WAAa,CAAC,GAAO,IAAI,OAAO,IACrC,GAAU,EAAmB,QAAS,EAAG,SAAS,GAQpD,IAAM,EAAW,EAAG,OACpB,GAAI,GAAY,CAAC,GAAO,IAAI,OAAO,EAAG,CACpC,IAAI,EAAM,GACV,IAAK,IAAM,KAAO,EAAU,CAC1B,IAAM,EAAM,EAAS,GACjB,GAAO,MAAQ,IAAQ,KAC3B,IAAQ,EAAM,IAAM,IAAM,EAAa,CAAG,EAAI,KAAO,OAAO,CAAG,EAAI,IACrE,CACI,IAAK,GAAU,WAAW,EAAW,CAAG,EAAE,GAChD,CAOA,GAAI,EACF,IAAK,GAAM,CAAC,EAAM,KAAU,EAAO,CACjC,IAAM,EAAW,EAAwB,IAAI,CAAI,EAAI,EAAO,EAAa,CAAI,EAC7E,GAAU,EAAmB,EAAU,CAAK,CAC9C,CAEF,OAAO,CACT,CAGA,GAAI,EAAQ,YAAc,EAAQ,WAAW,OAC3C,IAAK,IAAI,EAAI,EAAG,EAAI,EAAQ,WAAW,OAAQ,IAAK,CAClD,IAAM,EAAO,EAAQ,WAAW,GAC5B,GAAQ,EAAK,OACf,GAAU,EAAmB,EAAK,KAAM,EAAK,KAAK,EAEtD,CAGF,OAAO,CACT,CAQA,MAEM,EAAsC,OAAO,OAAO,OAAO,OAAO,IAAI,EAAG,CAE7E,KAAM,EAAU,KAAM,EAAU,GAAI,EAAU,IAAK,EAAU,MAAO,EACpE,GAAI,EAAU,IAAK,EAAU,MAAO,EAAU,KAAM,EAAU,KAAM,EACpE,MAAO,EAAU,OAAQ,EAAU,MAAO,EAAU,IAAK,EAMzD,OAAQ,EAAc,MAAO,CAC/B,CAAC,EAED,SAAS,EAAc,EAAiB,EAAsB,CAG5D,OAAO,EAAK,QAAY,OAAO,MAAQ,EAAU,IAAK,IAAI,EAAG,QAAQ,CACvE,CAKA,SAAS,EAAc,EAAoB,CAEzC,GAAI,EAAK,WAAa,EACpB,OAAO,EAAW,EAAK,aAAe,EAAE,EAI1C,GAAI,EAAK,WAAa,EACpB,MAAO,OAAO,EAAK,aAAe,GAAG,KAIvC,GAAI,EAAK,WAAa,EAAG,CACvB,IAAM,EAAU,EACV,EAAa,EAAQ,QAMrB,EAAkB,MAAM,QAAS,EAAgB,QAAQ,EAMzD,EAAU,aAAmB,EAAe,EAAa,EAAW,YAAY,EAChF,EAAa,EAAoB,EAAS,CAAe,EACzD,EAAW,EAAa,GAG9B,GAAI,IAAa,EACf,MAAO,IAAI,IAAU,EAAW,KAGlC,IAAM,EAA8B,EAAmB,EAAgB,SAAW,EAAQ,WAI1F,GAAI,IAAa,EAAc,CAC7B,IAAI,EAAa,GACX,EAAc,EACpB,GAAI,GAAe,EAAY,OAAS,EACtC,IAAK,IAAI,EAAI,EAAG,EAAI,EAAY,OAAQ,IAAK,CAC3C,IAAM,EAAQ,EAAY,GACtB,GAAS,EAAM,WAAa,IAC9B,GAAc,EAAM,aAAe,GAEvC,KACK,CACL,IAAM,EAAM,EAAmC,YAC3C,OAAO,GAAO,WAAU,EAAa,EAC3C,CACA,MAAO,IAAI,IAAU,EAAW,GAAG,EAAc,EAAS,CAAU,EAAE,IAAI,EAAQ,EACpF,CAGA,IAAI,EAAe,GACnB,GAAI,GAAc,EAAW,OAAS,EACpC,IAAK,IAAI,EAAI,EAAG,EAAI,EAAW,OAAQ,IAAK,CAC1C,IAAM,EAAQ,EAAW,GACrB,IACF,GAAgB,EAAc,CAAK,EAEvC,KACK,CAEL,IAAM,EAAM,EAAa,YACrB,OAAO,GAAO,UAAY,IAC5B,EAAe,EAAW,CAAE,EAEhC,CAEA,MAAO,IAAI,IAAU,EAAW,GAAG,EAAa,IAAI,EAAQ,EAC9D,CAGA,GAAI,EAAK,WAAa,GAAI,CACxB,IAAI,EAAS,GACP,EAA+B,EAAa,YAAc,CAAC,EACjE,GAAI,EAAW,OAAS,EACtB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAW,OAAQ,IAAK,CAC1C,IAAM,EAAQ,EAAW,GACrB,IACF,GAAU,EAAc,CAAK,EAEjC,CAEF,OAAO,CACT,CAEA,MAAO,EACT,CAmBA,SAAgB,EAAe,EAAgC,CAC7D,GAAI,CAAC,EAAO,MAAO,GAEnB,GAAI,OAAO,GAAU,WACnB,GAAI,CAIF,IAAM,EAAU,MAAqB,CACnC,GAAI,OAAO,SAAa,IAAa,MAAU,MAAM,4DAA4D,EACjH,OAAO,EAAM,SAAS,cAAc,KAAK,EAAiD,CAAC,CAC7F,CAAC,EACD,OAAO,GAAW,OAAO,GAAY,UAAY,aAAc,EAAU,EAAc,CAAe,EAAI,EAC5G,OAAS,EAAO,CAEd,OADA,QAAQ,MAAM,uCAAwC,CAAK,EACpD,EACT,CAOF,MAJI,aAAc,EACT,EAAc,CAAa,EAG7B,EACT,CAQA,SAAgB,EAAmB,EAAqC,CACtE,OAAO,EAAO,IAAI,GAAS,EAAe,CAAK,CAAC,CAClD,CAUA,SAAgB,EACd,EACA,EAAuB,MACvB,EAAyC,CAAC,EAClC,CACR,IAAM,EAAU,EAAe,CAAK,EAKpC,MAAO,IAAI,IAJG,OAAO,QAAQ,CAAc,CAAC,CACzC,KAAK,CAAC,EAAK,KAAW,EAAmB,EAAK,CAAK,CAAC,CAAC,CACrD,KAAK,EAEsB,EAAE,GAAG,EAAQ,IAAI,EAAa,EAC9D,CCvQA,SAAS,GAA2B,CACnC,MAAO,CACN,0BAAW,IAAI,IACf,yBAAU,IAAI,IACd,wBAAS,IAAI,IACb,UAAW,CAAC,EACZ,2BAAY,IAAI,IAChB,WAAY,EACZ,aAAc,KACd,MAAO,KACP,cAAe,KACf,cAAe,EACf,2BAAY,IAAI,IAChB,cAAe,KACf,kBAAmB,EACpB,CACD,CAEA,MAAM,EAAmB,OAAO,IAAI,uBAAuB,EACrD,EAAc,WAEpB,IAAI,EACJ,SAAS,GAAwB,CAChC,MAAO,KAAgB,EAAY,KAAsB,EAAY,GAAoB,EAAY,EACtG,CA6aA,SAAgB,GAAqB,CACpC,IAAM,EAAQ,EAAS,EACvB,GAAI,EAAM,eAAiB,KAAM,OAAO,EAAM,aAC9C,IAAI,EAAM,EAAM,UAAU,KAAK,EAAE,EACjC,IAAK,GAAM,CAAC,EAAO,KAAU,EAAM,WAC9B,EAAM,OAAS,IAAG,GAAO,EAAQ,IAAM,EAAM,KAAK,EAAE,EAAI,KAG7D,MADA,GAAM,aAAe,EACd,CACR"}
|
|
1
|
+
{"version":3,"file":"nuclo.ssr.mjs","names":[],"sources":["../../src/shared/strings.ts","../../src/hydration.ts","../../src/ssr/render-to-string.ts","../../src/style/engine.ts"],"sourcesContent":["/**\n * camelCase → kebab-case for CSS property (and attribute) names. A leading\n * capital becomes a leading dash, as vendor prefixes need\n * (WebkitTransition → -webkit-transition). Char-code loop with a\n * no-uppercase fast path: SSR runs this per style declaration.\n */\nexport function camelToKebab(str: string): string {\n let firstUpper = 0;\n for (; firstUpper < str.length; firstUpper++) {\n const code = str.charCodeAt(firstUpper);\n if (code >= 65 && code <= 90) break;\n }\n if (firstUpper === str.length) return str;\n\n let result = str.slice(0, firstUpper);\n for (let i = firstUpper; i < str.length; i++) {\n const code = str.charCodeAt(i);\n // A-Z is 65-90, a-z is 97-122 (difference of 32)\n result += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];\n }\n return result;\n}\n\nconst ATTR_ESCAPE_RE = /[&<>\"']/;\nconst TEXT_ESCAPE_RE = /[&<>]/;\n\n/**\n * Escapes HTML special characters in attribute values (includes \" and ')\n */\nexport function escapeHtml(text: string): string {\n // Fast path: most strings contain no special characters at all.\n const first = ATTR_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n case 34: escaped = '"'; break; // \"\n case 39: escaped = '''; break; // '\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n\n/**\n * Escapes HTML special characters in text node content.\n * Only &, < and > need escaping — quotes are safe inside text nodes.\n */\nexport function escapeText(text: string): string {\n const first = TEXT_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&'; break; // &\n case 60: escaped = '<'; break; // <\n case 62: escaped = '>'; break; // >\n default: continue;\n }\n result += text.slice(last, i) + escaped;\n last = i + 1;\n }\n return result + text.slice(last);\n}\n","/**\n * Hydration context — tracks global hydration state and per-parent cursor positions.\n *\n * During hydration the component tree is built against an existing DOM tree\n * (produced by SSR). Instead of creating new nodes, factories claim existing\n * children from their parent element by advancing a cursor.\n *\n * The cursor is a *node pointer* (the next unclaimed child), not an index.\n * Indexed childNodes access is O(n) after any tree/attribute mutation in\n * jsdom (live NodeList re-materialization) which made hydration O(n²);\n * firstChild/nextSibling pointers are O(1) in every DOM implementation.\n */\n\nimport { safeRemoveChild } from \"./shared/dom\";\n\nlet _hydrating = false;\n// True while forceUpdate() re-hydrates a live, client-rendered tree: text\n// claims work without `<!-- text-N -->` markers and reused elements get their\n// previous listeners/class state reset before modifiers re-apply.\nlet _force = false;\n// True while renderToString() is building a tree destined for serialization.\nlet _serializing = false;\n// Missing entry = cursor at parent.firstChild; null = past the last child.\nlet _cursors = new WeakMap<Node, Node | null>();\n\nexport function isHydrating(): boolean {\n return _hydrating;\n}\n\n/** True only inside a forceUpdate() pass (never during plain hydrate()). */\nexport function isForceHydrating(): boolean {\n return _hydrating && _force;\n}\n\n/**\n * True while a tree is being built for SSR serialization (inside\n * renderToString). Text children must keep their `<!-- text-N -->` markers in\n * this mode so the emitted HTML stays hydratable — even when `isBrowser` is\n * true (SSR running under jsdom, or an isomorphic worker that provides a DOM).\n * A pure client render leaves this false and skips the markers.\n */\nexport function isSerializing(): boolean {\n return _serializing;\n}\n\n/**\n * Runs `fn` with serialization mode enabled, restoring the previous state\n * afterwards (so nested/re-entrant renderToString calls behave correctly).\n */\nexport function runSerializing<T>(fn: () => T): T {\n const previous = _serializing;\n _serializing = true;\n try {\n return fn();\n } finally {\n _serializing = previous;\n }\n}\n\nexport function startHydration(force = false): void {\n _hydrating = true;\n _force = force;\n // Each hydration pass starts with a clean cursor slate. Stale cursors from\n // a previous pass would desynchronize claims when a container's content is\n // replaced and re-hydrated (HMR, islands re-mounting).\n _cursors = new WeakMap<Node, Node | null>();\n}\n\nexport function endHydration(): void {\n _hydrating = false;\n _force = false;\n}\n\n/**\n * Runs a callback with hydration temporarily disabled, restoring the previous\n * state afterwards. Used when a server/client mismatch forces a subtree to be\n * rendered fresh in the middle of a hydration pass — fresh rendering must not\n * claim nodes from the surrounding SSR DOM.\n */\nexport function runWithoutHydration<T>(fn: () => T): T {\n const wasHydrating = _hydrating;\n _hydrating = false;\n try {\n return fn();\n } finally {\n _hydrating = wasHydrating;\n }\n}\n\n/**\n * Returns the next unclaimed child of a parent node (or null at the end)\n * without advancing the cursor.\n */\nexport function peekChild(parent: Node): Node | null {\n const entry = _cursors.get(parent);\n return entry === undefined ? parent.firstChild : entry;\n}\n\n/**\n * Sets the cursor to a specific child node (or null for \"past the end\").\n */\nexport function setCursor(parent: Node, node: Node | null): void {\n _cursors.set(parent, node);\n}\n\nfunction isWhitespaceText(node: Node): boolean {\n return node.nodeType === 3 && !/\\S/.test(node.textContent || '');\n}\n\n/**\n * Advances the cursor past whitespace-only text nodes.\n *\n * Nuclo's own SSR output never produces bare whitespace text nodes (text\n * children are always preceded by a `<!-- text-N -->` marker), so unmarked\n * whitespace comes from the surrounding template or an HTML\n * formatter/minifier. Skipping (without removing) keeps the cursor aligned\n * while leaving the document's visual whitespace untouched.\n */\nexport function skipWhitespaceText(parent: Node): void {\n let child = peekChild(parent);\n let advanced = false;\n while (child && isWhitespaceText(child)) {\n child = child.nextSibling;\n advanced = true;\n }\n if (advanced) {\n setCursor(parent, child);\n }\n}\n\n/**\n * Claims the next child node from parent at the current cursor position.\n * Advances the cursor by one.\n */\nexport function claimChild(parent: Node): Node | null {\n const child = peekChild(parent);\n if (child) {\n setCursor(parent, child.nextSibling);\n }\n return child;\n}\n\n/**\n * forceUpdate() text claim: claims the next child when it is a bare text node\n * (client renders emit no `<!-- text-N -->` markers). Whitespace-only text\n * that is followed by a text marker is SSR template formatting, not content —\n * it is left for the marker path, which skips it. Returns null when nothing\n * claimable is at the cursor.\n */\nexport function claimBareText(parent: Node): Text | null {\n const next = peekChild(parent);\n if (!next || next.nodeType !== 3) return null;\n if (isWhitespaceText(next)) {\n let sibling = next.nextSibling;\n while (sibling && isWhitespaceText(sibling)) sibling = sibling.nextSibling;\n if (\n sibling && sibling.nodeType === 8 &&\n (sibling.textContent ?? \"\").trimStart().startsWith(\"text-\")\n ) return null;\n }\n claimChild(parent);\n return next as Text;\n}\n\n/**\n * Claims a server-rendered `<!--{prefix}-start-…-->` … `<!--{prefix}-end-->`\n * block (list()/when()) at the cursor, leaving the cursor just after the start\n * marker. Returns null, claiming nothing, when the next child isn't a start\n * marker. Pairs are depth-counted because directly nested blocks share a\n * host. A missing end marker (corrupt/truncated SSR output) is recreated right\n * after the start marker and reported as `recreated`.\n */\nexport function claimMarkerPair(\n parent: Node,\n prefix: \"list\" | \"when\",\n): { start: Comment; end: Comment; recreated: boolean } | null {\n skipWhitespaceText(parent);\n const start = peekChild(parent);\n const startPrefix = prefix + \"-start-\";\n if (!start || start.nodeType !== 8 || !start.textContent?.startsWith(startPrefix)) return null;\n claimChild(parent);\n const endText = prefix + \"-end\";\n let depth = 0;\n for (let node = start.nextSibling; node; node = node.nextSibling) {\n if (node.nodeType !== 8) continue;\n const text = node.textContent;\n if (text === endText) {\n if (depth === 0) return { start: start as Comment, end: node as Comment, recreated: false };\n depth--;\n } else if (text?.startsWith(startPrefix)) {\n depth++;\n }\n }\n const end = document.createComment(endText);\n parent.insertBefore(end, start.nextSibling);\n return { start: start as Comment, end, recreated: true };\n}\n\n/**\n * Attempts to claim an existing element from the parent during hydration.\n * Returns the claimed element if the next child matches the expected tag, or null.\n */\nexport function claimElement(parent: Node, tagName: string): Element | null {\n if (!_hydrating) return null;\n skipWhitespaceText(parent);\n const candidate = peekChild(parent);\n if (candidate && candidate.nodeType === 1 && (candidate as Element).tagName.toLowerCase() === tagName) {\n return claimChild(parent) as Element;\n }\n return null;\n}\n\n/**\n * Removes unclaimed SSR children from a hydrated element.\n * Must be called after modifiers have been applied to a claimed element.\n * Removes nodes from the cursor through `lastOriginalChild` (the element's\n * last child before modifiers ran) — nodes appended by modifiers come after\n * that boundary and are preserved.\n */\nexport function cleanupUnclaimedChildren(node: Node, lastOriginalChild: Node | null): void {\n if (!lastOriginalChild) return;\n let current = peekChild(node);\n while (current) {\n const next = current === lastOriginalChild ? null : current.nextSibling;\n // safeRemoveChild (not a bare removeChild): during forceUpdate() the\n // unclaimed nodes were live — their listeners, registrations and\n // onDestroy hooks must be released, not just detached.\n safeRemoveChild(current);\n current = next;\n }\n setCursor(node, null);\n}\n","/**\n * Server-Side Rendering (SSR) utilities for Nuclo\n * Renders Nuclo components to HTML strings in Node.js environment\n */\n\nimport { escapeHtml, escapeText, camelToKebab } from '../shared/strings';\nimport { runSerializing } from '../hydration';\nimport { NucloElement } from '../polyfill/Element';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\n\n/**\n * SVG attributes that are natively camelCase and must NOT be converted to kebab-case.\n * Most SVG attributes are already kebab-case (stroke-width, fill-rule, etc.), but a\n * handful are defined as camelCase in the SVG spec and must be preserved.\n */\nconst SVG_PRESERVE_CASE_ATTRS = new Set([\n 'viewBox', 'preserveAspectRatio', 'markerWidth', 'markerHeight',\n 'gradientTransform', 'patternTransform', 'clipPathUnits', 'gradientUnits',\n 'patternUnits', 'pathLength', 'refX', 'refY', 'stdDeviation',\n 'baseFrequency', 'numOctaves', 'kernelMatrix', 'tableValues',\n 'targetX', 'targetY', 'specularExponent', 'specularConstant',\n 'diffuseConstant', 'surfaceScale', 'xChannelSelector', 'yChannelSelector',\n 'edgeMode', 'stitchTiles', 'spreadMethod', 'patternContentUnits',\n 'markerUnits', 'startOffset', 'textLength', 'lengthAdjust',\n]);\n\n/**\n * HTML boolean attributes — presence means true, absence means false.\n * When the stored value is the string \"true\" or \"false\" (from setAttribute),\n * we must re-apply boolean semantics instead of outputting the raw string.\n */\nconst HTML_BOOLEAN_ATTRIBUTES = new Set([\n 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',\n 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'ismap', 'loop',\n 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'readonly', 'required',\n 'reversed', 'selected',\n]);\n\n/**\n * Serializes a DOM attribute value\n */\nfunction serializeAttribute(name: string, value: unknown): string {\n if (value === null || value === undefined || value === false) {\n return '';\n }\n\n if (value === true) {\n return ` ${name}`;\n }\n\n // Boolean attributes stored as strings via setAttribute() need special handling\n if (HTML_BOOLEAN_ATTRIBUTES.has(name)) {\n if (value === 'false') return '';\n if (value === 'true' || value === '' || value === name) return ` ${name}`;\n }\n\n // A style object (e.g. renderToStringWithContainer's containerAttrs from\n // untyped JS): serialize its declarations, skipping nullish/empty values.\n if (name === 'style' && typeof value === 'object') {\n let styleStr = '';\n for (const key in value as Record<string, unknown>) {\n const val = (value as Record<string, unknown>)[key];\n if (val == null || val === '') continue;\n styleStr += (styleStr ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n return styleStr ? ` style=\"${escapeHtml(styleStr)}\"` : '';\n }\n\n return ` ${name}=\"${escapeHtml(String(value))}\"`;\n}\n\n/**\n * Serializes DOM element attributes to HTML string\n *\n * `isPolyfill` is passed in by the caller (serializeNode already computed it\n * to decide whether tagName needs lowercasing) so this doesn't repeat the\n * same `Array.isArray` discriminator check per element.\n */\nfunction serializeAttributes(element: Element, isPolyfill: boolean): string {\n let result = '';\n\n // Handle polyfill elements. NucloElement keeps every child in a plain Array\n // (`children`), which a real DOM element never does (HTMLCollection) — this is\n // the same allocation-free discriminator serializeNode() uses. Reading\n // `el.attributes` directly would lazily allocate an empty Map for every\n // element being serialized, so the backing `_attributes` field is read instead.\n if (isPolyfill) {\n const el = element as any;\n const attrs = el._attributes as Map<string, string> | undefined;\n\n // id — may live on the property rather than in the Map\n if (el.id && !attrs?.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // class — kept on .className, mirrored to Map only when setAttribute is used\n if (el.className && !attrs?.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // style — lives on the backing _style object (its own properties are the\n // declarations), not in the attributes Map. Read the field directly (never\n // `el.style`) so elements that never set a style are not forced to lazily\n // allocate an empty SSRStyle just to serialize. Empty values (e.g. reactive\n // styles that resolved to undefined) are skipped.\n const styleObj = el._style as Record<string, unknown> | undefined;\n if (styleObj && !attrs?.has('style')) {\n let css = '';\n for (const key in styleObj) {\n const val = styleObj[key];\n if (val == null || val === '') continue;\n css += (css ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n if (css) result += ` style=\"${escapeHtml(css)}\"`;\n }\n\n // All remaining attributes from the Map\n // Convert camelCase ARIA/HTML attribute names (e.g. ariaLabel → aria-label) that the\n // polyfill stores as-is because NucloElement lacks the browser property mappings.\n // SVG attributes that are natively camelCase (e.g. viewBox, preserveAspectRatio) must\n // NOT be converted — they are stored via setAttribute() as-is.\n if (attrs) {\n for (const [name, value] of attrs) {\n const htmlName = SVG_PRESERVE_CASE_ATTRS.has(name) ? name : camelToKebab(name);\n result += serializeAttribute(htmlName, value);\n }\n }\n return result;\n }\n\n // Handle browser elements with NamedNodeMap attributes\n if (element.attributes && element.attributes.length) {\n for (let i = 0; i < element.attributes.length; i++) {\n const attr = element.attributes[i];\n if (attr && attr.name) {\n result += serializeAttribute(attr.name, attr.value);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Tag-category lookup, keyed by lowercase tag name. Every element in a tree\n * hits this once, so void + raw-text elements are folded into a single\n * dictionary lookup instead of two separate `Set.has()` probes (one of which\n * would always miss for the other category).\n */\nconst TAG_VOID = 1;\nconst TAG_RAW_TEXT = 2;\nconst TAG_CATEGORY: Record<string, 1 | 2> = Object.assign(Object.create(null), {\n // Self-closing HTML tags that don't have closing tags\n area: TAG_VOID, base: TAG_VOID, br: TAG_VOID, col: TAG_VOID, embed: TAG_VOID,\n hr: TAG_VOID, img: TAG_VOID, input: TAG_VOID, link: TAG_VOID, meta: TAG_VOID,\n param: TAG_VOID, source: TAG_VOID, track: TAG_VOID, wbr: TAG_VOID,\n // Raw-text elements — browsers never decode entities inside them, so their\n // text content must be emitted verbatim (escaping would corrupt inline\n // JS/CSS, e.g. `a < b` becoming `a < b` inside a script). A closing-tag\n // sequence in the content would terminate the element early, so it is\n // neutralized (see escapeRawText).\n script: TAG_RAW_TEXT, style: TAG_RAW_TEXT,\n});\n\nfunction escapeRawText(tagName: string, text: string): string {\n // \"</script\" (any case) inside a script would close it — break the sequence\n // the same way JSON serializers do (\"<\\/script\").\n return text.replace(new RegExp('</(' + tagName + ')', 'gi'), '<\\\\/$1');\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node — only & < > need escaping; quotes are safe in text content\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeText(node.textContent || '');\n }\n\n // Comment node\n if (node.nodeType === 8) { // Node.COMMENT_NODE\n return `<!--${node.textContent || ''}-->`;\n }\n\n // Element node\n if (node.nodeType === 1) { // Node.ELEMENT_NODE\n const element = node as Element;\n const rawTagName = element.tagName;\n // Duck-typed polyfill discriminator (also used by serializeAttributes): a\n // plain Array `children` field never occurs on a real\n // DOM element (HTMLCollection), only on NucloElement — and on hand-rolled\n // polyfill-shaped test doubles, which is why this alone isn't enough to\n // skip re-lowercasing tagName below.\n const isPolyfillShape = Array.isArray((element as any).children);\n // Only a *real* NucloElement is guaranteed to have pre-lowercased tagName\n // (done once in its constructor) — re-lowercasing it here on every\n // element would be pure waste, since this is the hottest function in\n // renderToString. Anything else (browser Element, or a polyfill-shaped\n // object that didn't go through `new NucloElement()`) still needs it.\n const tagName = element instanceof NucloElement ? rawTagName : rawTagName.toLowerCase();\n const attributes = serializeAttributes(element, isPolyfillShape);\n const category = TAG_CATEGORY[tagName];\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as any).children : element.childNodes;\n\n // Raw-text elements: emit text verbatim (no entity escaping, no Nuclo\n // text markers — `<!--` would act as a line comment inside a script).\n if (category === TAG_RAW_TEXT) {\n let rawContent = '';\n const rawChildren = childNodes;\n if (rawChildren && rawChildren.length > 0) {\n for (let i = 0; i < rawChildren.length; i++) {\n const child = rawChildren[i];\n if (child && child.nodeType === 3) {\n rawContent += child.textContent || '';\n }\n }\n } else {\n const tc = (node as { textContent?: unknown }).textContent;\n if (typeof tc === 'string') rawContent = tc;\n }\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n if (childNodes && childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n childrenHtml += serializeNode(child);\n }\n }\n } else {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as any).textContent;\n if (typeof tc === 'string' && tc) {\n childrenHtml = escapeText(tc);\n }\n }\n\n return `<${tagName}${attributes}>${childrenHtml}</${tagName}>`;\n }\n\n // Document fragment\n if (node.nodeType === 11) { // Node.DOCUMENT_FRAGMENT_NODE\n let result = '';\n const childNodes: ArrayLike<Node> = (node as any).childNodes ?? [];\n if (childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n result += serializeNode(child);\n }\n }\n }\n return result;\n }\n\n return '';\n}\n\n/**\n * Renders a Nuclo component to an HTML string for server-side rendering\n *\n * @param input - A Nuclo component function, DOM element, or node\n * @returns HTML string representation of the component\n *\n * @example\n * ```ts\n * import { renderToString } from 'nuclo/ssr';\n * import { div } from 'nuclo';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div>Hello, World!</div>'\n * ```\n */\nexport function renderToString(input: RenderableInput): string {\n if (!input) return '';\n\n if (typeof input === 'function') {\n try {\n // Build the tree in serialization mode so text children keep their\n // <!-- text-N --> markers (needed for hydration) even when isBrowser is\n // true, e.g. SSR running under jsdom.\n const element = runSerializing(() => {\n if (typeof document === 'undefined') throw new Error('Document is not available. Make sure polyfills are loaded.');\n return input(document.createElement('div') as unknown as ExpandedElement<ElementTagName>, 0);\n });\n return element && typeof element === 'object' && 'nodeType' in element ? serializeNode(element as Node) : '';\n } catch (error) {\n console.error('Error rendering component to string:', error);\n return '';\n }\n }\n\n if ('nodeType' in input) {\n return serializeNode(input as Node);\n }\n\n return '';\n}\n\n/**\n * Renders multiple Nuclo components to HTML strings\n *\n * @param inputs - Array of Nuclo components\n * @returns Array of HTML strings\n */\nexport function renderManyToString(inputs: RenderableInput[]): string[] {\n return inputs.map(input => renderToString(input));\n}\n\n/**\n * Renders a Nuclo component and wraps it in a container element\n *\n * @param input - A Nuclo component\n * @param containerTag - The tag name for the container (default: 'div')\n * @param containerAttrs - Attributes for the container element\n * @returns HTML string with container wrapper\n */\nexport function renderToStringWithContainer(\n input: RenderableInput,\n containerTag: string = 'div',\n containerAttrs: Record<string, string> = {}\n): string {\n const content = renderToString(input);\n const attrs = Object.entries(containerAttrs)\n .map(([key, value]) => serializeAttribute(key, value))\n .join('');\n\n return `<${containerTag}${attrs}>${content}</${containerTag}>`;\n}\n","/**\n * Shared generated-CSS engine state.\n *\n * One stylesheet, one rule registry, shared by every `createCss` instance so\n * identical declarations dedupe globally and cascade order stays deterministic:\n * base rules first (insertion order), then one grouping rule per at-rule query\n * in registration order (theme `screens` order).\n *\n * Browser: rules are injected through CSSOM with O(1) appends — no rescans.\n * SSR: rules accumulate in the registry; `getCssText()` serializes them.\n *\n * The state below lives on `globalThis`, keyed by a `Symbol.for(...)` (the\n * global symbol registry, shared by every realm-local copy of this module).\n * `nuclo` publishes `.`, `./ssr`, and `./polyfill` as independently built\n * bundle files (see tsdown.config.ts) — a consumer that resolves them\n * through normal package resolution (any bundler/runtime that doesn't\n * alias every subpath to this same source file) loads two or three\n * separate copies of this module, each with its own module-scope\n * variables. Without a realm-wide singleton, `css()` (from `nuclo`) would\n * mint rules into one copy's registry while `getCssText()` (from\n * `nuclo/ssr`) reads an entirely different, always-empty one — SSR output\n * would render the right classes with no matching CSS at all. Storing the registry on `globalThis` instead makes\n * every copy of this module share the same state regardless of how many\n * times it ends up bundled.\n *\n * The registry never shrinks on its own: atomCache/atomMeta/rawKeys/\n * baseRules/queryRules retain every rule for the life of the process —\n * content-addressed dedup needs a durable record of what each class means,\n * and the engine has no way to know whether a minted class still backs a\n * live DOM node (it's deliberately decoupled from element/list lifecycle),\n * so it can't safely evict. Only resetStyles() (a manual test helper)\n * clears it. This is fine for the intended use — a bounded design-token\n * vocabulary — but a caller piping truly per-instance dynamic values\n * through css({ raw: {...} }) (e.g. per-row data in a list) mints one\n * permanent class per unique value; prefer the `style` prop\n * (element/inline-style.ts) for values that vary per instance rather than\n * per design token. See test/style/registry-growth.test.ts.\n */\n\nexport interface BlockMeta {\n\treadonly query: string | undefined;\n\treadonly suffix: string;\n\treadonly decls: ReadonlyArray<readonly [string, string]>;\n\t/** Author-supplied identity from `css(name, …)`; inherited by cx() merges. */\n\treadonly name: string | undefined;\n}\n\ninterface EngineState {\n\t// Registry — the source of truth for all generated CSS.\n\t// atomCache: declaration-block key -> class name (dedup)\n\t// atomMeta: class name -> what the class means (its selector context and its\n\t// declarations). This is what lets cx() merge two same-context classes at the\n\t// declaration level instead of guessing: the engine can always recover the\n\t// exact content of any class it minted.\n\tatomCache: Map<string, string>;\n\tatomMeta: Map<string, BlockMeta>;\n\trawKeys: Set<string>;\n\tbaseRules: string[];\n\tqueryRules: Map<string, string[]>;\n\tstyleEpoch: number;\n\n\t// Memoized getCssText() output. SSR calls getCssText() once per request, but the\n\t// rule set is stable after warmup — so the serialized sheet is recomputed only\n\t// when a new rule is actually recorded. Invalidated in record() and resetStyles().\n\tcssTextCache: string | null;\n\n\t// Browser CSSOM state. Base rules occupy [0, baseRuleCount); grouping rules\n\t// (one per query, created in registration order) live after them. CSSRule\n\t// references stay live when indices shift, so insertion never rescans.\n\tsheet: CSSStyleSheet | null;\n\tsheetDocument: Document | null;\n\tbaseRuleCount: number;\n\tgroupRules: Map<string, CSSGroupingRule>;\n\n\t// Normalized rules already sitting in a freshly-adopted stylesheet that this\n\t// module didn't insert itself — e.g. server-rendered CSS being hydrated.\n\t// Rebuilt on every fresh bind in ensureSheet(); null when there is nothing\n\t// external to guard against.\n\texternalRules: Map<string, Set<string>> | null;\n\thasExternalGroups: boolean;\n}\n\nfunction createState(): EngineState {\n\treturn {\n\t\tatomCache: new Map(),\n\t\tatomMeta: new Map(),\n\t\trawKeys: new Set(),\n\t\tbaseRules: [],\n\t\tqueryRules: new Map(),\n\t\tstyleEpoch: 0,\n\t\tcssTextCache: null,\n\t\tsheet: null,\n\t\tsheetDocument: null,\n\t\tbaseRuleCount: 0,\n\t\tgroupRules: new Map(),\n\t\texternalRules: null,\n\t\thasExternalGroups: false,\n\t};\n}\n\nconst ENGINE_STATE_KEY = Symbol.for(\"nuclo.style.engine.v1\");\nconst globalScope = globalThis as unknown as Record<symbol, EngineState>;\n// Importing the runtime does not allocate the registry until styling is used.\nlet cachedState: EngineState | undefined;\nfunction getState(): EngineState {\n\treturn cachedState ??= globalScope[ENGINE_STATE_KEY] ?? (globalScope[ENGINE_STATE_KEY] = createState());\n}\n\n/** Pre-register at-rule queries so their cascade order follows theme order. */\nexport function registerQueries(queries: Iterable<string>): void {\n\tfor (const query of queries) {\n\t\tconst state = getState();\n\t\tif (!state.queryRules.has(query)) state.queryRules.set(query, []);\n\t}\n}\n\nfunction createGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\ttry {\n\t\tconst idx = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", idx);\n\t\tconst group = s.cssRules[idx] as CSSGroupingRule;\n\t\tstate.groupRules.set(query, group);\n\t\tstate.groupRules.set(groupQueryOf(group), group);\n\t\treturn group;\n\t} catch {\n\t\treturn null; // unsupported at-rule in this environment\n\t}\n}\n\n// A group prelude is stable while its children grow. Reading cssText on each\n// insertion would serialize all previous children and make loading quadratic.\n// Weak keys allow detached stylesheets to be collected after reset/rebinding.\nconst groupQueries = new WeakMap<CSSRule, string>();\nfunction groupQueryOf(rule: CSSRule): string {\n\tconst cached = groupQueries.get(rule);\n\tif (cached !== undefined) return cached;\n\tconst text = rule.cssText;\n\tconst brace = text.indexOf(\"{\");\n\tconst query = (brace === -1 ? text : text.slice(0, brace)).trim();\n\tgroupQueries.set(rule, query);\n\treturn query;\n}\n\nfunction ruleIdentity(rule: CSSRule): string {\n\treturn rule instanceof CSSStyleRule ? rule.selectorText : groupQueryOf(rule);\n}\n\nfunction rememberExternalRule(rule: CSSRule, query: string | undefined): void {\n\tconst state = getState();\n\tif (!state.externalRules) return;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tlet texts = state.externalRules.get(key);\n\tif (!texts) {\n\t\ttexts = new Set();\n\t\tstate.externalRules.set(key, texts);\n\t}\n\ttexts.add(rule.cssText);\n}\n\nfunction consumeExternalRule(rule: CSSRule, query: string | undefined): boolean {\n\tconst state = getState();\n\tif (!state.externalRules) return false;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tconst texts = state.externalRules.get(key);\n\tif (!texts?.delete(rule.cssText)) return false;\n\tif (texts.size === 0) state.externalRules.delete(key);\n\tif (state.externalRules.size === 0) state.externalRules = null;\n\treturn true;\n}\n\n/**\n * Index an already-populated stylesheet found on adopt (SSR output, most\n * commonly): every top-level conditional grouping rule (@media/@container/…)\n * is registered into groupRules (by its reconstructed at-rule text) so later\n * record() calls append into it instead of creating a sibling duplicate. The\n * other rules are indexed by scope, selector/prelude, and normalized cssText\n * so exact server/client matches can be skipped without hiding a changed\n * named or global rule that happens to reuse the same selector.\n */\nfunction indexExternalRules(rules: CSSRuleList): void {\n\tconst state = getState();\n\tfor (let i = 0; i < rules.length; i++) {\n\t\tconst rule = rules[i];\n\t\tif (rule instanceof CSSStyleRule) {\n\t\t\trememberExternalRule(rule, undefined);\n\t\t\tcontinue;\n\t\t}\n\t\tconst prelude = groupQueryOf(rule);\n\t\tif (\"cssRules\" in rule && !prelude.startsWith(\"@keyframes\")) {\n\t\t\tconst group = rule as CSSGroupingRule;\n\t\t\tstate.hasExternalGroups = true;\n\t\t\tstate.groupRules.set(prelude, group);\n\t\t\tfor (let j = 0; j < group.cssRules.length; j++) {\n\t\t\t\trememberExternalRule(group.cssRules[j], prelude);\n\t\t\t}\n\t\t} else {\n\t\t\t// @keyframes, @font-face, … — one-shot rules matched by prelude.\n\t\t\trememberExternalRule(rule, undefined);\n\t\t}\n\t}\n}\n\nfunction insertBase(s: CSSStyleSheet, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\ts.insertRule(rule, state.baseRuleCount);\n\t\tconst inserted = s.cssRules[state.baseRuleCount];\n\t\tif (consumeExternalRule(inserted, undefined)) {\n\t\t\ts.deleteRule(state.baseRuleCount);\n\t\t\treturn;\n\t\t}\n\t\tstate.baseRuleCount++;\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction insertGrouped(group: CSSGroupingRule, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\tconst index = group.cssRules.length;\n\t\tgroup.insertRule(rule, index);\n\t\tconst inserted = group.cssRules[index];\n\t\tif (state.externalRules && consumeExternalRule(inserted, groupQueryOf(group))) group.deleteRule(index);\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction getOrCreateGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\tconst direct = state.groupRules.get(query);\n\tif (direct) return direct;\n\tif (!state.hasExternalGroups) return createGroup(s, query);\n\n\t// CSSOM may normalize whitespace in an at-rule prelude. Normalize the\n\t// authored query through a temporary empty group before deciding that an\n\t// adopted SSR group is missing.\n\ttry {\n\t\tconst index = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", index);\n\t\tconst normalized = groupQueryOf(s.cssRules[index]);\n\t\ts.deleteRule(index);\n\t\tconst existing = state.groupRules.get(normalized);\n\t\tif (existing) {\n\t\t\tstate.groupRules.set(query, existing);\n\t\t\treturn existing;\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n\treturn createGroup(s, query);\n}\n\n/**\n * Returns the live stylesheet, (re)binding when the document changes — e.g.\n * fresh jsdom documents between tests. Rebinding replays the whole registry,\n * which replaces v1's per-call classExistsInDOM() DOM scans with a single\n * replay per document.\n */\nexport function ensureSheet(): CSSStyleSheet | null {\n\tconst state = getState();\n\tif (typeof document === \"undefined\") return null;\n\t// The nuclo SSR polyfill provides a document without getElementById — in\n\t// that context rules accumulate in the registry for getCssText() only.\n\tif (typeof document.getElementById !== \"function\") return null;\n\tif (\n\t\tstate.sheet &&\n\t\tstate.sheetDocument === document &&\n\t\tstate.sheet.ownerNode &&\n\t\t(state.sheet.ownerNode as Element).isConnected\n\t) {\n\t\treturn state.sheet;\n\t}\n\tconst existing = document.getElementById(\"nuclo-styles\");\n\tlet el: HTMLStyleElement;\n\tif (existing?.tagName === \"STYLE\") {\n\t\tel = existing as HTMLStyleElement;\n\t} else {\n\t\tel = document.createElement(\"style\");\n\t\tel.id = \"nuclo-styles\";\n\t\tif (existing) existing.replaceWith(el);\n\t\telse document.head.appendChild(el);\n\t}\n\tstate.sheet = el.sheet;\n\tstate.sheetDocument = document;\n\tstate.groupRules.clear();\n\tstate.baseRuleCount = 0;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tif (!state.sheet) return null;\n\tif (state.sheet.cssRules.length > 0) {\n\t\t// Leading top-level non-group rules (style rules, @keyframes — before\n\t\t// the first @media/@container group) are this element's existing\n\t\t// \"base\" region — count them so insertBase() keeps inserting new base\n\t\t// rules right after them, still ahead of every group.\n\t\tlet i = 0;\n\t\twhile (i < state.sheet.cssRules.length) {\n\t\t\tconst r = state.sheet.cssRules[i];\n\t\t\tif (!(r instanceof CSSStyleRule) && \"cssRules\" in r && !groupQueryOf(r).startsWith(\"@keyframes\")) break;\n\t\t\ti++;\n\t\t}\n\t\tstate.baseRuleCount = i;\n\t\tstate.externalRules = new Map();\n\t\tindexExternalRules(state.sheet.cssRules);\n\t}\n\tfor (const rule of state.baseRules) insertBase(state.sheet, rule);\n\tfor (const [query, rules] of state.queryRules) {\n\t\tconst group = getOrCreateGroup(state.sheet, query);\n\t\tif (group) for (const rule of rules) insertGrouped(group, rule);\n\t}\n\treturn state.sheet;\n}\n\nfunction record(rule: string, query: string | undefined): void {\n\tconst state = getState();\n\t// Bind/replay *before* this rule joins the registry, so a fresh bind's\n\t// replay only ever covers previously-known rules — never this one twice.\n\tconst s = ensureSheet();\n\n\tif (query === undefined) {\n\t\tstate.baseRules.push(rule);\n\t} else {\n\t\tlet bucket = state.queryRules.get(query);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tstate.queryRules.set(query, bucket);\n\t\t}\n\t\tbucket.push(rule);\n\t}\n\tstate.cssTextCache = null; // a new rule changes the serialized sheet\n\n\tif (s) {\n\t\tif (query === undefined) {\n\t\t\tinsertBase(s, rule);\n\t\t} else {\n\t\t\tconst group = getOrCreateGroup(s, query);\n\t\t\tif (group) insertGrouped(group, rule);\n\t\t}\n\t}\n}\n\n/**\n * Content-addressed ~42-bit hash (two interleaved FNV-1a style passes). Class\n * names must match between server and client for SSR hydration, so a counter\n * is not an option; 42 bits keeps collision odds low (~1e-5 at 10k\n * unique declaration blocks).\n */\nexport function hash(input: string): string {\n\tlet a = 0x811c9dc5;\n\tlet b = 0x7ee3623b;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst c = input.charCodeAt(i);\n\t\ta = Math.imul(a ^ c, 0x01000193);\n\t\tb = Math.imul(b ^ c, 0x85ebca6b);\n\t}\n\treturn (a >>> 0).toString(36) + ((b >>> 9) % 1296).toString(36);\n}\n\n/** Length-prefixed (query, selector-suffix) key — unambiguous for any strings. */\nexport function contextKeyOf(query: string | undefined, suffix: string): string {\n\tconst queryPart = query === undefined ? \"-\" : \"+\" + query.length + \":\" + query;\n\treturn queryPart + suffix.length + \":\" + suffix;\n}\n\nfunction appendKeyPart(key: string, value: string): string {\n\treturn key + value.length + \":\" + value;\n}\n\nexport function expandAmpersands(input: string, replacement: string): string {\n\tlet expanded = \"\";\n\tlet quote = 0;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst code = input.charCodeAt(i);\n\t\tif (code === 92 /* \\\\ */ && i + 1 < input.length) {\n\t\t\texpanded += input[i] + input[++i];\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote !== 0) {\n\t\t\texpanded += input[i];\n\t\t\tif (code === quote) quote = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tif (code === 34 /* \" */ || code === 39 /* ' */) {\n\t\t\tquote = code;\n\t\t\texpanded += input[i];\n\t\t} else {\n\t\t\texpanded += code === 38 /* & */ ? replacement : input[i];\n\t\t}\n\t}\n\treturn expanded;\n}\n\nfunction selectorFor(className: string, suffix: string): string {\n\tconst selector = \".\" + className;\n\treturn selector + expandAmpersands(suffix, selector);\n}\n\n/**\n * True for strings usable verbatim as an unescaped CSS class selector:\n * letters/digits/`-`/`_`, not starting with a digit or `-`+digit.\n * Char-code loop rather than a RegExp — this runs on the cold mint path but\n * stays allocation-free either way.\n */\nfunction isValidClassName(name: string): boolean {\n\tif (name.length === 0 || name === \"-\") return false;\n\tfor (let i = 0; i < name.length; i++) {\n\t\tconst c = name.charCodeAt(i);\n\t\tconst ok =\n\t\t\t(c >= 97 && c <= 122) || // a-z\n\t\t\t(c >= 65 && c <= 90) || // A-Z\n\t\t\tc === 45 || c === 95 || // - _\n\t\t\t(i > 0 && c >= 48 && c <= 57); // 0-9, never leading\n\t\tif (!ok) return false;\n\t}\n\t// \"-9foo\" is not a valid identifier start either.\n\tif (name.charCodeAt(0) === 45 && name.length > 1) {\n\t\tconst second = name.charCodeAt(1);\n\t\tif (second >= 48 && second <= 57) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Mint (or reuse) the class for a declaration block in a selector context.\n *\n * Declarations keep their authored order — CSS resolves shorthand/longhand\n * and repeated-property conflicts by declaration order, so reordering (e.g.\n * alphabetical sorting) would silently change what `{ pt: 20, p: 16 }` means.\n * The cost is that the same declarations authored in a different order mint a\n * second, content-identical class — harmless, and rare in practice.\n *\n * `name` is the author's identity for the style (`css(\"app-root\", …)`). It\n * becomes part of the cache key, so two different names never share a class\n * even with identical declarations. With `exactName`, this block *is* the\n * named one and takes the name verbatim; otherwise the name is only a\n * readable prefix on a content-addressed prefix (`app-root-1a2b3c`), which is\n * how a named style's non-base contexts and its cx() merges stay identifiable\n * in devtools without ever colliding with the base class.\n */\nexport function atomBlock(\n\tquery: string | undefined,\n\tsuffix: string,\n\tdecls: ReadonlyArray<readonly [string, string]>,\n\tname?: string,\n\texactName = false,\n): string {\n\tconst state = getState();\n\tlet contextKey = contextKeyOf(query, suffix);\n\tfor (const [prop, value] of decls) {\n\t\tcontextKey = appendKeyPart(appendKeyPart(contextKey, prop), value);\n\t}\n\tif (name !== undefined && !isValidClassName(name)) {\n\t\tconsole.warn(\n\t\t\t`[nuclo] css() name ${JSON.stringify(name)} is not a valid CSS class name ` +\n\t\t\t\t\"(letters, digits, - and _ only, not starting with a digit); falling back to a generated name.\",\n\t\t);\n\t\tname = undefined;\n\t}\n\tconst declKey = name === undefined ? \"a\" + contextKey : \"n\" + appendKeyPart(\"\", name) + contextKey;\n\tlet className = state.atomCache.get(declKey);\n\tif (className !== undefined) {\n\t\t// Touch the sheet so a swapped document (tests) gets the replayed rules.\n\t\tensureSheet();\n\t\treturn className;\n\t}\n\n\tif (name === undefined) {\n\t\tclassName = \"n\" + hash(declKey);\n\t} else {\n\t\tclassName = exactName ? name : name + \"-\" + hash(declKey);\n\t\tif (state.atomMeta.has(className)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[nuclo] css() name ${JSON.stringify(className)} is already used by a different style. ` +\n\t\t\t\t\t\"Both rules are emitted under the same class, so they will override each other — rename one.\",\n\t\t\t);\n\t\t}\n\t}\n\n\tlet body = \"\";\n\tfor (const [prop, value] of decls) body += (body ? \";\" : \"\") + prop + \":\" + value;\n\tstate.atomCache.set(declKey, className);\n\tstate.atomMeta.set(className, { query, suffix, decls, name });\n\trecord(selectorFor(className, suffix) + \"{\" + body + \"}\", query);\n\treturn className;\n}\n\n/**\n * Merge two same-context generated classes into one: `second`'s declarations\n * override `first`'s per exact property (shorthand/longhand interplay is left\n * to CSS order), and the combined block mints its own content-addressed class\n * — so cx(base, override) keeps base's untouched properties instead of\n * dropping the whole base class. Deterministic: the same pair always yields\n * the same class name, on server and client alike. A named base passes its\n * name down as a prefix, so `cx(appRoot, active)` reads as `app-root-1a2b3c`.\n */\nexport function mergeBlocks(first: string, second: string): string {\n\tconst state = getState();\n\tconst a = state.atomMeta.get(first);\n\tconst b = state.atomMeta.get(second);\n\tif (!a || !b) return second; // unknown input — nothing to merge with\n\tconst concat = [...a.decls, ...b.decls];\n\t// Keep only the last occurrence of each exact property, at its position —\n\t// so a later `padding` still fully resets an earlier `padding-top`.\n\tconst lastIndex = new Map<string, number>();\n\tfor (let i = 0; i < concat.length; i++) lastIndex.set(concat[i][0], i);\n\tconst merged = concat.filter((decl, i) => lastIndex.get(decl[0]) === i);\n\treturn atomBlock(a.query, a.suffix, merged, a.name ?? b.name);\n}\n\n/** Conflict key (query|suffix) for a generated class — lets cx() group classes by selector context. */\nexport function conflictKeyOf(className: string): string | undefined {\n\tconst state = getState();\n\tconst meta = state.atomMeta.get(className);\n\treturn meta === undefined ? undefined : contextKeyOf(meta.query, meta.suffix);\n}\n\n/** Current registry generation, used to invalidate per-instance WeakMap hits after resetStyles(). */\nexport function getStyleEpoch(): number {\n\treturn (cachedState ?? globalScope[ENGINE_STATE_KEY])?.styleEpoch ?? 0;\n}\n\n/** Register a non-atomic rule (keyframes, global styles) once per dedupe key. */\nexport function addRawRule(dedupeKey: string, rule: string): void {\n\tconst state = getState();\n\tif (state.rawKeys.has(dedupeKey)) {\n\t\tensureSheet();\n\t\treturn;\n\t}\n\tstate.rawKeys.add(dedupeKey);\n\trecord(rule, undefined);\n}\n\n/** All generated CSS — base rules first, then at-rule groups in registration order. */\nexport function getCssText(): string {\n\tconst state = getState();\n\tif (state.cssTextCache !== null) return state.cssTextCache;\n\tlet out = state.baseRules.join(\"\");\n\tfor (const [query, rules] of state.queryRules) {\n\t\tif (rules.length > 0) out += query + \"{\" + rules.join(\"\") + \"}\";\n\t}\n\tstate.cssTextCache = out;\n\treturn out;\n}\n\n/** Clear all engine state (test helper). Removes the injected style element. */\nexport function resetStyles(): void {\n\tconst state = getState();\n\tstate.atomCache.clear();\n\tstate.atomMeta.clear();\n\tstate.rawKeys.clear();\n\tstate.baseRules.length = 0;\n\tstate.queryRules.clear();\n\tstate.groupRules.clear();\n\tstate.cssTextCache = null;\n\tstate.baseRuleCount = 0;\n\tstate.sheet = null;\n\tstate.sheetDocument = null;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tstate.styleEpoch++;\n\tif (typeof document !== \"undefined\") {\n\t\tdocument.getElementById(\"nuclo-styles\")?.remove();\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuclo",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Lightweight, type-safe DOM library with plain mutable state and explicit updates — global tag builders, SSR, hydration, and typed CSS, no virtual DOM, no compiler.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Danilo Celestino de Castro (https://github.com/dan2dev)",
|
|
@@ -19,7 +19,7 @@ declare global {
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
function render<TTagName extends ElementTagName = ElementTagName>(
|
|
22
|
-
nodeModFn: NodeModFn<TTagName
|
|
22
|
+
nodeModFn: NodeModFn<TTagName> | (() => NodeModFn<TTagName>),
|
|
23
23
|
parent?: Element,
|
|
24
24
|
index?: number
|
|
25
25
|
): ExpandedElement<TTagName>;
|
|
@@ -42,7 +42,31 @@ declare global {
|
|
|
42
42
|
* ```
|
|
43
43
|
*/
|
|
44
44
|
function hydrate<TTagName extends ElementTagName = ElementTagName>(
|
|
45
|
-
nodeModFn: NodeModFn<TTagName
|
|
45
|
+
nodeModFn: NodeModFn<TTagName> | (() => NodeModFn<TTagName>),
|
|
46
|
+
parent?: Element,
|
|
47
|
+
): ExpandedElement<TTagName>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Re-evaluates every component-rendered root against its live DOM —
|
|
51
|
+
* including static values that update() never touches — reusing existing
|
|
52
|
+
* elements in place (focus, input values and scroll survive).
|
|
53
|
+
*
|
|
54
|
+
* Roots become refreshable by rendering the component function itself:
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const App = () => div(h1(labels[language].title));
|
|
59
|
+
* render(App, container); // the function, not App()
|
|
60
|
+
* // later, after `language` changed:
|
|
61
|
+
* forceUpdate();
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* The explicit form forceUpdate(App(), parent) force-rehydrates one tree
|
|
65
|
+
* manually and returns its root element.
|
|
66
|
+
*/
|
|
67
|
+
function forceUpdate(): void;
|
|
68
|
+
function forceUpdate<TTagName extends ElementTagName = ElementTagName>(
|
|
69
|
+
nodeModFn: NodeModFn<TTagName> | (() => NodeModFn<TTagName>),
|
|
46
70
|
parent?: Element,
|
|
47
71
|
): ExpandedElement<TTagName>;
|
|
48
72
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -31,12 +31,17 @@ export function scope<TTagName extends ElementTagName = ElementTagName>(
|
|
|
31
31
|
...ids: string[]
|
|
32
32
|
): NodeModFn<TTagName>;
|
|
33
33
|
export function render<TTagName extends ElementTagName = ElementTagName>(
|
|
34
|
-
nodeModFn: NodeModFn<TTagName
|
|
34
|
+
nodeModFn: NodeModFn<TTagName> | (() => NodeModFn<TTagName>),
|
|
35
35
|
parent?: Element,
|
|
36
36
|
index?: number,
|
|
37
37
|
): ExpandedElement<TTagName>;
|
|
38
38
|
export function hydrate<TTagName extends ElementTagName = ElementTagName>(
|
|
39
|
-
nodeModFn: NodeModFn<TTagName
|
|
39
|
+
nodeModFn: NodeModFn<TTagName> | (() => NodeModFn<TTagName>),
|
|
40
|
+
parent?: Element,
|
|
41
|
+
): ExpandedElement<TTagName>;
|
|
42
|
+
export function forceUpdate(): void;
|
|
43
|
+
export function forceUpdate<TTagName extends ElementTagName = ElementTagName>(
|
|
44
|
+
nodeModFn: NodeModFn<TTagName> | (() => NodeModFn<TTagName>),
|
|
40
45
|
parent?: Element,
|
|
41
46
|
): ExpandedElement<TTagName>;
|
|
42
47
|
|