nuclo 0.3.16 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"nuclo.ssr.mjs","names":["globalScope"],"sources":["../../src/shared/strings.ts","../../src/shared/serializing.ts","../../src/shared/dom.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 = '&amp;'; break; // &\n case 60: escaped = '&lt;'; break; // <\n case 62: escaped = '&gt;'; break; // >\n case 34: escaped = '&quot;'; break; // \"\n case 39: escaped = '&#039;'; 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 = '&amp;'; break; // &\n case 60: escaped = '&lt;'; break; // <\n case 62: escaped = '&gt;'; 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 * IDL property name → HTML content attribute name, for the camelCase keys a\n * real element reflects (tabIndex → tabindex, htmlFor → for,\n * ariaDescribedBy → aria-describedby, httpEquiv → http-equiv). Lowercase and\n * kebab-case names pass through unchanged. SSR runs this per polyfill attribute.\n */\nexport function propertyToAttribute(name: string): string {\n switch (name) {\n case 'htmlFor': return 'for';\n case 'httpEquiv': return 'http-equiv';\n case 'acceptCharset': return 'accept-charset';\n case 'defaultValue': return 'value';\n case 'defaultChecked': return 'checked';\n case 'defaultSelected': return 'selected';\n }\n // ARIA reflection: one dash after \"aria\", the rest lowercased\n // (ariaDescribedBy → aria-describedby, not aria-described-by).\n const c = name.charCodeAt(4);\n if (c >= 65 && c <= 90 && name.startsWith('aria')) return 'aria-' + name.slice(4).toLowerCase();\n return name.toLowerCase();\n}\n","/**\n * \"A tree is being built for renderToString()\" flag.\n *\n * True while renderToString() runs. Text children must keep their\n * `<!-- text-N -->` markers in this mode so the emitted HTML stays hydratable\n * — even when `isBrowser` is true (SSR under jsdom/happy-dom globals, or an\n * isomorphic worker that provides a DOM) — and nothing built for a throwaway\n * server tree may register itself anywhere (lifecycle hooks, reactive nodes,\n * list/when runtimes, scope roots). A pure client render leaves this false.\n *\n * Kept on globalThis under a Symbol.for key (like style/engine.ts's registry)\n * rather than in a module-level `let`: `nuclo` and `nuclo/ssr` ship as\n * separate bundles, each with its own module scope. renderToString() (in\n * nuclo/ssr) sets the flag and the tag builders (in nuclo) read it — as a\n * module variable those would be two unrelated booleans.\n */\nconst SERIALIZING_KEY = Symbol.for(\"nuclo.serializing.v1\");\nconst globalScope = globalThis as unknown as Record<symbol, { on: boolean } | undefined>;\nconst shared = globalScope[SERIALIZING_KEY] ?? (globalScope[SERIALIZING_KEY] = { on: false });\n\nexport function isSerializing(): boolean {\n return shared.on;\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 = shared.on;\n shared.on = true;\n try {\n return fn();\n } finally {\n shared.on = previous;\n }\n}\n","import { logError } from \"./errors\";\nimport { removeAllListeners } from \"../element/events\";\nimport { cleanupReactiveTextNode, cleanupReactiveElement } from \"../update/registry\";\nimport { disposeElementLifecycle, hasActiveLifecycleRegistrations } from \"../element/lifecycle\";\n\nexport const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n\n/**\n * Recursively removes all event listeners and reactive subscriptions from a node and its descendants\n * to prevent memory leaks when elements are removed from the DOM.\n */\nfunction cleanupNodeTree(node: Node): void {\n // Clean up the node itself based on its type. Order-independent bookkeeping\n // (listeners, reactive registrations) runs up front; onDestroy is fired\n // below, after descendants — see the comment there.\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n removeAllListeners(element);\n cleanupReactiveElement(element);\n } else if (node.nodeType === Node.TEXT_NODE) {\n cleanupReactiveTextNode(node as Text);\n }\n\n // Recursively clean up all child nodes\n // Destroy callbacks can mutate the live childNodes collection. Snapshot\n // only when hooks exist; ordinary cleanup needs no additional allocation.\n const children = hasActiveLifecycleRegistrations() ? Array.from(node.childNodes) : node.childNodes;\n for (let i = 0; i < children.length; i++) {\n cleanupNodeTree(children[i]);\n }\n\n // Fire onDestroy last — after every descendant's onDestroy has already run\n // (post-order) — so a parent's teardown can rely on its children having\n // already released whatever they were using (e.g. a subscription or timer\n // the parent owns and children only read from). This mirrors onMount's own\n // order, which is naturally parent-before-child (registration order is\n // construction order: a parent's own onMount/{onMount} modifier is applied\n // before its child factories are even invoked) — so mount is top-down,\n // destroy is bottom-up, the same LIFO discipline nested resource scopes\n // always use. No-op unless this exact element registered a callback.\n if (node.nodeType === Node.ELEMENT_NODE) {\n disposeElementLifecycle(node as HTMLElement);\n }\n}\n\n/**\n * Fires onDestroy for a subtree that list()'s bulk clear/replace is about to\n * drop without walking through cleanupNodeTree()/safeRemoveChild(). Callers\n * skip it entirely unless hasActiveLifecycleRegistrations(), so the fast path\n * stays fast when lifecycle hooks aren't in use.\n *\n * Same post-order (children, then self) as cleanupNodeTree() above, for the\n * same reason — see the comment there.\n */\nexport function disposeLifecyclesInSubtree(node: Node): void {\n const children = Array.from(node.childNodes);\n for (let i = 0; i < children.length; i++) {\n disposeLifecyclesInSubtree(children[i]);\n }\n if (node.nodeType === Node.ELEMENT_NODE) {\n disposeElementLifecycle(node as Element);\n }\n}\n\nexport function safeRemoveChild(child: Node): boolean {\n if (!child?.parentNode) return false;\n try {\n // Clean up all event listeners before removing the element\n cleanupNodeTree(child);\n // A destroy callback may already have removed the node.\n child.parentNode?.removeChild(child);\n return true;\n } catch (error) {\n logError('Failed to remove child node', error);\n return false;\n }\n}\n\nfunction safeInsertBefore(parent: Node, newNode: Node, referenceNode: Node | null): boolean {\n try {\n parent.insertBefore(newNode, referenceNode);\n return true;\n } catch (error) {\n logError('Failed to insert node before reference', error);\n return false;\n }\n}\n\nexport function createMarkerPair(prefix: string, id: number): { start: Comment; end: Comment } {\n return { start: document.createComment(`${prefix}-start-${id}`), end: document.createComment(`${prefix}-end`) };\n}\n\nexport function clearBetweenMarkers(startMarker: Comment, endMarker: Comment): void {\n let current = startMarker.nextSibling;\n while (current && current !== endMarker) {\n const next = current.nextSibling;\n safeRemoveChild(current);\n current = next;\n }\n}\n\nexport function insertNodesBefore(nodes: Node[], referenceNode: Node): void {\n const parent = referenceNode.parentNode;\n if (parent) {\n for (let i = 0; i < nodes.length; i++) {\n safeInsertBefore(parent, nodes[i], referenceNode);\n }\n }\n}\n\n/**\n * Creates a scoped DOM insertion context that temporarily redirects appendChild\n * to insertBefore at a specific reference node. This is useful for inserting\n * content at specific positions in the DOM tree.\n *\n * @param host - The host element\n * @param referenceNode - The node before which new nodes should be inserted\n * @param callback - Function to execute with the scoped context\n * @returns The result of the callback\n */\nexport function withScopedInsertion<T, THost extends Element | object>(\n host: THost,\n referenceNode: Node,\n callback: () => T\n): T {\n const parent = host as unknown as Node & ParentNode;\n const originalAppend = Object.getOwnPropertyDescriptor(parent, \"appendChild\");\n const originalInsert = parent.insertBefore;\n\n // Temporarily override appendChild to insert before the reference node\n // TypeScript doesn't like this override but it's safe at runtime\n (parent as unknown as Record<string, unknown>).appendChild = function(node: Node): Node {\n return originalInsert.call(parent, node, referenceNode);\n };\n\n try {\n return callback();\n } finally {\n // Restore original method\n if (originalAppend) Object.defineProperty(parent, \"appendChild\", originalAppend);\n else Reflect.deleteProperty(parent, \"appendChild\");\n }\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, propertyToAttribute } from '../shared/strings';\nimport { runSerializing } from '../shared/serializing';\nimport { SVG_NAMESPACE } from '../shared/dom';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | (() => NodeModFn<ElementTagName>)\n | Element\n | Node\n | null\n | undefined;\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/** The fields of the SSR polyfill's NucloElement that the serializer reads. */\ninterface PolyfillElementShape {\n id?: string;\n className?: string;\n namespaceURI?: string;\n children?: ArrayLike<Node>;\n _attributes?: Map<string, string>;\n _style?: Record<string, unknown>;\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, textarea: 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 unknown as PolyfillElementShape;\n const attrs = el._attributes;\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;\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. HTML elements: a camelCase key\n // (tabIndex, htmlFor, ariaDescribedBy) is an IDL property name that a real\n // element would have reflected to its content attribute (tabindex, for,\n // aria-describedby) — NucloElement has no such properties, so the key was\n // stored verbatim and is mapped here. SVG elements: browsers keep\n // setAttribute() names as-is (viewBox, preserveAspectRatio), so do we.\n if (attrs) {\n const svg = el.namespaceURI === SVG_NAMESPACE;\n for (const [name, value] of attrs) {\n // Rendered as the element's content, not as attributes — see serializeNode().\n if (name === 'innerHTML' || (textarea && name === 'value')) continue;\n result += serializeAttribute(svg ? name : propertyToAttribute(name), 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_RCDATA = 3;\nconst TAG_CATEGORY: Record<string, 1 | 2 | 3> = 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 &lt; 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 // Escapable raw-text elements — entities are decoded but markup is not\n // parsed, so a `<!-- text-N -->` marker would show up literally (in the\n // page title, in a textarea's value). Text only, escaped.\n title: TAG_RCDATA, textarea: TAG_RCDATA,\n});\n\n// One precompiled pattern per raw-text tag. Sharing a /g RegExp across calls\n// (and requests) is safe: String.prototype.replace resets lastIndex first.\nconst RAW_TEXT_CLOSE_RE: Record<string, RegExp> = { script: /<\\/(script)/gi, style: /<\\/(style)/gi };\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(RAW_TEXT_CLOSE_RE[tagName], '<\\\\/$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 // Duck-typed polyfill discriminator (also used by serializeAttributes): a\n // plain Array `children` field never occurs on a real DOM element\n // (HTMLCollection), only on NucloElement and polyfill-shaped test doubles.\n const isPolyfillShape = Array.isArray((element as unknown as PolyfillElementShape).children);\n // Always lowercased: NucloElement already is (toLowerCase() then returns\n // the same string), browser elements report \"DIV\". An `instanceof\n // NucloElement` shortcut only ever hit in-repo — the published nuclo/ssr\n // and nuclo/polyfill bundles hold different copies of the class.\n const tagName = element.tagName.toLowerCase();\n let category: 1 | 2 | 3 | undefined = TAG_CATEGORY[tagName];\n // <title> inside <svg> is an ordinary element — only HTML's is RCDATA.\n if (category === TAG_RCDATA && element.namespaceURI === SVG_NAMESPACE) category = undefined;\n const textarea = category === TAG_RCDATA && tagName === 'textarea';\n const attributes = serializeAttributes(element, isPolyfillShape, textarea);\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as unknown as PolyfillElementShape).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 || category === TAG_RCDATA) {\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 if (category === TAG_RAW_TEXT) {\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n if (textarea) {\n // A textarea's value is its content: `{ value }` (a property on a real\n // element, a stored attribute on the polyfill) wins over text children.\n const value = isPolyfillShape\n ? (element as unknown as PolyfillElementShape)._attributes?.get('value')\n : (element as HTMLTextAreaElement).value;\n if (typeof value === 'string' && value) rawContent = value;\n // The parser drops one newline right after the start tag.\n if (rawContent.charCodeAt(0) === 10) rawContent = '\\n' + rawContent;\n }\n return `<${tagName}${attributes}>${escapeText(rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children. `{ innerHTML }` is a property on a real\n // element (its parsed children are serialized below); the polyfill stores\n // it as an attribute, emitted here verbatim like the browser would parse it.\n const innerHTML = isPolyfillShape\n ? (element as unknown as PolyfillElementShape)._attributes?.get('innerHTML')\n : undefined;\n let childrenHtml = innerHTML === undefined ? '' : String(innerHTML);\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 if (innerHTML === undefined) {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as { textContent?: unknown }).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.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 'nuclo/polyfill';\n * import 'nuclo'; // tag builders such as div() are globals only\n * import { renderToString } from 'nuclo/ssr';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div><!-- text-0 -->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 const container = document.createElement('div') as unknown as ExpandedElement<ElementTagName>;\n let built: unknown = (input as NodeModFn<ElementTagName>)(container, 0);\n // render(App)-style component function: it returned the tree's\n // builder instead of the tree — build it (mirrors render()/hydrate()).\n if (typeof built === 'function') built = (built as NodeModFn<ElementTagName>)(container, 0);\n return built;\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.mjs","names":["globalScope","camelToKebab","firstUpper","str","code","result","i","ATTR_ESCAPE_RE","TEXT_ESCAPE_RE","escapeHtml","first","text","last","escaped","escapeText","propertyToAttribute","name","c","SERIALIZING_KEY","globalScope$1","shared","runSerializing","previous","fn","HTML_BOOLEAN_ATTRIBUTES","UNSAFE_ATTRIBUTE_NAME","htmlAttributeNames","htmlAttributeName","mapped","serializeAttribute","value","styleStr","key","val","serializeAttributes","isPolyfill","el","element","attrs","styleObj","css","svg","valueIsContent","attribute","attr","TAG_CATEGORY","RAW_TEXT_CLOSE_RE","escapeRawText","tagName","formStateAttributes","option","owner","select","input","textOf","children","serializeNode","node","data","isPolyfillShape","category","textarea","polyfillAttrs","attributes","selectValue","own","childNodes","rawContent","rawChildren","child","tc","innerHTML","childrenHtml","renderToString","container","built","error","renderManyToString","inputs","renderToStringWithContainer","content","containerTag","containerAttrs","createState","ENGINE_STATE_KEY","globalScope","cachedState","getState","getCssText","state","out","query","rules"],"sources":["../../src/shared/strings.ts","../../src/shared/serializing.ts","../../src/shared/dom.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 = '&amp;'; break; // &\n case 60: escaped = '&lt;'; break; // <\n case 62: escaped = '&gt;'; break; // >\n case 34: escaped = '&quot;'; break; // \"\n case 39: escaped = '&#039;'; 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 = '&amp;'; break; // &\n case 60: escaped = '&lt;'; break; // <\n case 62: escaped = '&gt;'; 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 * IDL property name → HTML content attribute name, for the camelCase keys a\n * real element reflects (tabIndex → tabindex, htmlFor → for,\n * ariaDescribedBy → aria-describedby, httpEquiv → http-equiv). Lowercase and\n * kebab-case names pass through unchanged. SSR runs this per polyfill attribute.\n */\nexport function propertyToAttribute(name: string): string {\n switch (name) {\n case 'htmlFor': return 'for';\n case 'httpEquiv': return 'http-equiv';\n case 'acceptCharset': return 'accept-charset';\n case 'defaultValue': return 'value';\n case 'defaultChecked': return 'checked';\n case 'defaultSelected': return 'selected';\n }\n // ARIA reflection: one dash after \"aria\", the rest lowercased\n // (ariaDescribedBy → aria-describedby, not aria-described-by).\n const c = name.charCodeAt(4);\n if (c >= 65 && c <= 90 && name.startsWith('aria')) return 'aria-' + name.slice(4).toLowerCase();\n return name.toLowerCase();\n}\n","/**\n * \"A tree is being built for renderToString()\" flag.\n *\n * True while renderToString() runs. Text children must keep their\n * `<!-- text-N -->` markers in this mode so the emitted HTML stays hydratable\n * — even when `isBrowser` is true (SSR under jsdom/happy-dom globals, or an\n * isomorphic worker that provides a DOM) — and nothing built for a throwaway\n * server tree may register itself anywhere (lifecycle hooks, reactive nodes,\n * list/when runtimes, scope roots). A pure client render leaves this false.\n *\n * Kept on globalThis under a Symbol.for key (like style/engine.ts's registry)\n * rather than in a module-level `let`: `nuclo` and `nuclo/ssr` ship as\n * separate bundles, each with its own module scope. renderToString() (in\n * nuclo/ssr) sets the flag and the tag builders (in nuclo) read it — as a\n * module variable those would be two unrelated booleans.\n */\nconst SERIALIZING_KEY = Symbol.for(\"nuclo.serializing.v1\");\nconst globalScope = globalThis as unknown as Record<symbol, { on: boolean } | undefined>;\nconst shared = globalScope[SERIALIZING_KEY] ?? (globalScope[SERIALIZING_KEY] = { on: false });\n\nexport function isSerializing(): boolean {\n return shared.on;\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 = shared.on;\n shared.on = true;\n try {\n return fn();\n } finally {\n shared.on = previous;\n }\n}\n","import { logError } from \"./errors\";\nimport { removeAllListeners } from \"../element/events\";\nimport { cleanupReactiveTextNode, cleanupReactiveElement } from \"../update/registry\";\nimport { disposeElementLifecycle, hasActiveLifecycleRegistrations } from \"../element/lifecycle\";\n\nexport const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n\n/**\n * Recursively removes all event listeners and reactive subscriptions from a node and its descendants\n * to prevent memory leaks when elements are removed from the DOM.\n */\nfunction cleanupNodeTree(node: Node): void {\n // Clean up the node itself based on its type. Order-independent bookkeeping\n // (listeners, reactive registrations) runs up front; onDestroy is fired\n // below, after descendants — see the comment there.\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n removeAllListeners(element);\n cleanupReactiveElement(element);\n } else if (node.nodeType === Node.TEXT_NODE) {\n cleanupReactiveTextNode(node as Text);\n }\n\n // Recursively clean up all child nodes\n // Destroy callbacks can mutate the live childNodes collection. Snapshot\n // only when hooks exist; ordinary cleanup needs no additional allocation.\n const children = hasActiveLifecycleRegistrations() ? Array.from(node.childNodes) : node.childNodes;\n for (let i = 0; i < children.length; i++) {\n cleanupNodeTree(children[i]);\n }\n\n // Fire onDestroy last — after every descendant's onDestroy has already run\n // (post-order) — so a parent's teardown can rely on its children having\n // already released whatever they were using (e.g. a subscription or timer\n // the parent owns and children only read from). This mirrors onMount's own\n // order, which is naturally parent-before-child (registration order is\n // construction order: a parent's own onMount/{onMount} modifier is applied\n // before its child factories are even invoked) — so mount is top-down,\n // destroy is bottom-up, the same LIFO discipline nested resource scopes\n // always use. No-op unless this exact element registered a callback.\n if (node.nodeType === Node.ELEMENT_NODE) {\n disposeElementLifecycle(node as HTMLElement);\n }\n}\n\n/**\n * Fires onDestroy for a subtree that list()'s bulk clear/replace is about to\n * drop without walking through cleanupNodeTree()/safeRemoveChild(). Callers\n * skip it entirely unless hasActiveLifecycleRegistrations(), so the fast path\n * stays fast when lifecycle hooks aren't in use.\n *\n * Same post-order (children, then self) as cleanupNodeTree() above, for the\n * same reason — see the comment there.\n */\nexport function disposeLifecyclesInSubtree(node: Node): void {\n const children = Array.from(node.childNodes);\n for (let i = 0; i < children.length; i++) {\n disposeLifecyclesInSubtree(children[i]);\n }\n if (node.nodeType === Node.ELEMENT_NODE) {\n disposeElementLifecycle(node as Element);\n }\n}\n\nexport function safeRemoveChild(child: Node): boolean {\n if (!child?.parentNode) return false;\n try {\n // Clean up all event listeners before removing the element\n cleanupNodeTree(child);\n // A destroy callback may already have removed the node.\n child.parentNode?.removeChild(child);\n return true;\n } catch (error) {\n logError('Failed to remove child node', error);\n return false;\n }\n}\n\nfunction safeInsertBefore(parent: Node, newNode: Node, referenceNode: Node | null): boolean {\n try {\n parent.insertBefore(newNode, referenceNode);\n return true;\n } catch (error) {\n logError('Failed to insert node before reference', error);\n return false;\n }\n}\n\n/**\n * Creates one of nuclo's own marker comments (text / list / when). The extra\n * argument is ignored by a real DOM; the SSR polyfill records it so\n * renderToString() can emit markers without inspecting their text.\n */\nexport function createMarker(text: string): Comment {\n return (document as unknown as { createComment(data: string, marker: boolean): Comment }).createComment(text, true);\n}\n\nexport function createMarkerPair(prefix: string, id: number): { start: Comment; end: Comment } {\n return { start: createMarker(`${prefix}-start-${id}`), end: createMarker(`${prefix}-end`) };\n}\n\nexport function clearBetweenMarkers(startMarker: Comment, endMarker: Comment): void {\n let current = startMarker.nextSibling;\n while (current && current !== endMarker) {\n const next = current.nextSibling;\n safeRemoveChild(current);\n current = next;\n }\n}\n\nexport function insertNodesBefore(nodes: Node[], referenceNode: Node): void {\n const parent = referenceNode.parentNode;\n if (parent) {\n for (let i = 0; i < nodes.length; i++) {\n safeInsertBefore(parent, nodes[i], referenceNode);\n }\n }\n}\n\n/**\n * Creates a scoped DOM insertion context that temporarily redirects appendChild\n * to insertBefore at a specific reference node. This is useful for inserting\n * content at specific positions in the DOM tree.\n *\n * @param host - The host element\n * @param referenceNode - The node before which new nodes should be inserted\n * @param callback - Function to execute with the scoped context\n * @returns The result of the callback\n */\nexport function withScopedInsertion<T, THost extends Element | object>(\n host: THost,\n referenceNode: Node,\n callback: () => T\n): T {\n const parent = host as unknown as Node & ParentNode;\n const originalAppend = Object.getOwnPropertyDescriptor(parent, \"appendChild\");\n const originalInsert = parent.insertBefore;\n\n // Temporarily override appendChild to insert before the reference node\n // TypeScript doesn't like this override but it's safe at runtime\n (parent as unknown as Record<string, unknown>).appendChild = function(node: Node): Node {\n return originalInsert.call(parent, node, referenceNode);\n };\n\n try {\n return callback();\n } finally {\n // Restore original method\n if (originalAppend) Object.defineProperty(parent, \"appendChild\", originalAppend);\n else Reflect.deleteProperty(parent, \"appendChild\");\n }\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, propertyToAttribute } from '../shared/strings';\nimport { runSerializing } from '../shared/serializing';\nimport { SVG_NAMESPACE } from '../shared/dom';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | (() => NodeModFn<ElementTagName>)\n | Element\n | Node\n | null\n | undefined;\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', 'disablepictureinpicture',\n 'disableremoteplayback', 'formnovalidate', 'hidden', 'inert', 'ismap',\n 'itemscope', 'loop', 'multiple', 'muted', 'nomodule', 'novalidate', 'open',\n 'playsinline', 'readonly', 'required', 'reversed', 'selected',\n]);\n\n/** The fields of the SSR polyfill's NucloElement that the serializer reads. */\ninterface PolyfillElementShape {\n id?: string;\n className?: string;\n namespaceURI?: string;\n children?: ArrayLike<Node>;\n _attributes?: Map<string, string>;\n _style?: Record<string, unknown>;\n}\n\n/**\n * Characters that cannot appear in an HTML attribute name. A name containing\n * one would end the attribute — or the tag — early and let the rest be parsed\n * as markup (`{ 'x><script>…': '' }`), so such attributes are dropped. The\n * browser refuses them too: setAttribute() throws on these names. Only names\n * that come from the app's own objects are checked — a real element's\n * attributes were validated by the DOM, and id/class/style are fixed.\n */\nconst UNSAFE_ATTRIBUTE_NAME = /[\\s\"'<>\\/=\\u0000-\\u001f\\u007f]/;\n\n/**\n * IDL property name → HTML attribute name, or '' for a name that must not be\n * emitted. The same few names repeat on every element, so both the mapping\n * and the safety check are paid once per distinct name. Bounded, so\n * attacker-chosen names cannot grow it without limit.\n */\nconst htmlAttributeNames = new Map<string, string>();\nfunction htmlAttributeName(name: string): string {\n let mapped = htmlAttributeNames.get(name);\n if (mapped === undefined) {\n mapped = UNSAFE_ATTRIBUTE_NAME.test(name) ? '' : propertyToAttribute(name);\n if (htmlAttributeNames.size < 1024) htmlAttributeNames.set(name, mapped);\n }\n return mapped;\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, valueIsContent: 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 unknown as PolyfillElementShape;\n const attrs = el._attributes;\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;\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. HTML elements: a camelCase key\n // (tabIndex, htmlFor, ariaDescribedBy) is an IDL property name that a real\n // element would have reflected to its content attribute (tabindex, for,\n // aria-describedby) — NucloElement has no such properties, so the key was\n // stored verbatim and is mapped here. SVG elements: browsers keep\n // setAttribute() names as-is (viewBox, preserveAspectRatio), so do we.\n if (attrs) {\n const svg = el.namespaceURI === SVG_NAMESPACE;\n for (const [name, value] of attrs) {\n // Rendered as the element's content, not as attributes — see serializeNode().\n if (name === 'innerHTML' || (valueIsContent && name === 'value')) continue;\n const attribute = htmlAttributeName(name);\n if (attribute === '') continue;\n result += serializeAttribute(svg ? name : attribute, 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_RCDATA = 3;\nconst TAG_SELECT = 4;\nconst TAG_OPTION = 5;\nconst TAG_OPTGROUP = 6;\ntype TagCategory = 1 | 2 | 3 | 4 | 5 | 6;\nconst TAG_CATEGORY: Record<string, TagCategory> = 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 &lt; 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 // Escapable raw-text elements — entities are decoded but markup is not\n // parsed, so a `<!-- text-N -->` marker would show up literally (in the\n // page title, in a textarea's value). Text only, escaped.\n title: TAG_RCDATA, textarea: TAG_RCDATA,\n // A select's `{ value }` is rendered as its matching option's `selected`.\n select: TAG_SELECT, option: TAG_OPTION, optgroup: TAG_OPTGROUP,\n});\n\n// One precompiled pattern per raw-text tag. Sharing a /g RegExp across calls\n// (and requests) is safe: String.prototype.replace resets lastIndex first.\nconst RAW_TEXT_CLOSE_RE: Record<string, RegExp> = { script: /<\\/(script)/gi, style: /<\\/(style)/gi };\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(RAW_TEXT_CLOSE_RE[tagName], '<\\\\/$1');\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\n/**\n * SSR on a real DOM (jsdom, happy-dom): a form control keeps what it shows in\n * properties — `input({ checked })`, `input({ value })` and a select's\n * selected option never become attributes there, so the markup would render\n * an unchecked box, an empty field and the first option. Emit the attributes\n * that make the parsed page show the same state. (The polyfill stores these\n * as attributes already.)\n */\nfunction formStateAttributes(element: Element): string {\n if ((element as HTMLOptionElement).selected !== undefined && element.localName === 'option') {\n const option = element as HTMLOptionElement;\n if (!option.selected || option.hasAttribute('selected')) return '';\n // The first option of a single-choice select is selected by default.\n // (Parent walk rather than closest(): jsdom caches selector matches on\n // the document, which would keep the serialized tree alive.)\n let owner = option.parentNode;\n if (owner && owner.nodeName === 'OPTGROUP') owner = owner.parentNode;\n const select = owner && owner.nodeName === 'SELECT' ? owner as HTMLSelectElement : null;\n return select && !select.multiple && select.options[0] === option ? '' : ' selected';\n }\n const input = element as HTMLInputElement;\n let result = '';\n if (input.checked === true && !input.hasAttribute('checked')) result += ' checked';\n const value = input.value;\n if (typeof value === 'string' && value !== '' && !input.hasAttribute('value')) {\n // An unset checkbox/radio reports the default value \"on\".\n const toggle = input.type === 'checkbox' || input.type === 'radio';\n if (!(toggle && value === 'on') && input.type !== 'file') result += serializeAttribute('value', value);\n }\n return result;\n}\n\n/** Concatenated data of an element's direct text children. */\nfunction textOf(element: Element): string {\n const children = (element as unknown as PolyfillElementShape).children;\n let text = '';\n if (children) {\n for (let i = 0; i < children.length; i++) {\n if (children[i].nodeType === 3) text += children[i].textContent || '';\n }\n }\n return text;\n}\n\n/**\n * `selectValue` is the `{ value }` of the enclosing <select> (polyfill trees\n * only): HTML has no value attribute for a select — the matching <option> is\n * marked `selected` instead.\n */\nfunction serializeNode(node: Node, selectValue?: string): 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. nuclo's own markers (flagged by the polyfill) are emitted\n // as they are. In a comment the app supplied, \"-->\" (or a leading \">\") would\n // end it early and expose the rest as markup, so \">\" is written as an\n // entity — comments do not decode entities.\n if (node.nodeType === 8) { // Node.COMMENT_NODE\n const data = node.textContent || '';\n return (node as { marker?: boolean }).marker === true || !data.includes('>')\n ? `<!--${data}-->`\n : `<!--${data.replace(/>/g, '&gt;')}-->`;\n }\n\n // Element node\n if (node.nodeType === 1) { // Node.ELEMENT_NODE\n const element = node as Element;\n // Duck-typed polyfill discriminator (also used by serializeAttributes): a\n // plain Array `children` field never occurs on a real DOM element\n // (HTMLCollection), only on NucloElement and polyfill-shaped test doubles.\n const isPolyfillShape = Array.isArray((element as unknown as PolyfillElementShape).children);\n // Always lowercased: NucloElement already is (toLowerCase() then returns\n // the same string), browser elements report \"DIV\". An `instanceof\n // NucloElement` shortcut only ever hit in-repo — the published nuclo/ssr\n // and nuclo/polyfill bundles hold different copies of the class.\n const tagName = element.tagName.toLowerCase();\n let category: TagCategory | undefined = TAG_CATEGORY[tagName];\n // <title> inside <svg> is an ordinary element — only HTML's is RCDATA.\n if (category === TAG_RCDATA && element.namespaceURI === SVG_NAMESPACE) category = undefined;\n const textarea = category === TAG_RCDATA && tagName === 'textarea';\n const polyfillAttrs = isPolyfillShape ? (element as unknown as PolyfillElementShape)._attributes : undefined;\n let attributes = serializeAttributes(element, isPolyfillShape, textarea || category === TAG_SELECT);\n if (category === TAG_SELECT) {\n const value = polyfillAttrs?.get('value');\n selectValue = value == null ? undefined : String(value);\n } else if (selectValue !== undefined) {\n if (category === TAG_OPTION) {\n // An option's value defaults to its text.\n const own = polyfillAttrs?.get('value') ?? textOf(element);\n if (String(own) === selectValue && !polyfillAttrs?.has('selected')) attributes += ' selected';\n }\n if (category !== TAG_OPTGROUP) selectValue = undefined;\n }\n if (!isPolyfillShape && (category === TAG_OPTION || tagName === 'input')) {\n attributes += formStateAttributes(element);\n }\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as unknown as PolyfillElementShape).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 || category === TAG_RCDATA) {\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 if (category === TAG_RAW_TEXT) {\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n if (textarea) {\n // A textarea's value is its content: `{ value }` (a property on a real\n // element, a stored attribute on the polyfill) wins over text children.\n const value = isPolyfillShape\n ? (element as unknown as PolyfillElementShape)._attributes?.get('value')\n : (element as HTMLTextAreaElement).value;\n if (typeof value === 'string' && value) rawContent = value;\n // The parser drops one newline right after the start tag.\n if (rawContent.charCodeAt(0) === 10) rawContent = '\\n' + rawContent;\n }\n return `<${tagName}${attributes}>${escapeText(rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children. `{ innerHTML }` is a property on a real\n // element (its parsed children are serialized below); the polyfill stores\n // it as an attribute, emitted here verbatim like the browser would parse it.\n const innerHTML = polyfillAttrs?.get('innerHTML');\n let childrenHtml = innerHTML === undefined ? '' : String(innerHTML);\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, selectValue);\n }\n }\n } else if (innerHTML === undefined) {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as { textContent?: unknown }).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.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 'nuclo/polyfill';\n * import 'nuclo'; // tag builders such as div() are globals only\n * import { renderToString } from 'nuclo/ssr';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div><!-- text-0 -->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 const container = document.createElement('div') as unknown as ExpandedElement<ElementTagName>;\n let built: unknown = (input as NodeModFn<ElementTagName>)(container, 0);\n // render(App)-style component function: it returned the tree's\n // builder instead of the tree — build it (mirrors render()/hydrate()).\n if (typeof built === 'function') built = (built as NodeModFn<ElementTagName>)(container, 0);\n return built;\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]) => (key === '' || UNSAFE_ATTRIBUTE_NAME.test(key) ? '' : 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.3.16",
4
+ "version": "0.3.18",
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)",
@@ -55,12 +55,12 @@
55
55
  "bench:when": "bun bench/when.bench.ts"
56
56
  },
57
57
  "devDependencies": {
58
- "@types/node": "^26.6.2",
59
- "@vitest/coverage-v8": "5.0.0",
58
+ "@types/node": "^26.6.3",
59
+ "@vitest/coverage-v8": "5.0.3",
60
60
  "jsdom": "^30.1.1",
61
61
  "tsdown": "^0.23.0",
62
62
  "typescript": "^7.0.2",
63
- "vitest": "^5.0.1"
63
+ "vitest": "^5.0.3"
64
64
  },
65
65
  "repository": {
66
66
  "type": "git",