nuclo 0.3.5 → 0.3.6

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":[],"sources":["../../src/shared/strings.ts","../../src/hydration.ts","../../src/ssr/render-to-string.ts","../../src/style/engine.ts"],"sourcesContent":["/**\n * camelCase → kebab-case for CSS property (and attribute) names. A leading\n * capital becomes a leading dash, as vendor prefixes need\n * (WebkitTransition → -webkit-transition). Char-code loop with a\n * no-uppercase fast path: SSR runs this per style declaration.\n */\nexport function camelToKebab(str: string): string {\n let firstUpper = 0;\n for (; firstUpper < str.length; firstUpper++) {\n const code = str.charCodeAt(firstUpper);\n if (code >= 65 && code <= 90) break;\n }\n if (firstUpper === str.length) return str;\n\n let result = str.slice(0, firstUpper);\n for (let i = firstUpper; i < str.length; i++) {\n const code = str.charCodeAt(i);\n // A-Z is 65-90, a-z is 97-122 (difference of 32)\n result += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];\n }\n return result;\n}\n\nconst ATTR_ESCAPE_RE = /[&<>\"']/;\nconst TEXT_ESCAPE_RE = /[&<>]/;\n\n/**\n * Escapes HTML special characters in attribute values (includes \" and ')\n */\nexport function escapeHtml(text: string): string {\n // Fast path: most strings contain no special characters at all.\n const first = ATTR_ESCAPE_RE.exec(text);\n if (!first) return text;\n\n let result = '';\n let last = 0;\n for (let i = first.index; i < text.length; i++) {\n let escaped: string;\n switch (text.charCodeAt(i)) {\n case 38: escaped = '&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 * Hydration context — tracks global hydration state and per-parent cursor positions.\n *\n * During hydration the component tree is built against an existing DOM tree\n * (produced by SSR). Instead of creating new nodes, factories claim existing\n * children from their parent element by advancing a cursor.\n *\n * The cursor is a *node pointer* (the next unclaimed child), not an index.\n * Indexed childNodes access is O(n) after any tree/attribute mutation in\n * jsdom (live NodeList re-materialization) which made hydration O(n²);\n * firstChild/nextSibling pointers are O(1) in every DOM implementation.\n */\n\nimport { safeRemoveChild } from \"./shared/dom\";\n\nlet _hydrating = false;\n// True while forceUpdate() re-hydrates a live, client-rendered tree: text\n// claims work without `<!-- text-N -->` markers and reused elements get their\n// previous listeners/class state reset before modifiers re-apply.\nlet _force = false;\n// True while renderToString() is building a tree destined for serialization.\nlet _serializing = false;\n// Missing entry = cursor at parent.firstChild; null = past the last child.\nlet _cursors = new WeakMap<Node, Node | null>();\n\nexport function isHydrating(): boolean {\n return _hydrating;\n}\n\n/** True only inside a forceUpdate() pass (never during plain hydrate()). */\nexport function isForceHydrating(): boolean {\n return _hydrating && _force;\n}\n\n/**\n * True while a tree is being built for SSR serialization (inside\n * renderToString). Text children must keep their `<!-- text-N -->` markers in\n * this mode so the emitted HTML stays hydratable — even when `isBrowser` is\n * true (SSR running under jsdom, or an isomorphic worker that provides a DOM).\n * A pure client render leaves this false and skips the markers.\n */\nexport function isSerializing(): boolean {\n return _serializing;\n}\n\n/**\n * Runs `fn` with serialization mode enabled, restoring the previous state\n * afterwards (so nested/re-entrant renderToString calls behave correctly).\n */\nexport function runSerializing<T>(fn: () => T): T {\n const previous = _serializing;\n _serializing = true;\n try {\n return fn();\n } finally {\n _serializing = previous;\n }\n}\n\nexport function startHydration(force = false): void {\n _hydrating = true;\n _force = force;\n // Each hydration pass starts with a clean cursor slate. Stale cursors from\n // a previous pass would desynchronize claims when a container's content is\n // replaced and re-hydrated (HMR, islands re-mounting).\n _cursors = new WeakMap<Node, Node | null>();\n}\n\nexport function endHydration(): void {\n _hydrating = false;\n _force = false;\n}\n\n/**\n * Runs a callback with hydration temporarily disabled, restoring the previous\n * state afterwards. Used when a server/client mismatch forces a subtree to be\n * rendered fresh in the middle of a hydration pass — fresh rendering must not\n * claim nodes from the surrounding SSR DOM.\n */\nexport function runWithoutHydration<T>(fn: () => T): T {\n const wasHydrating = _hydrating;\n _hydrating = false;\n try {\n return fn();\n } finally {\n _hydrating = wasHydrating;\n }\n}\n\n/**\n * Returns the next unclaimed child of a parent node (or null at the end)\n * without advancing the cursor.\n */\nexport function peekChild(parent: Node): Node | null {\n const entry = _cursors.get(parent);\n return entry === undefined ? parent.firstChild : entry;\n}\n\n/**\n * Sets the cursor to a specific child node (or null for \"past the end\").\n */\nexport function setCursor(parent: Node, node: Node | null): void {\n _cursors.set(parent, node);\n}\n\nfunction isWhitespaceText(node: Node): boolean {\n return node.nodeType === 3 && !/\\S/.test(node.textContent || '');\n}\n\n/**\n * Advances the cursor past whitespace-only text nodes.\n *\n * Nuclo's own SSR output never produces bare whitespace text nodes (text\n * children are always preceded by a `<!-- text-N -->` marker), so unmarked\n * whitespace comes from the surrounding template or an HTML\n * formatter/minifier. Skipping (without removing) keeps the cursor aligned\n * while leaving the document's visual whitespace untouched.\n */\nexport function skipWhitespaceText(parent: Node): void {\n let child = peekChild(parent);\n let advanced = false;\n while (child && isWhitespaceText(child)) {\n child = child.nextSibling;\n advanced = true;\n }\n if (advanced) {\n setCursor(parent, child);\n }\n}\n\n/**\n * Claims the next child node from parent at the current cursor position.\n * Advances the cursor by one.\n */\nexport function claimChild(parent: Node): Node | null {\n const child = peekChild(parent);\n if (child) {\n setCursor(parent, child.nextSibling);\n }\n return child;\n}\n\n/**\n * forceUpdate() text claim: claims the next child when it is a bare text node\n * (client renders emit no `<!-- text-N -->` markers). Whitespace-only text\n * that is followed by a text marker is SSR template formatting, not content —\n * it is left for the marker path, which skips it. Returns null when nothing\n * claimable is at the cursor.\n */\nexport function claimBareText(parent: Node): Text | null {\n const next = peekChild(parent);\n if (!next || next.nodeType !== 3) return null;\n if (isWhitespaceText(next)) {\n let sibling = next.nextSibling;\n while (sibling && isWhitespaceText(sibling)) sibling = sibling.nextSibling;\n if (\n sibling && sibling.nodeType === 8 &&\n (sibling.textContent ?? \"\").trimStart().startsWith(\"text-\")\n ) return null;\n }\n claimChild(parent);\n return next as Text;\n}\n\n/**\n * Claims a server-rendered `<!--{prefix}-start-…-->` … `<!--{prefix}-end-->`\n * block (list()/when()) at the cursor, leaving the cursor just after the start\n * marker. Returns null, claiming nothing, when the next child isn't a start\n * marker. Pairs are depth-counted because directly nested blocks share a\n * host. A missing end marker (corrupt/truncated SSR output) is recreated right\n * after the start marker and reported as `recreated`.\n */\nexport function claimMarkerPair(\n parent: Node,\n prefix: \"list\" | \"when\",\n): { start: Comment; end: Comment; recreated: boolean } | null {\n skipWhitespaceText(parent);\n const start = peekChild(parent);\n const startPrefix = prefix + \"-start-\";\n if (!start || start.nodeType !== 8 || !start.textContent?.startsWith(startPrefix)) return null;\n claimChild(parent);\n const endText = prefix + \"-end\";\n let depth = 0;\n for (let node = start.nextSibling; node; node = node.nextSibling) {\n if (node.nodeType !== 8) continue;\n const text = node.textContent;\n if (text === endText) {\n if (depth === 0) return { start: start as Comment, end: node as Comment, recreated: false };\n depth--;\n } else if (text?.startsWith(startPrefix)) {\n depth++;\n }\n }\n const end = document.createComment(endText);\n parent.insertBefore(end, start.nextSibling);\n return { start: start as Comment, end, recreated: true };\n}\n\n/**\n * Attempts to claim an existing element from the parent during hydration.\n * Returns the claimed element if the next child matches the expected tag, or null.\n */\nexport function claimElement(parent: Node, tagName: string): Element | null {\n if (!_hydrating) return null;\n skipWhitespaceText(parent);\n const candidate = peekChild(parent);\n if (candidate && candidate.nodeType === 1 && (candidate as Element).tagName.toLowerCase() === tagName) {\n return claimChild(parent) as Element;\n }\n return null;\n}\n\n/**\n * Removes unclaimed SSR children from a hydrated element.\n * Must be called after modifiers have been applied to a claimed element.\n * Removes nodes from the cursor through `lastOriginalChild` (the element's\n * last child before modifiers ran) — nodes appended by modifiers come after\n * that boundary and are preserved.\n */\nexport function cleanupUnclaimedChildren(node: Node, lastOriginalChild: Node | null): void {\n if (!lastOriginalChild) return;\n let current = peekChild(node);\n while (current) {\n const next = current === lastOriginalChild ? null : current.nextSibling;\n // safeRemoveChild (not a bare removeChild): during forceUpdate() the\n // unclaimed nodes were live — their listeners, registrations and\n // onDestroy hooks must be released, not just detached.\n safeRemoveChild(current);\n current = next;\n }\n setCursor(node, null);\n}\n","/**\n * Server-Side Rendering (SSR) utilities for Nuclo\n * Renders Nuclo components to HTML strings in Node.js environment\n */\n\nimport { escapeHtml, escapeText, camelToKebab } from '../shared/strings';\nimport { runSerializing } from '../hydration';\nimport { NucloElement } from '../polyfill/Element';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\n\n/**\n * SVG attributes that are natively camelCase and must NOT be converted to kebab-case.\n * Most SVG attributes are already kebab-case (stroke-width, fill-rule, etc.), but a\n * handful are defined as camelCase in the SVG spec and must be preserved.\n */\nconst SVG_PRESERVE_CASE_ATTRS = new Set([\n 'viewBox', 'preserveAspectRatio', 'markerWidth', 'markerHeight',\n 'gradientTransform', 'patternTransform', 'clipPathUnits', 'gradientUnits',\n 'patternUnits', 'pathLength', 'refX', 'refY', 'stdDeviation',\n 'baseFrequency', 'numOctaves', 'kernelMatrix', 'tableValues',\n 'targetX', 'targetY', 'specularExponent', 'specularConstant',\n 'diffuseConstant', 'surfaceScale', 'xChannelSelector', 'yChannelSelector',\n 'edgeMode', 'stitchTiles', 'spreadMethod', 'patternContentUnits',\n 'markerUnits', 'startOffset', 'textLength', 'lengthAdjust',\n]);\n\n/**\n * HTML boolean attributes — presence means true, absence means false.\n * When the stored value is the string \"true\" or \"false\" (from setAttribute),\n * we must re-apply boolean semantics instead of outputting the raw string.\n */\nconst HTML_BOOLEAN_ATTRIBUTES = new Set([\n 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'controls',\n 'default', 'defer', 'disabled', 'formnovalidate', 'hidden', 'ismap', 'loop',\n 'multiple', 'muted', 'nomodule', 'novalidate', 'open', 'readonly', 'required',\n 'reversed', 'selected',\n]);\n\n/**\n * Serializes a DOM attribute value\n */\nfunction serializeAttribute(name: string, value: unknown): string {\n if (value === null || value === undefined || value === false) {\n return '';\n }\n\n if (value === true) {\n return ` ${name}`;\n }\n\n // Boolean attributes stored as strings via setAttribute() need special handling\n if (HTML_BOOLEAN_ATTRIBUTES.has(name)) {\n if (value === 'false') return '';\n if (value === 'true' || value === '' || value === name) return ` ${name}`;\n }\n\n // A style object (e.g. renderToStringWithContainer's containerAttrs from\n // untyped JS): serialize its declarations, skipping nullish/empty values.\n if (name === 'style' && typeof value === 'object') {\n let styleStr = '';\n for (const key in value as Record<string, unknown>) {\n const val = (value as Record<string, unknown>)[key];\n if (val == null || val === '') continue;\n styleStr += (styleStr ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n return styleStr ? ` style=\"${escapeHtml(styleStr)}\"` : '';\n }\n\n return ` ${name}=\"${escapeHtml(String(value))}\"`;\n}\n\n/**\n * Serializes DOM element attributes to HTML string\n *\n * `isPolyfill` is passed in by the caller (serializeNode already computed it\n * to decide whether tagName needs lowercasing) so this doesn't repeat the\n * same `Array.isArray` discriminator check per element.\n */\nfunction serializeAttributes(element: Element, isPolyfill: boolean): string {\n let result = '';\n\n // Handle polyfill elements. NucloElement keeps every child in a plain Array\n // (`children`), which a real DOM element never does (HTMLCollection) — this is\n // the same allocation-free discriminator serializeNode() uses. Reading\n // `el.attributes` directly would lazily allocate an empty Map for every\n // element being serialized, so the backing `_attributes` field is read instead.\n if (isPolyfill) {\n const el = element as any;\n const attrs = el._attributes as Map<string, string> | undefined;\n\n // id — may live on the property rather than in the Map\n if (el.id && !attrs?.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // class — kept on .className, mirrored to Map only when setAttribute is used\n if (el.className && !attrs?.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // style — lives on the backing _style object (its own properties are the\n // declarations), not in the attributes Map. Read the field directly (never\n // `el.style`) so elements that never set a style are not forced to lazily\n // allocate an empty SSRStyle just to serialize. Empty values (e.g. reactive\n // styles that resolved to undefined) are skipped.\n const styleObj = el._style as Record<string, unknown> | undefined;\n if (styleObj && !attrs?.has('style')) {\n let css = '';\n for (const key in styleObj) {\n const val = styleObj[key];\n if (val == null || val === '') continue;\n css += (css ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n if (css) result += ` style=\"${escapeHtml(css)}\"`;\n }\n\n // All remaining attributes from the Map\n // Convert camelCase ARIA/HTML attribute names (e.g. ariaLabel → aria-label) that the\n // polyfill stores as-is because NucloElement lacks the browser property mappings.\n // SVG attributes that are natively camelCase (e.g. viewBox, preserveAspectRatio) must\n // NOT be converted — they are stored via setAttribute() as-is.\n if (attrs) {\n for (const [name, value] of attrs) {\n const htmlName = SVG_PRESERVE_CASE_ATTRS.has(name) ? name : camelToKebab(name);\n result += serializeAttribute(htmlName, value);\n }\n }\n return result;\n }\n\n // Handle browser elements with NamedNodeMap attributes\n if (element.attributes && element.attributes.length) {\n for (let i = 0; i < element.attributes.length; i++) {\n const attr = element.attributes[i];\n if (attr && attr.name) {\n result += serializeAttribute(attr.name, attr.value);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Tag-category lookup, keyed by lowercase tag name. Every element in a tree\n * hits this once, so void + raw-text elements are folded into a single\n * dictionary lookup instead of two separate `Set.has()` probes (one of which\n * would always miss for the other category).\n */\nconst TAG_VOID = 1;\nconst TAG_RAW_TEXT = 2;\nconst TAG_CATEGORY: Record<string, 1 | 2> = Object.assign(Object.create(null), {\n // Self-closing HTML tags that don't have closing tags\n area: TAG_VOID, base: TAG_VOID, br: TAG_VOID, col: TAG_VOID, embed: TAG_VOID,\n hr: TAG_VOID, img: TAG_VOID, input: TAG_VOID, link: TAG_VOID, meta: TAG_VOID,\n param: TAG_VOID, source: TAG_VOID, track: TAG_VOID, wbr: TAG_VOID,\n // Raw-text elements — browsers never decode entities inside them, so their\n // text content must be emitted verbatim (escaping would corrupt inline\n // JS/CSS, e.g. `a < b` becoming `a &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});\n\nfunction escapeRawText(tagName: string, text: string): string {\n // \"</script\" (any case) inside a script would close it — break the sequence\n // the same way JSON serializers do (\"<\\/script\").\n return text.replace(new RegExp('</(' + tagName + ')', 'gi'), '<\\\\/$1');\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node — only & < > need escaping; quotes are safe in text content\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeText(node.textContent || '');\n }\n\n // Comment node\n if (node.nodeType === 8) { // Node.COMMENT_NODE\n return `<!--${node.textContent || ''}-->`;\n }\n\n // Element node\n if (node.nodeType === 1) { // Node.ELEMENT_NODE\n const element = node as Element;\n const rawTagName = element.tagName;\n // Duck-typed polyfill discriminator (also used by serializeAttributes): a\n // plain Array `children` field never occurs on a real\n // DOM element (HTMLCollection), only on NucloElement — and on hand-rolled\n // polyfill-shaped test doubles, which is why this alone isn't enough to\n // skip re-lowercasing tagName below.\n const isPolyfillShape = Array.isArray((element as any).children);\n // Only a *real* NucloElement is guaranteed to have pre-lowercased tagName\n // (done once in its constructor) — re-lowercasing it here on every\n // element would be pure waste, since this is the hottest function in\n // renderToString. Anything else (browser Element, or a polyfill-shaped\n // object that didn't go through `new NucloElement()`) still needs it.\n const tagName = element instanceof NucloElement ? rawTagName : rawTagName.toLowerCase();\n const attributes = serializeAttributes(element, isPolyfillShape);\n const category = TAG_CATEGORY[tagName];\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as any).children : element.childNodes;\n\n // Raw-text elements: emit text verbatim (no entity escaping, no Nuclo\n // text markers — `<!--` would act as a line comment inside a script).\n if (category === TAG_RAW_TEXT) {\n let rawContent = '';\n const rawChildren = childNodes;\n if (rawChildren && rawChildren.length > 0) {\n for (let i = 0; i < rawChildren.length; i++) {\n const child = rawChildren[i];\n if (child && child.nodeType === 3) {\n rawContent += child.textContent || '';\n }\n }\n } else {\n const tc = (node as { textContent?: unknown }).textContent;\n if (typeof tc === 'string') rawContent = tc;\n }\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n if (childNodes && childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n childrenHtml += serializeNode(child);\n }\n }\n } else {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as any).textContent;\n if (typeof tc === 'string' && tc) {\n childrenHtml = escapeText(tc);\n }\n }\n\n return `<${tagName}${attributes}>${childrenHtml}</${tagName}>`;\n }\n\n // Document fragment\n if (node.nodeType === 11) { // Node.DOCUMENT_FRAGMENT_NODE\n let result = '';\n const childNodes: ArrayLike<Node> = (node as any).childNodes ?? [];\n if (childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n result += serializeNode(child);\n }\n }\n }\n return result;\n }\n\n return '';\n}\n\n/**\n * Renders a Nuclo component to an HTML string for server-side rendering\n *\n * @param input - A Nuclo component function, DOM element, or node\n * @returns HTML string representation of the component\n *\n * @example\n * ```ts\n * import { renderToString } from 'nuclo/ssr';\n * import { div } from 'nuclo';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div>Hello, World!</div>'\n * ```\n */\nexport function renderToString(input: RenderableInput): string {\n if (!input) return '';\n\n if (typeof input === 'function') {\n try {\n // Build the tree in serialization mode so text children keep their\n // <!-- text-N --> markers (needed for hydration) even when isBrowser is\n // true, e.g. SSR running under jsdom.\n const element = runSerializing(() => {\n if (typeof document === 'undefined') throw new Error('Document is not available. Make sure polyfills are loaded.');\n return input(document.createElement('div') as unknown as ExpandedElement<ElementTagName>, 0);\n });\n return element && typeof element === 'object' && 'nodeType' in element ? serializeNode(element as Node) : '';\n } catch (error) {\n console.error('Error rendering component to string:', error);\n return '';\n }\n }\n\n if ('nodeType' in input) {\n return serializeNode(input as Node);\n }\n\n return '';\n}\n\n/**\n * Renders multiple Nuclo components to HTML strings\n *\n * @param inputs - Array of Nuclo components\n * @returns Array of HTML strings\n */\nexport function renderManyToString(inputs: RenderableInput[]): string[] {\n return inputs.map(input => renderToString(input));\n}\n\n/**\n * Renders a Nuclo component and wraps it in a container element\n *\n * @param input - A Nuclo component\n * @param containerTag - The tag name for the container (default: 'div')\n * @param containerAttrs - Attributes for the container element\n * @returns HTML string with container wrapper\n */\nexport function renderToStringWithContainer(\n input: RenderableInput,\n containerTag: string = 'div',\n containerAttrs: Record<string, string> = {}\n): string {\n const content = renderToString(input);\n const attrs = Object.entries(containerAttrs)\n .map(([key, value]) => serializeAttribute(key, value))\n .join('');\n\n return `<${containerTag}${attrs}>${content}</${containerTag}>`;\n}\n","/**\n * Shared generated-CSS engine state.\n *\n * One stylesheet, one rule registry, shared by every `createCss` instance so\n * identical declarations dedupe globally and cascade order stays deterministic:\n * base rules first (insertion order), then one grouping rule per at-rule query\n * in registration order (theme `screens` order).\n *\n * Browser: rules are injected through CSSOM with O(1) appends — no rescans.\n * SSR: rules accumulate in the registry; `getCssText()` serializes them.\n *\n * The state below lives on `globalThis`, keyed by a `Symbol.for(...)` (the\n * global symbol registry, shared by every realm-local copy of this module).\n * `nuclo` publishes `.`, `./ssr`, and `./polyfill` as independently built\n * bundle files (see tsdown.config.ts) — a consumer that resolves them\n * through normal package resolution (any bundler/runtime that doesn't\n * alias every subpath to this same source file) loads two or three\n * separate copies of this module, each with its own module-scope\n * variables. Without a realm-wide singleton, `css()` (from `nuclo`) would\n * mint rules into one copy's registry while `getCssText()` (from\n * `nuclo/ssr`) reads an entirely different, always-empty one — SSR output\n * would render the right classes with no matching CSS at all. Storing the registry on `globalThis` instead makes\n * every copy of this module share the same state regardless of how many\n * times it ends up bundled.\n *\n * The registry never shrinks on its own: atomCache/atomMeta/rawKeys/\n * baseRules/queryRules retain every rule for the life of the process —\n * content-addressed dedup needs a durable record of what each class means,\n * and the engine has no way to know whether a minted class still backs a\n * live DOM node (it's deliberately decoupled from element/list lifecycle),\n * so it can't safely evict. Only resetStyles() (a manual test helper)\n * clears it. This is fine for the intended use — a bounded design-token\n * vocabulary — but a caller piping truly per-instance dynamic values\n * through css({ raw: {...} }) (e.g. per-row data in a list) mints one\n * permanent class per unique value; prefer the `style` prop\n * (element/inline-style.ts) for values that vary per instance rather than\n * per design token. See test/style/registry-growth.test.ts.\n */\n\nexport interface BlockMeta {\n\treadonly query: string | undefined;\n\treadonly suffix: string;\n\treadonly decls: ReadonlyArray<readonly [string, string]>;\n\t/** Author-supplied identity from `css(name, …)`; inherited by cx() merges. */\n\treadonly name: string | undefined;\n}\n\ninterface EngineState {\n\t// Registry — the source of truth for all generated CSS.\n\t// atomCache: declaration-block key -> class name (dedup)\n\t// atomMeta: class name -> what the class means (its selector context and its\n\t// declarations). This is what lets cx() merge two same-context classes at the\n\t// declaration level instead of guessing: the engine can always recover the\n\t// exact content of any class it minted.\n\tatomCache: Map<string, string>;\n\tatomMeta: Map<string, BlockMeta>;\n\trawKeys: Set<string>;\n\tbaseRules: string[];\n\tqueryRules: Map<string, string[]>;\n\tstyleEpoch: number;\n\n\t// Memoized getCssText() output. SSR calls getCssText() once per request, but the\n\t// rule set is stable after warmup — so the serialized sheet is recomputed only\n\t// when a new rule is actually recorded. Invalidated in record() and resetStyles().\n\tcssTextCache: string | null;\n\n\t// Browser CSSOM state. Base rules occupy [0, baseRuleCount); grouping rules\n\t// (one per query, created in registration order) live after them. CSSRule\n\t// references stay live when indices shift, so insertion never rescans.\n\tsheet: CSSStyleSheet | null;\n\tsheetDocument: Document | null;\n\tbaseRuleCount: number;\n\tgroupRules: Map<string, CSSGroupingRule>;\n\n\t// Normalized rules already sitting in a freshly-adopted stylesheet that this\n\t// module didn't insert itself — e.g. server-rendered CSS being hydrated.\n\t// Rebuilt on every fresh bind in ensureSheet(); null when there is nothing\n\t// external to guard against.\n\texternalRules: Map<string, Set<string>> | null;\n\thasExternalGroups: boolean;\n}\n\nfunction createState(): EngineState {\n\treturn {\n\t\tatomCache: new Map(),\n\t\tatomMeta: new Map(),\n\t\trawKeys: new Set(),\n\t\tbaseRules: [],\n\t\tqueryRules: new Map(),\n\t\tstyleEpoch: 0,\n\t\tcssTextCache: null,\n\t\tsheet: null,\n\t\tsheetDocument: null,\n\t\tbaseRuleCount: 0,\n\t\tgroupRules: new Map(),\n\t\texternalRules: null,\n\t\thasExternalGroups: false,\n\t};\n}\n\nconst ENGINE_STATE_KEY = Symbol.for(\"nuclo.style.engine.v1\");\nconst globalScope = globalThis as unknown as Record<symbol, EngineState>;\n// Importing the runtime does not allocate the registry until styling is used.\nlet cachedState: EngineState | undefined;\nfunction getState(): EngineState {\n\treturn cachedState ??= globalScope[ENGINE_STATE_KEY] ?? (globalScope[ENGINE_STATE_KEY] = createState());\n}\n\n/** Pre-register at-rule queries so their cascade order follows theme order. */\nexport function registerQueries(queries: Iterable<string>): void {\n\tfor (const query of queries) {\n\t\tconst state = getState();\n\t\tif (!state.queryRules.has(query)) state.queryRules.set(query, []);\n\t}\n}\n\nfunction createGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\ttry {\n\t\tconst idx = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", idx);\n\t\tconst group = s.cssRules[idx] as CSSGroupingRule;\n\t\tstate.groupRules.set(query, group);\n\t\tstate.groupRules.set(groupQueryOf(group), group);\n\t\treturn group;\n\t} catch {\n\t\treturn null; // unsupported at-rule in this environment\n\t}\n}\n\n// A group prelude is stable while its children grow. Reading cssText on each\n// insertion would serialize all previous children and make loading quadratic.\n// Weak keys allow detached stylesheets to be collected after reset/rebinding.\nconst groupQueries = new WeakMap<CSSRule, string>();\nfunction groupQueryOf(rule: CSSRule): string {\n\tconst cached = groupQueries.get(rule);\n\tif (cached !== undefined) return cached;\n\tconst text = rule.cssText;\n\tconst brace = text.indexOf(\"{\");\n\tconst query = (brace === -1 ? text : text.slice(0, brace)).trim();\n\tgroupQueries.set(rule, query);\n\treturn query;\n}\n\nfunction ruleIdentity(rule: CSSRule): string {\n\treturn rule instanceof CSSStyleRule ? rule.selectorText : groupQueryOf(rule);\n}\n\nfunction rememberExternalRule(rule: CSSRule, query: string | undefined): void {\n\tconst state = getState();\n\tif (!state.externalRules) return;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tlet texts = state.externalRules.get(key);\n\tif (!texts) {\n\t\ttexts = new Set();\n\t\tstate.externalRules.set(key, texts);\n\t}\n\ttexts.add(rule.cssText);\n}\n\nfunction consumeExternalRule(rule: CSSRule, query: string | undefined): boolean {\n\tconst state = getState();\n\tif (!state.externalRules) return false;\n\tconst key = contextKeyOf(query, ruleIdentity(rule));\n\tconst texts = state.externalRules.get(key);\n\tif (!texts?.delete(rule.cssText)) return false;\n\tif (texts.size === 0) state.externalRules.delete(key);\n\tif (state.externalRules.size === 0) state.externalRules = null;\n\treturn true;\n}\n\n/**\n * Index an already-populated stylesheet found on adopt (SSR output, most\n * commonly): every top-level conditional grouping rule (@media/@container/…)\n * is registered into groupRules (by its reconstructed at-rule text) so later\n * record() calls append into it instead of creating a sibling duplicate. The\n * other rules are indexed by scope, selector/prelude, and normalized cssText\n * so exact server/client matches can be skipped without hiding a changed\n * named or global rule that happens to reuse the same selector.\n */\nfunction indexExternalRules(rules: CSSRuleList): void {\n\tconst state = getState();\n\tfor (let i = 0; i < rules.length; i++) {\n\t\tconst rule = rules[i];\n\t\tif (rule instanceof CSSStyleRule) {\n\t\t\trememberExternalRule(rule, undefined);\n\t\t\tcontinue;\n\t\t}\n\t\tconst prelude = groupQueryOf(rule);\n\t\tif (\"cssRules\" in rule && !prelude.startsWith(\"@keyframes\")) {\n\t\t\tconst group = rule as CSSGroupingRule;\n\t\t\tstate.hasExternalGroups = true;\n\t\t\tstate.groupRules.set(prelude, group);\n\t\t\tfor (let j = 0; j < group.cssRules.length; j++) {\n\t\t\t\trememberExternalRule(group.cssRules[j], prelude);\n\t\t\t}\n\t\t} else {\n\t\t\t// @keyframes, @font-face, … — one-shot rules matched by prelude.\n\t\t\trememberExternalRule(rule, undefined);\n\t\t}\n\t}\n}\n\nfunction insertBase(s: CSSStyleSheet, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\ts.insertRule(rule, state.baseRuleCount);\n\t\tconst inserted = s.cssRules[state.baseRuleCount];\n\t\tif (consumeExternalRule(inserted, undefined)) {\n\t\t\ts.deleteRule(state.baseRuleCount);\n\t\t\treturn;\n\t\t}\n\t\tstate.baseRuleCount++;\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction insertGrouped(group: CSSGroupingRule, rule: string): void {\n\tconst state = getState();\n\ttry {\n\t\tconst index = group.cssRules.length;\n\t\tgroup.insertRule(rule, index);\n\t\tconst inserted = group.cssRules[index];\n\t\tif (state.externalRules && consumeExternalRule(inserted, groupQueryOf(group))) group.deleteRule(index);\n\t} catch {\n\t\t// Invalid value/selector: drop the declaration, like browsers do for bad CSS.\n\t}\n}\n\nfunction getOrCreateGroup(s: CSSStyleSheet, query: string): CSSGroupingRule | null {\n\tconst state = getState();\n\tconst direct = state.groupRules.get(query);\n\tif (direct) return direct;\n\tif (!state.hasExternalGroups) return createGroup(s, query);\n\n\t// CSSOM may normalize whitespace in an at-rule prelude. Normalize the\n\t// authored query through a temporary empty group before deciding that an\n\t// adopted SSR group is missing.\n\ttry {\n\t\tconst index = s.cssRules.length;\n\t\ts.insertRule(query + \"{}\", index);\n\t\tconst normalized = groupQueryOf(s.cssRules[index]);\n\t\ts.deleteRule(index);\n\t\tconst existing = state.groupRules.get(normalized);\n\t\tif (existing) {\n\t\t\tstate.groupRules.set(query, existing);\n\t\t\treturn existing;\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n\treturn createGroup(s, query);\n}\n\n/**\n * Returns the live stylesheet, (re)binding when the document changes — e.g.\n * fresh jsdom documents between tests. Rebinding replays the whole registry,\n * which replaces v1's per-call classExistsInDOM() DOM scans with a single\n * replay per document.\n */\nexport function ensureSheet(): CSSStyleSheet | null {\n\tconst state = getState();\n\tif (typeof document === \"undefined\") return null;\n\t// The nuclo SSR polyfill provides a document without getElementById — in\n\t// that context rules accumulate in the registry for getCssText() only.\n\tif (typeof document.getElementById !== \"function\") return null;\n\tif (\n\t\tstate.sheet &&\n\t\tstate.sheetDocument === document &&\n\t\tstate.sheet.ownerNode &&\n\t\t(state.sheet.ownerNode as Element).isConnected\n\t) {\n\t\treturn state.sheet;\n\t}\n\tconst existing = document.getElementById(\"nuclo-styles\");\n\tlet el: HTMLStyleElement;\n\tif (existing?.tagName === \"STYLE\") {\n\t\tel = existing as HTMLStyleElement;\n\t} else {\n\t\tel = document.createElement(\"style\");\n\t\tel.id = \"nuclo-styles\";\n\t\tif (existing) existing.replaceWith(el);\n\t\telse document.head.appendChild(el);\n\t}\n\tstate.sheet = el.sheet;\n\tstate.sheetDocument = document;\n\tstate.groupRules.clear();\n\tstate.baseRuleCount = 0;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tif (!state.sheet) return null;\n\tif (state.sheet.cssRules.length > 0) {\n\t\t// Leading top-level non-group rules (style rules, @keyframes — before\n\t\t// the first @media/@container group) are this element's existing\n\t\t// \"base\" region — count them so insertBase() keeps inserting new base\n\t\t// rules right after them, still ahead of every group.\n\t\tlet i = 0;\n\t\twhile (i < state.sheet.cssRules.length) {\n\t\t\tconst r = state.sheet.cssRules[i];\n\t\t\tif (!(r instanceof CSSStyleRule) && \"cssRules\" in r && !groupQueryOf(r).startsWith(\"@keyframes\")) break;\n\t\t\ti++;\n\t\t}\n\t\tstate.baseRuleCount = i;\n\t\tstate.externalRules = new Map();\n\t\tindexExternalRules(state.sheet.cssRules);\n\t}\n\tfor (const rule of state.baseRules) insertBase(state.sheet, rule);\n\tfor (const [query, rules] of state.queryRules) {\n\t\tconst group = getOrCreateGroup(state.sheet, query);\n\t\tif (group) for (const rule of rules) insertGrouped(group, rule);\n\t}\n\treturn state.sheet;\n}\n\nfunction record(rule: string, query: string | undefined): void {\n\tconst state = getState();\n\t// Bind/replay *before* this rule joins the registry, so a fresh bind's\n\t// replay only ever covers previously-known rules — never this one twice.\n\tconst s = ensureSheet();\n\n\tif (query === undefined) {\n\t\tstate.baseRules.push(rule);\n\t} else {\n\t\tlet bucket = state.queryRules.get(query);\n\t\tif (!bucket) {\n\t\t\tbucket = [];\n\t\t\tstate.queryRules.set(query, bucket);\n\t\t}\n\t\tbucket.push(rule);\n\t}\n\tstate.cssTextCache = null; // a new rule changes the serialized sheet\n\n\tif (s) {\n\t\tif (query === undefined) {\n\t\t\tinsertBase(s, rule);\n\t\t} else {\n\t\t\tconst group = getOrCreateGroup(s, query);\n\t\t\tif (group) insertGrouped(group, rule);\n\t\t}\n\t}\n}\n\n/**\n * Content-addressed ~42-bit hash (two interleaved FNV-1a style passes). Class\n * names must match between server and client for SSR hydration, so a counter\n * is not an option; 42 bits keeps collision odds low (~1e-5 at 10k\n * unique declaration blocks).\n */\nexport function hash(input: string): string {\n\tlet a = 0x811c9dc5;\n\tlet b = 0x7ee3623b;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst c = input.charCodeAt(i);\n\t\ta = Math.imul(a ^ c, 0x01000193);\n\t\tb = Math.imul(b ^ c, 0x85ebca6b);\n\t}\n\treturn (a >>> 0).toString(36) + ((b >>> 9) % 1296).toString(36);\n}\n\n/** Length-prefixed (query, selector-suffix) key — unambiguous for any strings. */\nexport function contextKeyOf(query: string | undefined, suffix: string): string {\n\tconst queryPart = query === undefined ? \"-\" : \"+\" + query.length + \":\" + query;\n\treturn queryPart + suffix.length + \":\" + suffix;\n}\n\nfunction appendKeyPart(key: string, value: string): string {\n\treturn key + value.length + \":\" + value;\n}\n\nexport function expandAmpersands(input: string, replacement: string): string {\n\tlet expanded = \"\";\n\tlet quote = 0;\n\tfor (let i = 0; i < input.length; i++) {\n\t\tconst code = input.charCodeAt(i);\n\t\tif (code === 92 /* \\\\ */ && i + 1 < input.length) {\n\t\t\texpanded += input[i] + input[++i];\n\t\t\tcontinue;\n\t\t}\n\t\tif (quote !== 0) {\n\t\t\texpanded += input[i];\n\t\t\tif (code === quote) quote = 0;\n\t\t\tcontinue;\n\t\t}\n\t\tif (code === 34 /* \" */ || code === 39 /* ' */) {\n\t\t\tquote = code;\n\t\t\texpanded += input[i];\n\t\t} else {\n\t\t\texpanded += code === 38 /* & */ ? replacement : input[i];\n\t\t}\n\t}\n\treturn expanded;\n}\n\nfunction selectorFor(className: string, suffix: string): string {\n\tconst selector = \".\" + className;\n\treturn selector + expandAmpersands(suffix, selector);\n}\n\n/**\n * True for strings usable verbatim as an unescaped CSS class selector:\n * letters/digits/`-`/`_`, not starting with a digit or `-`+digit.\n * Char-code loop rather than a RegExp — this runs on the cold mint path but\n * stays allocation-free either way.\n */\nfunction isValidClassName(name: string): boolean {\n\tif (name.length === 0 || name === \"-\") return false;\n\tfor (let i = 0; i < name.length; i++) {\n\t\tconst c = name.charCodeAt(i);\n\t\tconst ok =\n\t\t\t(c >= 97 && c <= 122) || // a-z\n\t\t\t(c >= 65 && c <= 90) || // A-Z\n\t\t\tc === 45 || c === 95 || // - _\n\t\t\t(i > 0 && c >= 48 && c <= 57); // 0-9, never leading\n\t\tif (!ok) return false;\n\t}\n\t// \"-9foo\" is not a valid identifier start either.\n\tif (name.charCodeAt(0) === 45 && name.length > 1) {\n\t\tconst second = name.charCodeAt(1);\n\t\tif (second >= 48 && second <= 57) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Mint (or reuse) the class for a declaration block in a selector context.\n *\n * Declarations keep their authored order — CSS resolves shorthand/longhand\n * and repeated-property conflicts by declaration order, so reordering (e.g.\n * alphabetical sorting) would silently change what `{ pt: 20, p: 16 }` means.\n * The cost is that the same declarations authored in a different order mint a\n * second, content-identical class — harmless, and rare in practice.\n *\n * `name` is the author's identity for the style (`css(\"app-root\", …)`). It\n * becomes part of the cache key, so two different names never share a class\n * even with identical declarations. With `exactName`, this block *is* the\n * named one and takes the name verbatim; otherwise the name is only a\n * readable prefix on a content-addressed prefix (`app-root-1a2b3c`), which is\n * how a named style's non-base contexts and its cx() merges stay identifiable\n * in devtools without ever colliding with the base class.\n */\nexport function atomBlock(\n\tquery: string | undefined,\n\tsuffix: string,\n\tdecls: ReadonlyArray<readonly [string, string]>,\n\tname?: string,\n\texactName = false,\n): string {\n\tconst state = getState();\n\tlet contextKey = contextKeyOf(query, suffix);\n\tfor (const [prop, value] of decls) {\n\t\tcontextKey = appendKeyPart(appendKeyPart(contextKey, prop), value);\n\t}\n\tif (name !== undefined && !isValidClassName(name)) {\n\t\tconsole.warn(\n\t\t\t`[nuclo] css() name ${JSON.stringify(name)} is not a valid CSS class name ` +\n\t\t\t\t\"(letters, digits, - and _ only, not starting with a digit); falling back to a generated name.\",\n\t\t);\n\t\tname = undefined;\n\t}\n\tconst declKey = name === undefined ? \"a\" + contextKey : \"n\" + appendKeyPart(\"\", name) + contextKey;\n\tlet className = state.atomCache.get(declKey);\n\tif (className !== undefined) {\n\t\t// Touch the sheet so a swapped document (tests) gets the replayed rules.\n\t\tensureSheet();\n\t\treturn className;\n\t}\n\n\tif (name === undefined) {\n\t\tclassName = \"n\" + hash(declKey);\n\t} else {\n\t\tclassName = exactName ? name : name + \"-\" + hash(declKey);\n\t\tif (state.atomMeta.has(className)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[nuclo] css() name ${JSON.stringify(className)} is already used by a different style. ` +\n\t\t\t\t\t\"Both rules are emitted under the same class, so they will override each other — rename one.\",\n\t\t\t);\n\t\t}\n\t}\n\n\tlet body = \"\";\n\tfor (const [prop, value] of decls) body += (body ? \";\" : \"\") + prop + \":\" + value;\n\tstate.atomCache.set(declKey, className);\n\tstate.atomMeta.set(className, { query, suffix, decls, name });\n\trecord(selectorFor(className, suffix) + \"{\" + body + \"}\", query);\n\treturn className;\n}\n\n/**\n * Merge two same-context generated classes into one: `second`'s declarations\n * override `first`'s per exact property (shorthand/longhand interplay is left\n * to CSS order), and the combined block mints its own content-addressed class\n * — so cx(base, override) keeps base's untouched properties instead of\n * dropping the whole base class. Deterministic: the same pair always yields\n * the same class name, on server and client alike. A named base passes its\n * name down as a prefix, so `cx(appRoot, active)` reads as `app-root-1a2b3c`.\n */\nexport function mergeBlocks(first: string, second: string): string {\n\tconst state = getState();\n\tconst a = state.atomMeta.get(first);\n\tconst b = state.atomMeta.get(second);\n\tif (!a || !b) return second; // unknown input — nothing to merge with\n\tconst concat = [...a.decls, ...b.decls];\n\t// Keep only the last occurrence of each exact property, at its position —\n\t// so a later `padding` still fully resets an earlier `padding-top`.\n\tconst lastIndex = new Map<string, number>();\n\tfor (let i = 0; i < concat.length; i++) lastIndex.set(concat[i][0], i);\n\tconst merged = concat.filter((decl, i) => lastIndex.get(decl[0]) === i);\n\treturn atomBlock(a.query, a.suffix, merged, a.name ?? b.name);\n}\n\n/** Conflict key (query|suffix) for a generated class — lets cx() group classes by selector context. */\nexport function conflictKeyOf(className: string): string | undefined {\n\tconst state = getState();\n\tconst meta = state.atomMeta.get(className);\n\treturn meta === undefined ? undefined : contextKeyOf(meta.query, meta.suffix);\n}\n\n/** Current registry generation, used to invalidate per-instance WeakMap hits after resetStyles(). */\nexport function getStyleEpoch(): number {\n\treturn (cachedState ?? globalScope[ENGINE_STATE_KEY])?.styleEpoch ?? 0;\n}\n\n/** Register a non-atomic rule (keyframes, global styles) once per dedupe key. */\nexport function addRawRule(dedupeKey: string, rule: string): void {\n\tconst state = getState();\n\tif (state.rawKeys.has(dedupeKey)) {\n\t\tensureSheet();\n\t\treturn;\n\t}\n\tstate.rawKeys.add(dedupeKey);\n\trecord(rule, undefined);\n}\n\n/** All generated CSS — base rules first, then at-rule groups in registration order. */\nexport function getCssText(): string {\n\tconst state = getState();\n\tif (state.cssTextCache !== null) return state.cssTextCache;\n\tlet out = state.baseRules.join(\"\");\n\tfor (const [query, rules] of state.queryRules) {\n\t\tif (rules.length > 0) out += query + \"{\" + rules.join(\"\") + \"}\";\n\t}\n\tstate.cssTextCache = out;\n\treturn out;\n}\n\n/** Clear all engine state (test helper). Removes the injected style element. */\nexport function resetStyles(): void {\n\tconst state = getState();\n\tstate.atomCache.clear();\n\tstate.atomMeta.clear();\n\tstate.rawKeys.clear();\n\tstate.baseRules.length = 0;\n\tstate.queryRules.clear();\n\tstate.groupRules.clear();\n\tstate.cssTextCache = null;\n\tstate.baseRuleCount = 0;\n\tstate.sheet = null;\n\tstate.sheetDocument = null;\n\tstate.externalRules = null;\n\tstate.hasExternalGroups = false;\n\tstate.styleEpoch++;\n\tif (typeof document !== \"undefined\") {\n\t\tdocument.getElementById(\"nuclo-styles\")?.remove();\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"nuclo.ssr.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/**\n * Serializes a DOM attribute value\n */\nfunction serializeAttribute(name: string, value: unknown): string {\n if (value === null || value === undefined || value === false) {\n return '';\n }\n\n if (value === true) {\n return ` ${name}`;\n }\n\n // Boolean attributes stored as strings via setAttribute() need special handling\n if (HTML_BOOLEAN_ATTRIBUTES.has(name)) {\n if (value === 'false') return '';\n if (value === 'true' || value === '' || value === name) return ` ${name}`;\n }\n\n // A style object (e.g. renderToStringWithContainer's containerAttrs from\n // untyped JS): serialize its declarations, skipping nullish/empty values.\n if (name === 'style' && typeof value === 'object') {\n let styleStr = '';\n for (const key in value as Record<string, unknown>) {\n const val = (value as Record<string, unknown>)[key];\n if (val == null || val === '') continue;\n styleStr += (styleStr ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n return styleStr ? ` style=\"${escapeHtml(styleStr)}\"` : '';\n }\n\n return ` ${name}=\"${escapeHtml(String(value))}\"`;\n}\n\n/**\n * Serializes DOM element attributes to HTML string\n *\n * `isPolyfill` is passed in by the caller (serializeNode already computed it\n * to decide whether tagName needs lowercasing) so this doesn't repeat the\n * same `Array.isArray` discriminator check per element.\n */\nfunction serializeAttributes(element: Element, isPolyfill: boolean): string {\n let result = '';\n\n // Handle polyfill elements. NucloElement keeps every child in a plain Array\n // (`children`), which a real DOM element never does (HTMLCollection) — this is\n // the same allocation-free discriminator serializeNode() uses. Reading\n // `el.attributes` directly would lazily allocate an empty Map for every\n // element being serialized, so the backing `_attributes` field is read instead.\n if (isPolyfill) {\n const el = element as any;\n const attrs = el._attributes as Map<string, string> | undefined;\n\n // id — may live on the property rather than in the Map\n if (el.id && !attrs?.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // class — kept on .className, mirrored to Map only when setAttribute is used\n if (el.className && !attrs?.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // style — lives on the backing _style object (its own properties are the\n // declarations), not in the attributes Map. Read the field directly (never\n // `el.style`) so elements that never set a style are not forced to lazily\n // allocate an empty SSRStyle just to serialize. Empty values (e.g. reactive\n // styles that resolved to undefined) are skipped.\n const styleObj = el._style as Record<string, unknown> | undefined;\n if (styleObj && !attrs?.has('style')) {\n let css = '';\n for (const key in styleObj) {\n const val = styleObj[key];\n if (val == null || val === '') continue;\n css += (css ? ' ' : '') + camelToKebab(key) + ': ' + String(val) + ';';\n }\n if (css) result += ` style=\"${escapeHtml(css)}\"`;\n }\n\n // All remaining attributes from the Map. 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 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_CATEGORY: Record<string, 1 | 2> = Object.assign(Object.create(null), {\n // Self-closing HTML tags that don't have closing tags\n area: TAG_VOID, base: TAG_VOID, br: TAG_VOID, col: TAG_VOID, embed: TAG_VOID,\n hr: TAG_VOID, img: TAG_VOID, input: TAG_VOID, link: TAG_VOID, meta: TAG_VOID,\n param: TAG_VOID, source: TAG_VOID, track: TAG_VOID, wbr: TAG_VOID,\n // Raw-text elements — browsers never decode entities inside them, so their\n // text content must be emitted verbatim (escaping would corrupt inline\n // JS/CSS, e.g. `a < b` becoming `a &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});\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 any).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 const attributes = serializeAttributes(element, isPolyfillShape);\n const category = TAG_CATEGORY[tagName];\n\n // Self-closing tags\n if (category === TAG_VOID) {\n return `<${tagName}${attributes} />`;\n }\n\n const childNodes: ArrayLike<Node> = isPolyfillShape ? (element as any).children : element.childNodes;\n\n // Raw-text elements: emit text verbatim (no entity escaping, no Nuclo\n // text markers — `<!--` would act as a line comment inside a script).\n if (category === TAG_RAW_TEXT) {\n let rawContent = '';\n const rawChildren = childNodes;\n if (rawChildren && rawChildren.length > 0) {\n for (let i = 0; i < rawChildren.length; i++) {\n const child = rawChildren[i];\n if (child && child.nodeType === 3) {\n rawContent += child.textContent || '';\n }\n }\n } else {\n const tc = (node as { textContent?: unknown }).textContent;\n if (typeof tc === 'string') rawContent = tc;\n }\n return `<${tagName}${attributes}>${escapeRawText(tagName, rawContent)}</${tagName}>`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n if (childNodes && childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n childrenHtml += serializeNode(child);\n }\n }\n } else {\n // Fallback: textContent set directly on the element (e.g. el.textContent = \"...\")\n const tc = (node as any).textContent;\n if (typeof tc === 'string' && tc) {\n childrenHtml = escapeText(tc);\n }\n }\n\n return `<${tagName}${attributes}>${childrenHtml}</${tagName}>`;\n }\n\n // Document fragment\n if (node.nodeType === 11) { // Node.DOCUMENT_FRAGMENT_NODE\n let result = '';\n const childNodes: ArrayLike<Node> = (node as any).childNodes ?? [];\n if (childNodes.length > 0) {\n for (let i = 0; i < childNodes.length; i++) {\n const child = childNodes[i];\n if (child) {\n result += serializeNode(child);\n }\n }\n }\n return result;\n }\n\n return '';\n}\n\n/**\n * Renders a Nuclo component to an HTML string for server-side rendering\n *\n * @param input - A Nuclo component function, DOM element, or node\n * @returns HTML string representation of the component\n *\n * @example\n * ```ts\n * import { renderToString } from 'nuclo/ssr';\n * import { div } from 'nuclo';\n *\n * const html = renderToString(\n * div(\"Hello, World!\")\n * );\n * // Returns: '<div>Hello, World!</div>'\n * ```\n */\nexport function renderToString(input: RenderableInput): string {\n if (!input) return '';\n\n if (typeof input === 'function') {\n try {\n // Build the tree in serialization mode so text children keep their\n // <!-- text-N --> markers (needed for hydration) even when isBrowser is\n // true, e.g. SSR running under jsdom.\n const element = runSerializing(() => {\n if (typeof document === 'undefined') throw new Error('Document is not available. Make sure polyfills are loaded.');\n 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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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuclo",
3
3
  "private": false,
4
- "version": "0.3.5",
4
+ "version": "0.3.6",
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)",
package/types/ssr.d.ts CHANGED
@@ -11,9 +11,13 @@
11
11
  // so SSR-only consumers get them without importing the main entry.
12
12
  import "./index";
13
13
 
14
- /** Anything renderToString accepts: a component factory, a DOM node, or nothing. */
14
+ /**
15
+ * Anything renderToString accepts: a built tree (`App()`), a component
16
+ * function (`App`, as render()/hydrate() take it), a DOM node, or nothing.
17
+ */
15
18
  export type RenderableInput =
16
19
  | NodeModFn<ElementTagName>
20
+ | (() => NodeModFn<ElementTagName>)
17
21
  | Element
18
22
  | Node
19
23
  | null
@@ -1,2 +0,0 @@
1
- const e=[];var t=class{nodeType=1;nodeName=``;nodeValue=null;parentNode=null;textContent=``;get childNodes(){return e}},n=class{get cssText(){let e=Object.entries(this);return e.length===0?``:e.map(([e,t])=>`${e}: ${t}`).join(`; `)}setProperty(e,t){this[e]=t}getPropertyValue(e){return this[e]||``}},r=class{_el;constructor(e){this._el=e}tokens(){return this._el.className.split(` `).filter(Boolean)}add(...e){let t=this.tokens();for(let n of e)n&&!t.includes(n)&&t.push(n);this._el.className=t.join(` `)}remove(...e){this._el.className=this.tokens().filter(t=>!e.includes(t)).join(` `)}contains(e){return this.tokens().includes(e)}toggle(e,t){let n=t??!this.contains(e);return n?this.add(e):this.remove(e),n}replace(e,t){return this.contains(e)?(this.remove(e),this.add(t),!0):!1}item(e){return this.tokens()[e]??null}get length(){return this.tokens().length}get value(){return this._el.className}set value(e){this._el.className=e}toString(){return this._el.className}supports(e){return!1}forEach(e){this.tokens().forEach((t,n)=>e(t,n,this))}[Symbol.iterator](){return this.tokens().values()}entries(){return this.tokens().entries()}keys(){return this.tokens().keys()}values(){return this.tokens().values()}},i=class extends t{tagName;children;className=``;textContent=``;parentNode=null;id=``;namespaceURI;_style;_classList;_attributes;get attributes(){return this._attributes??=/* @__PURE__ */ new Map}set attributes(e){this._attributes=e}get style(){return this._style??=new n}set style(e){this._style=e}get classList(){return this._classList??=new r(this)}set classList(e){this._classList=e}constructor(e){super(),this.tagName=e.toLowerCase(),this.nodeType=1,this.nodeName=e.toUpperCase(),this.children=[]}get childNodes(){return this.children}appendChild(e){if(e?.nodeType===11){let t=e.childNodes;e.childNodes=[],e.children&&=[];for(let e=0;e<t.length;e++)this.appendChild(t[e]);return e}return this.children.push(e),typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=this),e}setAttribute(e,t){(this._attributes??=/* @__PURE__ */ new Map).set(e,t),e===`class`?this.className=t:e===`id`&&(this.id=t)}getAttribute(e){return this._attributes?.get(e)??null}removeAttribute(e){this._attributes?.delete(e),e===`class`&&(this.className=``)}hasAttribute(e){return this._attributes?.has(e)??!1}insertBefore(e,t){if(t===null)return this.appendChild(e);if(e?.nodeType===11){let n=e.childNodes;e.childNodes=[],e.children&&=[];for(let e=0;e<n.length;e++)this.insertBefore(n[e],t);return e}let n=this.children.indexOf(t);return n!==-1&&(this.children.splice(n,0,e),typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=this)),e}removeChild(e){let t=this.children.indexOf(e);return t!==-1&&(this.children.splice(t,1),typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=null)),e}replaceChild(e,t){let n=this.children.indexOf(t);return n!==-1&&(this.children[n]=e,typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=this),typeof t==`object`&&t&&`parentNode`in t&&(t.parentNode=null)),t}addEventListener(e,t){}removeEventListener(e,t){}dispatchEvent(e){return!0}querySelector(e){return null}querySelectorAll(e){return[]}};export{t as n,i as t};
2
- //# sourceMappingURL=Element-DLSdmoqX.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Element-DLSdmoqX.mjs","names":[],"sources":["../src/polyfill/Node.ts","../src/polyfill/Element.ts"],"sourcesContent":["const NO_CHILDREN: never[] = [];\n\n/**\n * Base Node class for instanceof checks in Node.js polyfill\n */\nexport class NucloNode {\n nodeType: number = 1; // ELEMENT_NODE\n nodeName: string = '';\n nodeValue: string | null = null;\n parentNode: unknown = null;\n textContent: string = '';\n\n get childNodes(): NodeListOf<ChildNode> {\n return NO_CHILDREN as unknown as NodeListOf<ChildNode>;\n }\n}\n","import { NucloNode } from './Node';\n\n/**\n * Lightweight style object for SSR — prototype methods instead of Object.defineProperty per element.\n * Stores CSS properties as own properties and provides cssText/setProperty/getPropertyValue via prototype.\n */\nclass SSRStyle {\n [key: string]: unknown;\n\n get cssText(): string {\n const entries = Object.entries(this);\n if (entries.length === 0) return '';\n return entries.map(([k, v]) => `${k}: ${v}`).join('; ');\n }\n\n setProperty(name: string, value: string): void {\n (this as Record<string, unknown>)[name] = value;\n }\n\n getPropertyValue(name: string): string {\n return ((this as Record<string, unknown>)[name] as string) || '';\n }\n}\n\n// Lightweight classList for SSR — prototype methods instead of per-element closures\nclass SSRClassList {\n private _el: NucloElement;\n constructor(el: NucloElement) { this._el = el; }\n\n private tokens(): string[] { return this._el.className.split(' ').filter(Boolean); }\n\n add(...tokens: string[]): void {\n const classes = this.tokens();\n for (const t of tokens) if (t && !classes.includes(t)) classes.push(t);\n this._el.className = classes.join(' ');\n }\n remove(...tokens: string[]): void {\n this._el.className = this.tokens().filter(c => !tokens.includes(c)).join(' ');\n }\n contains(token: string): boolean { return this.tokens().includes(token); }\n toggle(token: string, force?: boolean): boolean {\n const on = force ?? !this.contains(token);\n if (on) this.add(token); else this.remove(token);\n return on;\n }\n replace(oldToken: string, newToken: string): boolean {\n if (!this.contains(oldToken)) return false;\n this.remove(oldToken); this.add(newToken); return true;\n }\n item(index: number): string | null { return this.tokens()[index] ?? null; }\n get length(): number { return this.tokens().length; }\n get value(): string { return this._el.className; }\n set value(v: string) { this._el.className = v; }\n toString(): string { return this._el.className; }\n supports(_token: string): boolean { return false; }\n forEach(cb: (value: string, key: number, parent: DOMTokenList) => void): void {\n this.tokens().forEach((token, i) => cb(token, i, this as unknown as DOMTokenList));\n }\n [Symbol.iterator](): IterableIterator<string> { return this.tokens().values(); }\n entries(): IterableIterator<[number, string]> { return this.tokens().entries(); }\n keys(): IterableIterator<number> { return this.tokens().keys(); }\n values(): IterableIterator<string> { return this.tokens().values(); }\n}\n\nexport class NucloElement extends NucloNode {\n tagName: string;\n children: unknown[];\n className: string = '';\n textContent: string = '';\n parentNode: unknown = null;\n id: string = '';\n namespaceURI?: string;\n\n // style/classList are allocated lazily: the vast majority of SSR elements\n // never touch either (className is stored on the .className string, and the\n // serializer reads the backing field directly), so eagerly constructing an\n // SSRStyle + SSRClassList per element would be pure allocation churn.\n private _style?: CSSStyleDeclaration;\n private _classList?: DOMTokenList;\n\n // attributes is allocated lazily too — most elements carry their identity on\n // .className/.id (separate string fields) and never call setAttribute. Reads\n // short-circuit on the undefined backing map without allocating; the\n // serializer reads _attributes directly. The public getter still lazily\n // materializes a Map for any external `el.attributes` consumer.\n private _attributes?: Map<string, string>;\n\n get attributes(): Map<string, string> {\n return (this._attributes ??= new Map<string, string>());\n }\n set attributes(value: Map<string, string>) {\n this._attributes = value;\n }\n\n get style(): CSSStyleDeclaration {\n return (this._style ??= new SSRStyle() as unknown as CSSStyleDeclaration);\n }\n set style(value: CSSStyleDeclaration) {\n this._style = value;\n }\n\n get classList(): DOMTokenList {\n return (this._classList ??= new SSRClassList(this) as unknown as DOMTokenList);\n }\n set classList(value: DOMTokenList) {\n this._classList = value;\n }\n\n constructor(tagName: string) {\n super();\n this.tagName = tagName.toLowerCase();\n this.nodeType = 1; // ELEMENT_NODE\n this.nodeName = tagName.toUpperCase();\n this.children = [];\n }\n\n /** Every node, elements, text and comments alike (children holds them all). */\n get childNodes(): NodeListOf<ChildNode> {\n return this.children as unknown as NodeListOf<ChildNode>;\n }\n\n appendChild<T extends Node>(child: T): T {\n // Spec behaviour: appending a DocumentFragment moves its children into this\n // node and leaves the fragment empty. The list runtime relies on this to\n // batch row insertions.\n if ((child as any)?.nodeType === 11) {\n // Detach the fragment's backing array before iterating (instead of\n // slicing it) — the fragment is emptied either way, and reassigning\n // first means the loop never has to allocate a copy just to guard\n // against the array it's about to discard.\n const kids = (child as any).childNodes as Node[];\n (child as any).childNodes = [];\n if ((child as any).children) (child as any).children = [];\n for (let i = 0; i < kids.length; i++) this.appendChild(kids[i]);\n return child;\n }\n this.children.push(child);\n if (typeof child === 'object' && child !== null && 'parentNode' in child) {\n (child as { parentNode: unknown }).parentNode = this;\n }\n return child;\n }\n \n setAttribute(name: string, value: string): void {\n (this._attributes ??= new Map<string, string>()).set(name, value);\n if (name === 'class') {\n this.className = value;\n } else if (name === 'id') {\n this.id = value;\n }\n }\n\n getAttribute(name: string): string | null {\n return this._attributes?.get(name) ?? null;\n }\n\n removeAttribute(name: string): void {\n this._attributes?.delete(name);\n if (name === 'class') {\n this.className = '';\n }\n }\n\n hasAttribute(name: string): boolean {\n return this._attributes?.has(name) ?? false;\n }\n \n insertBefore<T extends Node>(newNode: T, referenceNode: Node | null): T {\n if (referenceNode === null) {\n return this.appendChild(newNode);\n }\n // Spec behaviour: inserting a DocumentFragment moves its children (in order)\n // before the reference node and empties the fragment.\n if ((newNode as any)?.nodeType === 11) {\n const kids = (newNode as any).childNodes as Node[];\n (newNode as any).childNodes = [];\n if ((newNode as any).children) (newNode as any).children = [];\n for (let i = 0; i < kids.length; i++) this.insertBefore(kids[i], referenceNode);\n return newNode;\n }\n const index = this.children.indexOf(referenceNode);\n if (index !== -1) {\n this.children.splice(index, 0, newNode);\n if (typeof newNode === 'object' && newNode !== null && 'parentNode' in newNode) {\n (newNode as { parentNode: unknown }).parentNode = this;\n }\n }\n return newNode;\n }\n \n removeChild<T extends Node>(child: T): T {\n const index = this.children.indexOf(child);\n if (index !== -1) {\n this.children.splice(index, 1);\n if (typeof child === 'object' && child !== null && 'parentNode' in child) {\n (child as { parentNode: unknown }).parentNode = null;\n }\n }\n return child;\n }\n\n replaceChild<T extends Node>(newChild: Node, oldChild: T): T {\n const index = this.children.indexOf(oldChild);\n if (index !== -1) {\n this.children[index] = newChild;\n if (typeof newChild === 'object' && newChild !== null && 'parentNode' in newChild) {\n (newChild as { parentNode: unknown }).parentNode = this;\n }\n if (typeof oldChild === 'object' && oldChild !== null && 'parentNode' in oldChild) {\n (oldChild as { parentNode: unknown }).parentNode = null;\n }\n }\n return oldChild;\n }\n \n // SSR never dispatches events: listeners are accepted and dropped, and\n // queries find nothing (serialize with renderToString instead).\n addEventListener(_type: string, _listener: EventListener): void {}\n removeEventListener(_type: string, _listener: EventListener): void {}\n dispatchEvent(_event: Event): boolean {\n return true;\n }\n querySelector(_selector: string): Element | null {\n return null;\n }\n querySelectorAll(_selector: string): NodeListOf<Element> {\n return [] as unknown as NodeListOf<Element>;\n }\n}\n"],"mappings":"AAAA,MAAM,EAAuB,CAAC,EAK9B,IAAa,EAAb,KAAuB,CACrB,SAAmB,EACnB,SAAmB,GACnB,UAA2B,KAC3B,WAAsB,KACtB,YAAsB,GAEtB,IAAI,YAAoC,CACtC,OAAO,CACT,CACF,ECTM,EAAN,KAAe,CAGb,IAAI,SAAkB,CACpB,IAAM,EAAU,OAAO,QAAQ,IAAI,EAEnC,OADI,EAAQ,SAAW,EAAU,GAC1B,EAAQ,KAAK,CAAC,EAAG,KAAO,GAAG,EAAE,IAAI,GAAG,CAAC,CAAC,KAAK,IAAI,CACxD,CAEA,YAAY,EAAc,EAAqB,CAC7C,KAAkC,GAAQ,CAC5C,CAEA,iBAAiB,EAAsB,CACrC,OAAS,KAAiC,IAAoB,EAChE,CACF,EAGM,EAAN,KAAmB,CACjB,IACA,YAAY,EAAkB,CAAE,KAAK,IAAM,CAAI,CAE/C,QAA2B,CAAE,OAAO,KAAK,IAAI,UAAU,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO,CAAG,CAEnF,IAAI,GAAG,EAAwB,CAC7B,IAAM,EAAU,KAAK,OAAO,EAC5B,IAAK,IAAM,KAAK,EAAY,GAAK,CAAC,EAAQ,SAAS,CAAC,GAAG,EAAQ,KAAK,CAAC,EACrE,KAAK,IAAI,UAAY,EAAQ,KAAK,GAAG,CACvC,CACA,OAAO,GAAG,EAAwB,CAChC,KAAK,IAAI,UAAY,KAAK,OAAO,CAAC,CAAC,OAAO,GAAK,CAAC,EAAO,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAC9E,CACA,SAAS,EAAwB,CAAE,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,CAAK,CAAG,CACzE,OAAO,EAAe,EAA0B,CAC9C,IAAM,EAAK,GAAS,CAAC,KAAK,SAAS,CAAK,EAExC,OADI,EAAI,KAAK,IAAI,CAAK,EAAQ,KAAK,OAAO,CAAK,EACxC,CACT,CACA,QAAQ,EAAkB,EAA2B,CAER,OADtC,KAAK,SAAS,CAAQ,GAC3B,KAAK,OAAO,CAAQ,EAAG,KAAK,IAAI,CAAQ,EAAU,IADb,EAEvC,CACA,KAAK,EAA8B,CAAE,OAAO,KAAK,OAAO,CAAC,CAAC,IAAU,IAAM,CAC1E,IAAI,QAAiB,CAAE,OAAO,KAAK,OAAO,CAAC,CAAC,MAAQ,CACpD,IAAI,OAAgB,CAAE,OAAO,KAAK,IAAI,SAAW,CACjD,IAAI,MAAM,EAAW,CAAE,KAAK,IAAI,UAAY,CAAG,CAC/C,UAAmB,CAAE,OAAO,KAAK,IAAI,SAAW,CAChD,SAAS,EAAyB,CAAE,MAAO,EAAO,CAClD,QAAQ,EAAsE,CAC5E,KAAK,OAAO,CAAC,CAAC,SAAS,EAAO,IAAM,EAAG,EAAO,EAAG,IAA+B,CAAC,CACnF,CACA,CAAC,OAAO,WAAsC,CAAE,OAAO,KAAK,OAAO,CAAC,CAAC,OAAO,CAAG,CAC/E,SAA8C,CAAE,OAAO,KAAK,OAAO,CAAC,CAAC,QAAQ,CAAG,CAChF,MAAiC,CAAE,OAAO,KAAK,OAAO,CAAC,CAAC,KAAK,CAAG,CAChE,QAAmC,CAAE,OAAO,KAAK,OAAO,CAAC,CAAC,OAAO,CAAG,CACtE,EAEa,EAAb,cAAkC,CAAU,CAC1C,QACA,SACA,UAAoB,GACpB,YAAsB,GACtB,WAAsB,KACtB,GAAa,GACb,aAMA,OACA,WAOA,YAEA,IAAI,YAAkC,CACpC,MAAQ,MAAK,8BAAgB,IAAI,GACnC,CACA,IAAI,WAAW,EAA4B,CACzC,KAAK,YAAc,CACrB,CAEA,IAAI,OAA6B,CAC/B,MAAQ,MAAK,SAAW,IAAI,CAC9B,CACA,IAAI,MAAM,EAA4B,CACpC,KAAK,OAAS,CAChB,CAEA,IAAI,WAA0B,CAC5B,MAAQ,MAAK,aAAe,IAAI,EAAa,IAAI,CACnD,CACA,IAAI,UAAU,EAAqB,CACjC,KAAK,WAAa,CACpB,CAEA,YAAY,EAAiB,CAC3B,MAAM,EACN,KAAK,QAAU,EAAQ,YAAY,EACnC,KAAK,SAAW,EAChB,KAAK,SAAW,EAAQ,YAAY,EACpC,KAAK,SAAW,CAAC,CACnB,CAGA,IAAI,YAAoC,CACtC,OAAO,KAAK,QACd,CAEA,YAA4B,EAAa,CAIvC,GAAK,GAAe,WAAa,GAAI,CAKnC,IAAM,EAAQ,EAAc,WAC5B,EAAe,WAAa,CAAC,EAC7B,AAA6B,EAAe,WAAW,CAAC,EACxD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAQ,IAAK,KAAK,YAAY,EAAK,EAAE,EAC9D,OAAO,CACT,CAKA,OAJA,KAAK,SAAS,KAAK,CAAK,EACpB,OAAO,GAAU,UAAY,GAAkB,eAAgB,IACjE,EAAmC,WAAa,MAE3C,CACT,CAEA,aAAa,EAAc,EAAqB,EAC7C,KAAK,8BAAgB,IAAI,IAAoB,CAAG,IAAI,EAAM,CAAK,EAC5D,IAAS,QACX,KAAK,UAAY,EACR,IAAS,OAClB,KAAK,GAAK,EAEd,CAEA,aAAa,EAA6B,CACxC,OAAO,KAAK,aAAa,IAAI,CAAI,GAAK,IACxC,CAEA,gBAAgB,EAAoB,CAClC,KAAK,aAAa,OAAO,CAAI,EACzB,IAAS,UACX,KAAK,UAAY,GAErB,CAEA,aAAa,EAAuB,CAClC,OAAO,KAAK,aAAa,IAAI,CAAI,GAAK,EACxC,CAEA,aAA6B,EAAY,EAA+B,CACtE,GAAI,IAAkB,KACpB,OAAO,KAAK,YAAY,CAAO,EAIjC,GAAK,GAAiB,WAAa,GAAI,CACrC,IAAM,EAAQ,EAAgB,WAC9B,EAAiB,WAAa,CAAC,EAC/B,AAA+B,EAAiB,WAAW,CAAC,EAC5D,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAQ,IAAK,KAAK,aAAa,EAAK,GAAI,CAAa,EAC9E,OAAO,CACT,CACA,IAAM,EAAQ,KAAK,SAAS,QAAQ,CAAa,EAOjD,OANI,IAAU,KACZ,KAAK,SAAS,OAAO,EAAO,EAAG,CAAO,EAClC,OAAO,GAAY,UAAY,GAAoB,eAAgB,IACrE,EAAqC,WAAa,OAG/C,CACT,CAEA,YAA4B,EAAa,CACvC,IAAM,EAAQ,KAAK,SAAS,QAAQ,CAAK,EAOzC,OANI,IAAU,KACZ,KAAK,SAAS,OAAO,EAAO,CAAC,EACzB,OAAO,GAAU,UAAY,GAAkB,eAAgB,IACjE,EAAmC,WAAa,OAG7C,CACT,CAEA,aAA6B,EAAgB,EAAgB,CAC3D,IAAM,EAAQ,KAAK,SAAS,QAAQ,CAAQ,EAU5C,OATI,IAAU,KACZ,KAAK,SAAS,GAAS,EACnB,OAAO,GAAa,UAAY,GAAqB,eAAgB,IACvE,EAAsC,WAAa,MAEjD,OAAO,GAAa,UAAY,GAAqB,eAAgB,IACvE,EAAsC,WAAa,OAGhD,CACT,CAIA,iBAAiB,EAAe,EAAgC,CAAC,CACjE,oBAAoB,EAAe,EAAgC,CAAC,CACpE,cAAc,EAAwB,CACpC,MAAO,EACT,CACA,cAAc,EAAmC,CAC/C,OAAO,IACT,CACA,iBAAiB,EAAwC,CACvD,MAAO,CAAC,CACV,CACF"}
@@ -1,2 +0,0 @@
1
- const e=[];var t=class{nodeType=1;nodeName=``;nodeValue=null;parentNode=null;textContent=``;get childNodes(){return e}},n=class{get cssText(){let e=Object.entries(this);return e.length===0?``:e.map(([e,t])=>`${e}: ${t}`).join(`; `)}setProperty(e,t){this[e]=t}getPropertyValue(e){return this[e]||``}},r=class{_el;constructor(e){this._el=e}tokens(){return this._el.className.split(` `).filter(Boolean)}add(...e){let t=this.tokens();for(let n of e)n&&!t.includes(n)&&t.push(n);this._el.className=t.join(` `)}remove(...e){this._el.className=this.tokens().filter(t=>!e.includes(t)).join(` `)}contains(e){return this.tokens().includes(e)}toggle(e,t){let n=t??!this.contains(e);return n?this.add(e):this.remove(e),n}replace(e,t){return this.contains(e)?(this.remove(e),this.add(t),!0):!1}item(e){return this.tokens()[e]??null}get length(){return this.tokens().length}get value(){return this._el.className}set value(e){this._el.className=e}toString(){return this._el.className}supports(e){return!1}forEach(e){this.tokens().forEach((t,n)=>e(t,n,this))}[Symbol.iterator](){return this.tokens().values()}entries(){return this.tokens().entries()}keys(){return this.tokens().keys()}values(){return this.tokens().values()}},i=class extends t{tagName;children;className=``;textContent=``;parentNode=null;id=``;namespaceURI;_style;_classList;_attributes;get attributes(){return this._attributes??=/* @__PURE__ */ new Map}set attributes(e){this._attributes=e}get style(){return this._style??=new n}set style(e){this._style=e}get classList(){return this._classList??=new r(this)}set classList(e){this._classList=e}constructor(e){super(),this.tagName=e.toLowerCase(),this.nodeType=1,this.nodeName=e.toUpperCase(),this.children=[]}get childNodes(){return this.children}appendChild(e){if(e?.nodeType===11){let t=e.childNodes;e.childNodes=[],e.children&&=[];for(let e=0;e<t.length;e++)this.appendChild(t[e]);return e}return this.children.push(e),typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=this),e}setAttribute(e,t){(this._attributes??=/* @__PURE__ */ new Map).set(e,t),e===`class`?this.className=t:e===`id`&&(this.id=t)}getAttribute(e){return this._attributes?.get(e)??null}removeAttribute(e){this._attributes?.delete(e),e===`class`&&(this.className=``)}hasAttribute(e){return this._attributes?.has(e)??!1}insertBefore(e,t){if(t===null)return this.appendChild(e);if(e?.nodeType===11){let n=e.childNodes;e.childNodes=[],e.children&&=[];for(let e=0;e<n.length;e++)this.insertBefore(n[e],t);return e}let n=this.children.indexOf(t);return n!==-1&&(this.children.splice(n,0,e),typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=this)),e}removeChild(e){let t=this.children.indexOf(e);return t!==-1&&(this.children.splice(t,1),typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=null)),e}replaceChild(e,t){let n=this.children.indexOf(t);return n!==-1&&(this.children[n]=e,typeof e==`object`&&e&&`parentNode`in e&&(e.parentNode=this),typeof t==`object`&&t&&`parentNode`in t&&(t.parentNode=null)),t}addEventListener(e,t){}removeEventListener(e,t){}dispatchEvent(e){return!0}querySelector(e){return null}querySelectorAll(e){return[]}};Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return i}});
2
- //# sourceMappingURL=Element-DfxBbUgD.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Element-DfxBbUgD.cjs","names":[],"sources":["../src/polyfill/Node.ts","../src/polyfill/Element.ts"],"sourcesContent":["const NO_CHILDREN: never[] = [];\n\n/**\n * Base Node class for instanceof checks in Node.js polyfill\n */\nexport class NucloNode {\n nodeType: number = 1; // ELEMENT_NODE\n nodeName: string = '';\n nodeValue: string | null = null;\n parentNode: unknown = null;\n textContent: string = '';\n\n get childNodes(): NodeListOf<ChildNode> {\n return NO_CHILDREN as unknown as NodeListOf<ChildNode>;\n }\n}\n","import { NucloNode } from './Node';\n\n/**\n * Lightweight style object for SSR — prototype methods instead of Object.defineProperty per element.\n * Stores CSS properties as own properties and provides cssText/setProperty/getPropertyValue via prototype.\n */\nclass SSRStyle {\n [key: string]: unknown;\n\n get cssText(): string {\n const entries = Object.entries(this);\n if (entries.length === 0) return '';\n return entries.map(([k, v]) => `${k}: ${v}`).join('; ');\n }\n\n setProperty(name: string, value: string): void {\n (this as Record<string, unknown>)[name] = value;\n }\n\n getPropertyValue(name: string): string {\n return ((this as Record<string, unknown>)[name] as string) || '';\n }\n}\n\n// Lightweight classList for SSR — prototype methods instead of per-element closures\nclass SSRClassList {\n private _el: NucloElement;\n constructor(el: NucloElement) { this._el = el; }\n\n private tokens(): string[] { return this._el.className.split(' ').filter(Boolean); }\n\n add(...tokens: string[]): void {\n const classes = this.tokens();\n for (const t of tokens) if (t && !classes.includes(t)) classes.push(t);\n this._el.className = classes.join(' ');\n }\n remove(...tokens: string[]): void {\n this._el.className = this.tokens().filter(c => !tokens.includes(c)).join(' ');\n }\n contains(token: string): boolean { return this.tokens().includes(token); }\n toggle(token: string, force?: boolean): boolean {\n const on = force ?? !this.contains(token);\n if (on) this.add(token); else this.remove(token);\n return on;\n }\n replace(oldToken: string, newToken: string): boolean {\n if (!this.contains(oldToken)) return false;\n this.remove(oldToken); this.add(newToken); return true;\n }\n item(index: number): string | null { return this.tokens()[index] ?? null; }\n get length(): number { return this.tokens().length; }\n get value(): string { return this._el.className; }\n set value(v: string) { this._el.className = v; }\n toString(): string { return this._el.className; }\n supports(_token: string): boolean { return false; }\n forEach(cb: (value: string, key: number, parent: DOMTokenList) => void): void {\n this.tokens().forEach((token, i) => cb(token, i, this as unknown as DOMTokenList));\n }\n [Symbol.iterator](): IterableIterator<string> { return this.tokens().values(); }\n entries(): IterableIterator<[number, string]> { return this.tokens().entries(); }\n keys(): IterableIterator<number> { return this.tokens().keys(); }\n values(): IterableIterator<string> { return this.tokens().values(); }\n}\n\nexport class NucloElement extends NucloNode {\n tagName: string;\n children: unknown[];\n className: string = '';\n textContent: string = '';\n parentNode: unknown = null;\n id: string = '';\n namespaceURI?: string;\n\n // style/classList are allocated lazily: the vast majority of SSR elements\n // never touch either (className is stored on the .className string, and the\n // serializer reads the backing field directly), so eagerly constructing an\n // SSRStyle + SSRClassList per element would be pure allocation churn.\n private _style?: CSSStyleDeclaration;\n private _classList?: DOMTokenList;\n\n // attributes is allocated lazily too — most elements carry their identity on\n // .className/.id (separate string fields) and never call setAttribute. Reads\n // short-circuit on the undefined backing map without allocating; the\n // serializer reads _attributes directly. The public getter still lazily\n // materializes a Map for any external `el.attributes` consumer.\n private _attributes?: Map<string, string>;\n\n get attributes(): Map<string, string> {\n return (this._attributes ??= new Map<string, string>());\n }\n set attributes(value: Map<string, string>) {\n this._attributes = value;\n }\n\n get style(): CSSStyleDeclaration {\n return (this._style ??= new SSRStyle() as unknown as CSSStyleDeclaration);\n }\n set style(value: CSSStyleDeclaration) {\n this._style = value;\n }\n\n get classList(): DOMTokenList {\n return (this._classList ??= new SSRClassList(this) as unknown as DOMTokenList);\n }\n set classList(value: DOMTokenList) {\n this._classList = value;\n }\n\n constructor(tagName: string) {\n super();\n this.tagName = tagName.toLowerCase();\n this.nodeType = 1; // ELEMENT_NODE\n this.nodeName = tagName.toUpperCase();\n this.children = [];\n }\n\n /** Every node, elements, text and comments alike (children holds them all). */\n get childNodes(): NodeListOf<ChildNode> {\n return this.children as unknown as NodeListOf<ChildNode>;\n }\n\n appendChild<T extends Node>(child: T): T {\n // Spec behaviour: appending a DocumentFragment moves its children into this\n // node and leaves the fragment empty. The list runtime relies on this to\n // batch row insertions.\n if ((child as any)?.nodeType === 11) {\n // Detach the fragment's backing array before iterating (instead of\n // slicing it) — the fragment is emptied either way, and reassigning\n // first means the loop never has to allocate a copy just to guard\n // against the array it's about to discard.\n const kids = (child as any).childNodes as Node[];\n (child as any).childNodes = [];\n if ((child as any).children) (child as any).children = [];\n for (let i = 0; i < kids.length; i++) this.appendChild(kids[i]);\n return child;\n }\n this.children.push(child);\n if (typeof child === 'object' && child !== null && 'parentNode' in child) {\n (child as { parentNode: unknown }).parentNode = this;\n }\n return child;\n }\n \n setAttribute(name: string, value: string): void {\n (this._attributes ??= new Map<string, string>()).set(name, value);\n if (name === 'class') {\n this.className = value;\n } else if (name === 'id') {\n this.id = value;\n }\n }\n\n getAttribute(name: string): string | null {\n return this._attributes?.get(name) ?? null;\n }\n\n removeAttribute(name: string): void {\n this._attributes?.delete(name);\n if (name === 'class') {\n this.className = '';\n }\n }\n\n hasAttribute(name: string): boolean {\n return this._attributes?.has(name) ?? false;\n }\n \n insertBefore<T extends Node>(newNode: T, referenceNode: Node | null): T {\n if (referenceNode === null) {\n return this.appendChild(newNode);\n }\n // Spec behaviour: inserting a DocumentFragment moves its children (in order)\n // before the reference node and empties the fragment.\n if ((newNode as any)?.nodeType === 11) {\n const kids = (newNode as any).childNodes as Node[];\n (newNode as any).childNodes = [];\n if ((newNode as any).children) (newNode as any).children = [];\n for (let i = 0; i < kids.length; i++) this.insertBefore(kids[i], referenceNode);\n return newNode;\n }\n const index = this.children.indexOf(referenceNode);\n if (index !== -1) {\n this.children.splice(index, 0, newNode);\n if (typeof newNode === 'object' && newNode !== null && 'parentNode' in newNode) {\n (newNode as { parentNode: unknown }).parentNode = this;\n }\n }\n return newNode;\n }\n \n removeChild<T extends Node>(child: T): T {\n const index = this.children.indexOf(child);\n if (index !== -1) {\n this.children.splice(index, 1);\n if (typeof child === 'object' && child !== null && 'parentNode' in child) {\n (child as { parentNode: unknown }).parentNode = null;\n }\n }\n return child;\n }\n\n replaceChild<T extends Node>(newChild: Node, oldChild: T): T {\n const index = this.children.indexOf(oldChild);\n if (index !== -1) {\n this.children[index] = newChild;\n if (typeof newChild === 'object' && newChild !== null && 'parentNode' in newChild) {\n (newChild as { parentNode: unknown }).parentNode = this;\n }\n if (typeof oldChild === 'object' && oldChild !== null && 'parentNode' in oldChild) {\n (oldChild as { parentNode: unknown }).parentNode = null;\n }\n }\n return oldChild;\n }\n \n // SSR never dispatches events: listeners are accepted and dropped, and\n // queries find nothing (serialize with renderToString instead).\n addEventListener(_type: string, _listener: EventListener): void {}\n removeEventListener(_type: string, _listener: EventListener): void {}\n dispatchEvent(_event: Event): boolean {\n return true;\n }\n querySelector(_selector: string): Element | null {\n return null;\n }\n querySelectorAll(_selector: string): NodeListOf<Element> {\n return [] as unknown as NodeListOf<Element>;\n }\n}\n"],"mappings":"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"}