nuclo 0.1.87 → 0.1.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/reactive.d.ts +2 -2
- package/dist/core/reactive.d.ts.map +1 -1
- package/dist/core/reactiveAttributes.d.ts +5 -0
- package/dist/core/reactiveAttributes.d.ts.map +1 -1
- package/dist/core/reactiveText.d.ts +5 -0
- package/dist/core/reactiveText.d.ts.map +1 -1
- package/dist/list/runtime.d.ts.map +1 -1
- package/dist/nuclo.cjs +1 -1
- package/dist/nuclo.cjs.map +1 -1
- package/dist/nuclo.js +1 -1
- package/dist/nuclo.js.map +1 -1
- package/dist/nuclo.umd.js +1 -1
- package/dist/nuclo.umd.js.map +1 -1
- package/dist/ssr/nuclo.ssr.cjs.map +1 -1
- package/dist/ssr/nuclo.ssr.js.map +1 -1
- package/dist/utility/dom.d.ts.map +1 -1
- package/dist/utility/on.d.ts.map +1 -1
- package/dist/when/runtime.d.ts +4 -2
- package/dist/when/runtime.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nuclo.ssr.cjs","sources":["../../src/utility/stringUtils.ts","../../src/ssr/renderToString.ts","../../src/utility/dom.ts"],"sourcesContent":["/**\n * String utility functions\n */\n\n/**\n * Converts camelCase to kebab-case (optimized for performance)\n * Uses direct character code manipulation for maximum speed\n * @example camelToKebab('backgroundColor') => 'background-color'\n */\nexport function camelToKebab(str: string): string {\n let result = '';\n for (let i = 0; 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 if (code >= 65 && code <= 90) {\n if (i > 0) result += '-';\n result += String.fromCharCode(code + 32);\n } else {\n result += str[i];\n }\n }\n return result;\n}\n\n/**\n * Escapes HTML special characters to prevent XSS\n */\nexport function escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return text.replace(/[&<>\"']/g, (char) => map[char]);\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, camelToKebab } from '../utility/stringUtils';\nimport { createElement } from '../utility/dom';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\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 if (name === 'style' && typeof value === 'object') {\n const styleStr = Object.entries(value)\n .map(([key, val]) => {\n const cssKey = camelToKebab(key);\n return `${cssKey}:${val}`;\n })\n .join(';');\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 */\nfunction serializeAttributes(element: Element): string {\n let result = '';\n\n // Handle polyfill elements with Map-based attributes\n if ('attributes' in element && element.attributes instanceof Map) {\n // First, handle special properties that might not be in the attributes Map\n const el = element as any;\n\n // Add id if it exists and is not already in attributes\n if (el.id && !element.attributes.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // Add class if it exists and is not already in attributes\n if (el.className && !element.attributes.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // Then add all attributes from the Map\n for (const [name, value] of element.attributes) {\n result += serializeAttribute(name, value);\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 * Self-closing HTML tags that don't have closing tags\n */\nconst VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr'\n]);\n\n/**\n * Get child nodes from an element (handles both browser and polyfill elements)\n */\nfunction getChildNodes(node: Node): ArrayLike<Node> {\n // Check for childNodes first (document fragments and browser elements)\n if ('childNodes' in node) {\n const childNodes = (node as any).childNodes;\n if (childNodes && (Array.isArray(childNodes) || childNodes.length !== undefined)) {\n return childNodes;\n }\n }\n // Polyfill elements have children array\n if ('children' in node) {\n const children = (node as any).children;\n if (children && Array.isArray(children)) {\n return children as ArrayLike<Node>;\n }\n }\n return [] as ArrayLike<Node>;\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeHtml(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 tagName = element.tagName.toLowerCase();\n const attributes = serializeAttributes(element);\n\n // Self-closing tags\n if (VOID_ELEMENTS.has(tagName)) {\n return `<${tagName}${attributes} />`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n const childNodes = getChildNodes(element);\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 }\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 = getChildNodes(node);\n if (childNodes && 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) {\n return '';\n }\n\n // If it's a function (NodeModFn), call it to create the element\n if (typeof input === 'function') {\n try {\n // Create a temporary container to render into\n const container = createElement('div');\n if (!container) {\n throw new Error('Document is not available. Make sure polyfills are loaded.');\n }\n\n const element = input(container as ExpandedElement<ElementTagName>, 0);\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 it's already a Node, serialize it directly\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","import { isBrowser } from \"./environment\";\nimport { logError } from \"./errorHandler\";\n\n/**\n * Creates an HTML element.\n * Wrapper for document.createElement with type safety.\n */\nexport function createElement<K extends keyof HTMLElementTagNameMap>(\n tagName: K\n): HTMLElementTagNameMap[K] | null;\nexport function createElement(tagName: string): ExpandedElement | null;\nexport function createElement(tagName: string): ExpandedElement | null {\n return globalThis.document ? document.createElement(tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates an element in the given namespace (typically for SVG elements).\n * Wrapper for document.createElementNS with type safety.\n */\nexport function createElementNS(\n namespace: string,\n tagName: string\n): ExpandedElement | null {\n return globalThis.document ? document.createElementNS(namespace, tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates a text node with the given content.\n * Wrapper for document.createTextNode.\n */\nexport function createTextNode(text: string): Text | null {\n return globalThis.document ? document.createTextNode(text) : null;\n}\n\n/**\n * Creates a document fragment.\n * Wrapper for document.createDocumentFragment.\n */\nexport function createDocumentFragment(): DocumentFragment | null {\n return globalThis.document ? document.createDocumentFragment() : null;\n}\n\nfunction safeAppendChild(parent: Element | Node, child: Node): boolean {\n try {\n parent.appendChild(child);\n return true;\n } catch (error) {\n logError('Failed to append child node', error);\n return false;\n }\n}\n\nexport function safeRemoveChild(child: Node): boolean {\n if (!child?.parentNode) return false;\n try {\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\nfunction createTextNodeSafely(text: string | number | boolean): Text | null {\n if (!isBrowser) return null;\n try {\n return createTextNode(String(text));\n } catch (error) {\n logError('Failed to create text node', error);\n return null;\n }\n}\n\nfunction createCommentSafely(text: string): Comment | null {\n if (!isBrowser) return null;\n try {\n return document.createComment(text);\n } catch (error) {\n logError('Failed to create comment node', error);\n return null;\n }\n}\n\n/**\n * Creates a comment node safely with error handling.\n * Exported for use across the codebase.\n */\nexport function createComment(text: string): Comment | null {\n return createCommentSafely(text);\n}\n\n/**\n * Creates a conditional comment placeholder node.\n * In SSR environments, this will still work because we bypass the isBrowser check.\n */\nexport function createConditionalComment(tagName: string, suffix: string = \"hidden\"): Comment | null {\n // For SSR, we need to create comments even when isBrowser is false\n // This function intentionally skips the isBrowser check for SSR compatibility\n try {\n return document.createComment(`conditional-${tagName}-${suffix}`);\n } catch (error) {\n logError('Failed to create conditional comment', error);\n return null;\n }\n}\n\nexport function createMarkerComment(prefix: string): Comment {\n if (!isBrowser) {\n throw new Error(\"Cannot create comment in non-browser environment\");\n }\n const comment = createCommentSafely(`${prefix}-${Math.random().toString(36).slice(2)}`);\n if (!comment) {\n throw new Error(\"Failed to create comment\");\n }\n return comment;\n}\n\nexport function createMarkerPair(prefix: string): { start: Comment; end: Comment } {\n const endComment = createCommentSafely(`${prefix}-end`);\n if (!endComment) {\n throw new Error(\"Failed to create end comment\");\n }\n return {\n start: createMarkerComment(`${prefix}-start`),\n end: endComment\n };\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 nodes.forEach(function(node) { safeInsertBefore(parent, node, referenceNode); });\n }\n}\n\nexport function appendChildren(\n parent: Element | Node,\n ...children: Array<Element | Node | string | null | undefined>\n): Element | Node {\n if (!parent) return parent;\n\n children.forEach(function(child) {\n if (child != null) {\n let nodeToAppend: Node;\n\n if (typeof child === \"string\") {\n const textNode = createTextNodeSafely(child);\n if (textNode) {\n nodeToAppend = textNode;\n } else {\n return;\n }\n } else {\n nodeToAppend = child as Node;\n }\n\n safeAppendChild(parent, nodeToAppend);\n }\n });\n\n return parent;\n}\n\nexport function isNodeConnected(node: Node | null | undefined): boolean {\n if (!node) return false;\n\n // Prefer the built-in isConnected property\n if (typeof node.isConnected === \"boolean\") {\n return node.isConnected;\n }\n\n // Fallback for older browsers (only if in browser environment)\n if (isBrowser && typeof document !== 'undefined') {\n return document.contains(node);\n }\n\n // In SSR or when document is not available, assume disconnected\n return false;\n}\n\n/**\n * Safely replaces an old node with a new node in the DOM.\n * Returns true on success, false on failure (and logs the error).\n */\nexport function replaceNodeSafely(oldNode: Node, newNode: Node): boolean {\n if (!oldNode?.parentNode) return false;\n try {\n oldNode.parentNode.replaceChild(newNode, oldNode);\n return true;\n } catch (error) {\n logError(\"Error replacing conditional node\", error);\n return false;\n }\n}\n"],"names":["escapeHtml","text","map","replace","char","serializeAttribute","name","value","styleStr","Object","entries","key","val","str","result","i","length","code","charCodeAt","String","fromCharCode","camelToKebab","join","VOID_ELEMENTS","Set","getChildNodes","node","childNodes","Array","isArray","undefined","children","serializeNode","nodeType","textContent","element","tagName","toLowerCase","attributes","Map","el","id","has","className","attr","serializeAttributes","childrenHtml","child","renderToString","input","container","globalThis","document","createElement","Error","error","console","inputs","containerTag","containerAttrs","content"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nuclo.ssr.cjs","sources":["../../src/utility/stringUtils.ts","../../src/ssr/renderToString.ts","../../src/utility/dom.ts"],"sourcesContent":["/**\n * String utility functions\n */\n\n/**\n * Converts camelCase to kebab-case (optimized for performance)\n * Uses direct character code manipulation for maximum speed\n * @example camelToKebab('backgroundColor') => 'background-color'\n */\nexport function camelToKebab(str: string): string {\n let result = '';\n for (let i = 0; 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 if (code >= 65 && code <= 90) {\n if (i > 0) result += '-';\n result += String.fromCharCode(code + 32);\n } else {\n result += str[i];\n }\n }\n return result;\n}\n\n/**\n * Escapes HTML special characters to prevent XSS\n */\nexport function escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return text.replace(/[&<>\"']/g, (char) => map[char]);\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, camelToKebab } from '../utility/stringUtils';\nimport { createElement } from '../utility/dom';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\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 if (name === 'style' && typeof value === 'object') {\n const styleStr = Object.entries(value)\n .map(([key, val]) => {\n const cssKey = camelToKebab(key);\n return `${cssKey}:${val}`;\n })\n .join(';');\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 */\nfunction serializeAttributes(element: Element): string {\n let result = '';\n\n // Handle polyfill elements with Map-based attributes\n if ('attributes' in element && element.attributes instanceof Map) {\n // First, handle special properties that might not be in the attributes Map\n const el = element as any;\n\n // Add id if it exists and is not already in attributes\n if (el.id && !element.attributes.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // Add class if it exists and is not already in attributes\n if (el.className && !element.attributes.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // Then add all attributes from the Map\n for (const [name, value] of element.attributes) {\n result += serializeAttribute(name, value);\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 * Self-closing HTML tags that don't have closing tags\n */\nconst VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr'\n]);\n\n/**\n * Get child nodes from an element (handles both browser and polyfill elements)\n */\nfunction getChildNodes(node: Node): ArrayLike<Node> {\n // Check for childNodes first (document fragments and browser elements)\n if ('childNodes' in node) {\n const childNodes = (node as any).childNodes;\n if (childNodes && (Array.isArray(childNodes) || childNodes.length !== undefined)) {\n return childNodes;\n }\n }\n // Polyfill elements have children array\n if ('children' in node) {\n const children = (node as any).children;\n if (children && Array.isArray(children)) {\n return children as ArrayLike<Node>;\n }\n }\n return [] as ArrayLike<Node>;\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeHtml(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 tagName = element.tagName.toLowerCase();\n const attributes = serializeAttributes(element);\n\n // Self-closing tags\n if (VOID_ELEMENTS.has(tagName)) {\n return `<${tagName}${attributes} />`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n const childNodes = getChildNodes(element);\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 }\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 = getChildNodes(node);\n if (childNodes && 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) {\n return '';\n }\n\n // If it's a function (NodeModFn), call it to create the element\n if (typeof input === 'function') {\n try {\n // Create a temporary container to render into\n const container = createElement('div');\n if (!container) {\n throw new Error('Document is not available. Make sure polyfills are loaded.');\n }\n\n const element = input(container as ExpandedElement<ElementTagName>, 0);\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 it's already a Node, serialize it directly\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","import { isBrowser } from \"./environment\";\nimport { logError } from \"./errorHandler\";\nimport { removeAllListeners } from \"./on\";\nimport { cleanupReactiveElement, cleanupReactiveTextNode } from \"../core/reactive\";\nimport { unregisterConditionalNode } from \"./conditionalInfo\";\n\n/**\n * Creates an HTML element.\n * Wrapper for document.createElement with type safety.\n */\nexport function createElement<K extends keyof HTMLElementTagNameMap>(\n tagName: K\n): HTMLElementTagNameMap[K] | null;\nexport function createElement(tagName: string): ExpandedElement | null;\nexport function createElement(tagName: string): ExpandedElement | null {\n return globalThis.document ? document.createElement(tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates an element in the given namespace (typically for SVG elements).\n * Wrapper for document.createElementNS with type safety.\n */\nexport function createElementNS(\n namespace: string,\n tagName: string\n): ExpandedElement | null {\n return globalThis.document ? document.createElementNS(namespace, tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates a text node with the given content.\n * Wrapper for document.createTextNode.\n */\nexport function createTextNode(text: string): Text | null {\n return globalThis.document ? document.createTextNode(text) : null;\n}\n\n/**\n * Creates a document fragment.\n * Wrapper for document.createDocumentFragment.\n */\nexport function createDocumentFragment(): DocumentFragment | null {\n return globalThis.document ? document.createDocumentFragment() : null;\n}\n\nfunction safeAppendChild(parent: Element | Node, child: Node): boolean {\n try {\n parent.appendChild(child);\n return true;\n } catch (error) {\n logError('Failed to append child node', error);\n return false;\n }\n}\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 cleanupEventListeners(node: Node): void {\n // Clean up the node itself based on its type\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Remove all event listeners\n removeAllListeners(element);\n // Remove reactive attribute resolvers\n cleanupReactiveElement(element);\n // Remove conditional info\n unregisterConditionalNode(element);\n } else if (node.nodeType === Node.TEXT_NODE) {\n // Remove reactive text node info\n cleanupReactiveTextNode(node as Text);\n } else if (node.nodeType === Node.COMMENT_NODE) {\n // Remove conditional info from comment nodes (used by when/list)\n unregisterConditionalNode(node);\n }\n \n // Recursively clean up all child nodes\n if (node.childNodes && node.childNodes.length > 0) {\n for (let i = 0; i < node.childNodes.length; i++) {\n cleanupEventListeners(node.childNodes[i]);\n }\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 cleanupEventListeners(child);\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\nfunction createTextNodeSafely(text: string | number | boolean): Text | null {\n if (!isBrowser) return null;\n try {\n return createTextNode(String(text));\n } catch (error) {\n logError('Failed to create text node', error);\n return null;\n }\n}\n\nfunction createCommentSafely(text: string): Comment | null {\n if (!isBrowser) return null;\n try {\n return document.createComment(text);\n } catch (error) {\n logError('Failed to create comment node', error);\n return null;\n }\n}\n\n/**\n * Creates a comment node safely with error handling.\n * Exported for use across the codebase.\n */\nexport function createComment(text: string): Comment | null {\n return createCommentSafely(text);\n}\n\n/**\n * Creates a conditional comment placeholder node.\n * In SSR environments, this will still work because we bypass the isBrowser check.\n */\nexport function createConditionalComment(tagName: string, suffix: string = \"hidden\"): Comment | null {\n // For SSR, we need to create comments even when isBrowser is false\n // This function intentionally skips the isBrowser check for SSR compatibility\n try {\n return document.createComment(`conditional-${tagName}-${suffix}`);\n } catch (error) {\n logError('Failed to create conditional comment', error);\n return null;\n }\n}\n\nexport function createMarkerComment(prefix: string): Comment {\n if (!isBrowser) {\n throw new Error(\"Cannot create comment in non-browser environment\");\n }\n const comment = createCommentSafely(`${prefix}-${Math.random().toString(36).slice(2)}`);\n if (!comment) {\n throw new Error(\"Failed to create comment\");\n }\n return comment;\n}\n\nexport function createMarkerPair(prefix: string): { start: Comment; end: Comment } {\n const endComment = createCommentSafely(`${prefix}-end`);\n if (!endComment) {\n throw new Error(\"Failed to create end comment\");\n }\n return {\n start: createMarkerComment(`${prefix}-start`),\n end: endComment\n };\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 nodes.forEach(function(node) { safeInsertBefore(parent, node, referenceNode); });\n }\n}\n\nexport function appendChildren(\n parent: Element | Node,\n ...children: Array<Element | Node | string | null | undefined>\n): Element | Node {\n if (!parent) return parent;\n\n children.forEach(function(child) {\n if (child != null) {\n let nodeToAppend: Node;\n\n if (typeof child === \"string\") {\n const textNode = createTextNodeSafely(child);\n if (textNode) {\n nodeToAppend = textNode;\n } else {\n return;\n }\n } else {\n nodeToAppend = child as Node;\n }\n\n safeAppendChild(parent, nodeToAppend);\n }\n });\n\n return parent;\n}\n\nexport function isNodeConnected(node: Node | null | undefined): boolean {\n if (!node) return false;\n\n // Prefer the built-in isConnected property\n if (typeof node.isConnected === \"boolean\") {\n return node.isConnected;\n }\n\n // Fallback for older browsers (only if in browser environment)\n if (isBrowser && typeof document !== 'undefined') {\n return document.contains(node);\n }\n\n // In SSR or when document is not available, assume disconnected\n return false;\n}\n\n/**\n * Safely replaces an old node with a new node in the DOM.\n * Returns true on success, false on failure (and logs the error).\n */\nexport function replaceNodeSafely(oldNode: Node, newNode: Node): boolean {\n if (!oldNode?.parentNode) return false;\n try {\n oldNode.parentNode.replaceChild(newNode, oldNode);\n return true;\n } catch (error) {\n logError(\"Error replacing conditional node\", error);\n return false;\n }\n}\n"],"names":["escapeHtml","text","map","replace","char","serializeAttribute","name","value","styleStr","Object","entries","key","val","str","result","i","length","code","charCodeAt","String","fromCharCode","camelToKebab","join","VOID_ELEMENTS","Set","getChildNodes","node","childNodes","Array","isArray","undefined","children","serializeNode","nodeType","textContent","element","tagName","toLowerCase","attributes","Map","el","id","has","className","attr","serializeAttributes","childrenHtml","child","renderToString","input","container","globalThis","document","createElement","Error","error","console","inputs","containerTag","containerAttrs","content"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nuclo.ssr.js","sources":["../../src/utility/stringUtils.ts","../../src/ssr/renderToString.ts","../../src/utility/dom.ts"],"sourcesContent":["/**\n * String utility functions\n */\n\n/**\n * Converts camelCase to kebab-case (optimized for performance)\n * Uses direct character code manipulation for maximum speed\n * @example camelToKebab('backgroundColor') => 'background-color'\n */\nexport function camelToKebab(str: string): string {\n let result = '';\n for (let i = 0; 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 if (code >= 65 && code <= 90) {\n if (i > 0) result += '-';\n result += String.fromCharCode(code + 32);\n } else {\n result += str[i];\n }\n }\n return result;\n}\n\n/**\n * Escapes HTML special characters to prevent XSS\n */\nexport function escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return text.replace(/[&<>\"']/g, (char) => map[char]);\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, camelToKebab } from '../utility/stringUtils';\nimport { createElement } from '../utility/dom';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\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 if (name === 'style' && typeof value === 'object') {\n const styleStr = Object.entries(value)\n .map(([key, val]) => {\n const cssKey = camelToKebab(key);\n return `${cssKey}:${val}`;\n })\n .join(';');\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 */\nfunction serializeAttributes(element: Element): string {\n let result = '';\n\n // Handle polyfill elements with Map-based attributes\n if ('attributes' in element && element.attributes instanceof Map) {\n // First, handle special properties that might not be in the attributes Map\n const el = element as any;\n\n // Add id if it exists and is not already in attributes\n if (el.id && !element.attributes.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // Add class if it exists and is not already in attributes\n if (el.className && !element.attributes.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // Then add all attributes from the Map\n for (const [name, value] of element.attributes) {\n result += serializeAttribute(name, value);\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 * Self-closing HTML tags that don't have closing tags\n */\nconst VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr'\n]);\n\n/**\n * Get child nodes from an element (handles both browser and polyfill elements)\n */\nfunction getChildNodes(node: Node): ArrayLike<Node> {\n // Check for childNodes first (document fragments and browser elements)\n if ('childNodes' in node) {\n const childNodes = (node as any).childNodes;\n if (childNodes && (Array.isArray(childNodes) || childNodes.length !== undefined)) {\n return childNodes;\n }\n }\n // Polyfill elements have children array\n if ('children' in node) {\n const children = (node as any).children;\n if (children && Array.isArray(children)) {\n return children as ArrayLike<Node>;\n }\n }\n return [] as ArrayLike<Node>;\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeHtml(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 tagName = element.tagName.toLowerCase();\n const attributes = serializeAttributes(element);\n\n // Self-closing tags\n if (VOID_ELEMENTS.has(tagName)) {\n return `<${tagName}${attributes} />`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n const childNodes = getChildNodes(element);\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 }\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 = getChildNodes(node);\n if (childNodes && 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) {\n return '';\n }\n\n // If it's a function (NodeModFn), call it to create the element\n if (typeof input === 'function') {\n try {\n // Create a temporary container to render into\n const container = createElement('div');\n if (!container) {\n throw new Error('Document is not available. Make sure polyfills are loaded.');\n }\n\n const element = input(container as ExpandedElement<ElementTagName>, 0);\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 it's already a Node, serialize it directly\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","import { isBrowser } from \"./environment\";\nimport { logError } from \"./errorHandler\";\n\n/**\n * Creates an HTML element.\n * Wrapper for document.createElement with type safety.\n */\nexport function createElement<K extends keyof HTMLElementTagNameMap>(\n tagName: K\n): HTMLElementTagNameMap[K] | null;\nexport function createElement(tagName: string): ExpandedElement | null;\nexport function createElement(tagName: string): ExpandedElement | null {\n return globalThis.document ? document.createElement(tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates an element in the given namespace (typically for SVG elements).\n * Wrapper for document.createElementNS with type safety.\n */\nexport function createElementNS(\n namespace: string,\n tagName: string\n): ExpandedElement | null {\n return globalThis.document ? document.createElementNS(namespace, tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates a text node with the given content.\n * Wrapper for document.createTextNode.\n */\nexport function createTextNode(text: string): Text | null {\n return globalThis.document ? document.createTextNode(text) : null;\n}\n\n/**\n * Creates a document fragment.\n * Wrapper for document.createDocumentFragment.\n */\nexport function createDocumentFragment(): DocumentFragment | null {\n return globalThis.document ? document.createDocumentFragment() : null;\n}\n\nfunction safeAppendChild(parent: Element | Node, child: Node): boolean {\n try {\n parent.appendChild(child);\n return true;\n } catch (error) {\n logError('Failed to append child node', error);\n return false;\n }\n}\n\nexport function safeRemoveChild(child: Node): boolean {\n if (!child?.parentNode) return false;\n try {\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\nfunction createTextNodeSafely(text: string | number | boolean): Text | null {\n if (!isBrowser) return null;\n try {\n return createTextNode(String(text));\n } catch (error) {\n logError('Failed to create text node', error);\n return null;\n }\n}\n\nfunction createCommentSafely(text: string): Comment | null {\n if (!isBrowser) return null;\n try {\n return document.createComment(text);\n } catch (error) {\n logError('Failed to create comment node', error);\n return null;\n }\n}\n\n/**\n * Creates a comment node safely with error handling.\n * Exported for use across the codebase.\n */\nexport function createComment(text: string): Comment | null {\n return createCommentSafely(text);\n}\n\n/**\n * Creates a conditional comment placeholder node.\n * In SSR environments, this will still work because we bypass the isBrowser check.\n */\nexport function createConditionalComment(tagName: string, suffix: string = \"hidden\"): Comment | null {\n // For SSR, we need to create comments even when isBrowser is false\n // This function intentionally skips the isBrowser check for SSR compatibility\n try {\n return document.createComment(`conditional-${tagName}-${suffix}`);\n } catch (error) {\n logError('Failed to create conditional comment', error);\n return null;\n }\n}\n\nexport function createMarkerComment(prefix: string): Comment {\n if (!isBrowser) {\n throw new Error(\"Cannot create comment in non-browser environment\");\n }\n const comment = createCommentSafely(`${prefix}-${Math.random().toString(36).slice(2)}`);\n if (!comment) {\n throw new Error(\"Failed to create comment\");\n }\n return comment;\n}\n\nexport function createMarkerPair(prefix: string): { start: Comment; end: Comment } {\n const endComment = createCommentSafely(`${prefix}-end`);\n if (!endComment) {\n throw new Error(\"Failed to create end comment\");\n }\n return {\n start: createMarkerComment(`${prefix}-start`),\n end: endComment\n };\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 nodes.forEach(function(node) { safeInsertBefore(parent, node, referenceNode); });\n }\n}\n\nexport function appendChildren(\n parent: Element | Node,\n ...children: Array<Element | Node | string | null | undefined>\n): Element | Node {\n if (!parent) return parent;\n\n children.forEach(function(child) {\n if (child != null) {\n let nodeToAppend: Node;\n\n if (typeof child === \"string\") {\n const textNode = createTextNodeSafely(child);\n if (textNode) {\n nodeToAppend = textNode;\n } else {\n return;\n }\n } else {\n nodeToAppend = child as Node;\n }\n\n safeAppendChild(parent, nodeToAppend);\n }\n });\n\n return parent;\n}\n\nexport function isNodeConnected(node: Node | null | undefined): boolean {\n if (!node) return false;\n\n // Prefer the built-in isConnected property\n if (typeof node.isConnected === \"boolean\") {\n return node.isConnected;\n }\n\n // Fallback for older browsers (only if in browser environment)\n if (isBrowser && typeof document !== 'undefined') {\n return document.contains(node);\n }\n\n // In SSR or when document is not available, assume disconnected\n return false;\n}\n\n/**\n * Safely replaces an old node with a new node in the DOM.\n * Returns true on success, false on failure (and logs the error).\n */\nexport function replaceNodeSafely(oldNode: Node, newNode: Node): boolean {\n if (!oldNode?.parentNode) return false;\n try {\n oldNode.parentNode.replaceChild(newNode, oldNode);\n return true;\n } catch (error) {\n logError(\"Error replacing conditional node\", error);\n return false;\n }\n}\n"],"names":["escapeHtml","text","map","replace","char","serializeAttribute","name","value","styleStr","Object","entries","key","val","str","result","i","length","code","charCodeAt","String","fromCharCode","camelToKebab","join","VOID_ELEMENTS","Set","getChildNodes","node","childNodes","Array","isArray","undefined","children","serializeNode","nodeType","textContent","element","tagName","toLowerCase","attributes","Map","el","id","has","className","attr","serializeAttributes","childrenHtml","child","renderToString","input","container","globalThis","document","createElement","Error","error","console","renderManyToString","inputs","renderToStringWithContainer","containerTag","containerAttrs","content"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nuclo.ssr.js","sources":["../../src/utility/stringUtils.ts","../../src/ssr/renderToString.ts","../../src/utility/dom.ts"],"sourcesContent":["/**\n * String utility functions\n */\n\n/**\n * Converts camelCase to kebab-case (optimized for performance)\n * Uses direct character code manipulation for maximum speed\n * @example camelToKebab('backgroundColor') => 'background-color'\n */\nexport function camelToKebab(str: string): string {\n let result = '';\n for (let i = 0; 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 if (code >= 65 && code <= 90) {\n if (i > 0) result += '-';\n result += String.fromCharCode(code + 32);\n } else {\n result += str[i];\n }\n }\n return result;\n}\n\n/**\n * Escapes HTML special characters to prevent XSS\n */\nexport function escapeHtml(text: string): string {\n const map: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\n };\n return text.replace(/[&<>\"']/g, (char) => map[char]);\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, camelToKebab } from '../utility/stringUtils';\nimport { createElement } from '../utility/dom';\n\ntype RenderableInput =\n | NodeModFn<ElementTagName>\n | Element\n | Node\n | null\n | undefined;\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 if (name === 'style' && typeof value === 'object') {\n const styleStr = Object.entries(value)\n .map(([key, val]) => {\n const cssKey = camelToKebab(key);\n return `${cssKey}:${val}`;\n })\n .join(';');\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 */\nfunction serializeAttributes(element: Element): string {\n let result = '';\n\n // Handle polyfill elements with Map-based attributes\n if ('attributes' in element && element.attributes instanceof Map) {\n // First, handle special properties that might not be in the attributes Map\n const el = element as any;\n\n // Add id if it exists and is not already in attributes\n if (el.id && !element.attributes.has('id')) {\n result += serializeAttribute('id', el.id);\n }\n\n // Add class if it exists and is not already in attributes\n if (el.className && !element.attributes.has('class')) {\n result += serializeAttribute('class', el.className);\n }\n\n // Then add all attributes from the Map\n for (const [name, value] of element.attributes) {\n result += serializeAttribute(name, value);\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 * Self-closing HTML tags that don't have closing tags\n */\nconst VOID_ELEMENTS = new Set([\n 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',\n 'link', 'meta', 'param', 'source', 'track', 'wbr'\n]);\n\n/**\n * Get child nodes from an element (handles both browser and polyfill elements)\n */\nfunction getChildNodes(node: Node): ArrayLike<Node> {\n // Check for childNodes first (document fragments and browser elements)\n if ('childNodes' in node) {\n const childNodes = (node as any).childNodes;\n if (childNodes && (Array.isArray(childNodes) || childNodes.length !== undefined)) {\n return childNodes;\n }\n }\n // Polyfill elements have children array\n if ('children' in node) {\n const children = (node as any).children;\n if (children && Array.isArray(children)) {\n return children as ArrayLike<Node>;\n }\n }\n return [] as ArrayLike<Node>;\n}\n\n/**\n * Serializes a DOM node to HTML string\n */\nfunction serializeNode(node: Node): string {\n // Text node\n if (node.nodeType === 3) { // Node.TEXT_NODE\n return escapeHtml(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 tagName = element.tagName.toLowerCase();\n const attributes = serializeAttributes(element);\n\n // Self-closing tags\n if (VOID_ELEMENTS.has(tagName)) {\n return `<${tagName}${attributes} />`;\n }\n\n // Regular elements with children\n let childrenHtml = '';\n const childNodes = getChildNodes(element);\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 }\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 = getChildNodes(node);\n if (childNodes && 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) {\n return '';\n }\n\n // If it's a function (NodeModFn), call it to create the element\n if (typeof input === 'function') {\n try {\n // Create a temporary container to render into\n const container = createElement('div');\n if (!container) {\n throw new Error('Document is not available. Make sure polyfills are loaded.');\n }\n\n const element = input(container as ExpandedElement<ElementTagName>, 0);\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 it's already a Node, serialize it directly\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","import { isBrowser } from \"./environment\";\nimport { logError } from \"./errorHandler\";\nimport { removeAllListeners } from \"./on\";\nimport { cleanupReactiveElement, cleanupReactiveTextNode } from \"../core/reactive\";\nimport { unregisterConditionalNode } from \"./conditionalInfo\";\n\n/**\n * Creates an HTML element.\n * Wrapper for document.createElement with type safety.\n */\nexport function createElement<K extends keyof HTMLElementTagNameMap>(\n tagName: K\n): HTMLElementTagNameMap[K] | null;\nexport function createElement(tagName: string): ExpandedElement | null;\nexport function createElement(tagName: string): ExpandedElement | null {\n return globalThis.document ? document.createElement(tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates an element in the given namespace (typically for SVG elements).\n * Wrapper for document.createElementNS with type safety.\n */\nexport function createElementNS(\n namespace: string,\n tagName: string\n): ExpandedElement | null {\n return globalThis.document ? document.createElementNS(namespace, tagName) as ExpandedElement : null;\n}\n\n/**\n * Creates a text node with the given content.\n * Wrapper for document.createTextNode.\n */\nexport function createTextNode(text: string): Text | null {\n return globalThis.document ? document.createTextNode(text) : null;\n}\n\n/**\n * Creates a document fragment.\n * Wrapper for document.createDocumentFragment.\n */\nexport function createDocumentFragment(): DocumentFragment | null {\n return globalThis.document ? document.createDocumentFragment() : null;\n}\n\nfunction safeAppendChild(parent: Element | Node, child: Node): boolean {\n try {\n parent.appendChild(child);\n return true;\n } catch (error) {\n logError('Failed to append child node', error);\n return false;\n }\n}\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 cleanupEventListeners(node: Node): void {\n // Clean up the node itself based on its type\n if (node.nodeType === Node.ELEMENT_NODE) {\n const element = node as HTMLElement;\n // Remove all event listeners\n removeAllListeners(element);\n // Remove reactive attribute resolvers\n cleanupReactiveElement(element);\n // Remove conditional info\n unregisterConditionalNode(element);\n } else if (node.nodeType === Node.TEXT_NODE) {\n // Remove reactive text node info\n cleanupReactiveTextNode(node as Text);\n } else if (node.nodeType === Node.COMMENT_NODE) {\n // Remove conditional info from comment nodes (used by when/list)\n unregisterConditionalNode(node);\n }\n \n // Recursively clean up all child nodes\n if (node.childNodes && node.childNodes.length > 0) {\n for (let i = 0; i < node.childNodes.length; i++) {\n cleanupEventListeners(node.childNodes[i]);\n }\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 cleanupEventListeners(child);\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\nfunction createTextNodeSafely(text: string | number | boolean): Text | null {\n if (!isBrowser) return null;\n try {\n return createTextNode(String(text));\n } catch (error) {\n logError('Failed to create text node', error);\n return null;\n }\n}\n\nfunction createCommentSafely(text: string): Comment | null {\n if (!isBrowser) return null;\n try {\n return document.createComment(text);\n } catch (error) {\n logError('Failed to create comment node', error);\n return null;\n }\n}\n\n/**\n * Creates a comment node safely with error handling.\n * Exported for use across the codebase.\n */\nexport function createComment(text: string): Comment | null {\n return createCommentSafely(text);\n}\n\n/**\n * Creates a conditional comment placeholder node.\n * In SSR environments, this will still work because we bypass the isBrowser check.\n */\nexport function createConditionalComment(tagName: string, suffix: string = \"hidden\"): Comment | null {\n // For SSR, we need to create comments even when isBrowser is false\n // This function intentionally skips the isBrowser check for SSR compatibility\n try {\n return document.createComment(`conditional-${tagName}-${suffix}`);\n } catch (error) {\n logError('Failed to create conditional comment', error);\n return null;\n }\n}\n\nexport function createMarkerComment(prefix: string): Comment {\n if (!isBrowser) {\n throw new Error(\"Cannot create comment in non-browser environment\");\n }\n const comment = createCommentSafely(`${prefix}-${Math.random().toString(36).slice(2)}`);\n if (!comment) {\n throw new Error(\"Failed to create comment\");\n }\n return comment;\n}\n\nexport function createMarkerPair(prefix: string): { start: Comment; end: Comment } {\n const endComment = createCommentSafely(`${prefix}-end`);\n if (!endComment) {\n throw new Error(\"Failed to create end comment\");\n }\n return {\n start: createMarkerComment(`${prefix}-start`),\n end: endComment\n };\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 nodes.forEach(function(node) { safeInsertBefore(parent, node, referenceNode); });\n }\n}\n\nexport function appendChildren(\n parent: Element | Node,\n ...children: Array<Element | Node | string | null | undefined>\n): Element | Node {\n if (!parent) return parent;\n\n children.forEach(function(child) {\n if (child != null) {\n let nodeToAppend: Node;\n\n if (typeof child === \"string\") {\n const textNode = createTextNodeSafely(child);\n if (textNode) {\n nodeToAppend = textNode;\n } else {\n return;\n }\n } else {\n nodeToAppend = child as Node;\n }\n\n safeAppendChild(parent, nodeToAppend);\n }\n });\n\n return parent;\n}\n\nexport function isNodeConnected(node: Node | null | undefined): boolean {\n if (!node) return false;\n\n // Prefer the built-in isConnected property\n if (typeof node.isConnected === \"boolean\") {\n return node.isConnected;\n }\n\n // Fallback for older browsers (only if in browser environment)\n if (isBrowser && typeof document !== 'undefined') {\n return document.contains(node);\n }\n\n // In SSR or when document is not available, assume disconnected\n return false;\n}\n\n/**\n * Safely replaces an old node with a new node in the DOM.\n * Returns true on success, false on failure (and logs the error).\n */\nexport function replaceNodeSafely(oldNode: Node, newNode: Node): boolean {\n if (!oldNode?.parentNode) return false;\n try {\n oldNode.parentNode.replaceChild(newNode, oldNode);\n return true;\n } catch (error) {\n logError(\"Error replacing conditional node\", error);\n return false;\n }\n}\n"],"names":["escapeHtml","text","map","replace","char","serializeAttribute","name","value","styleStr","Object","entries","key","val","str","result","i","length","code","charCodeAt","String","fromCharCode","camelToKebab","join","VOID_ELEMENTS","Set","getChildNodes","node","childNodes","Array","isArray","undefined","children","serializeNode","nodeType","textContent","element","tagName","toLowerCase","attributes","Map","el","id","has","className","attr","serializeAttributes","childrenHtml","child","renderToString","input","container","globalThis","document","createElement","Error","error","console","renderManyToString","inputs","renderToStringWithContainer","containerTag","containerAttrs","content"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/utility/dom.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/utility/dom.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,qBAAqB,EACjE,OAAO,EAAE,CAAC,GACT,qBAAqB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;AACnC,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAAC;AAKvE;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,eAAe,GAAG,IAAI,CAExB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAExD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,GAAG,IAAI,CAEhE;AA0CD,wBAAgB,eAAe,CAAC,KAAK,EAAE,IAAI,GAAG,OAAO,CAWpD;AAgCD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAE1D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAiB,GAAG,OAAO,GAAG,IAAI,CASnG;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAS3D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,CASjF;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAOlF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,GAAG,IAAI,CAK1E;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,GAAG,IAAI,EACtB,GAAG,QAAQ,EAAE,KAAK,CAAC,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAC7D,OAAO,GAAG,IAAI,CAuBhB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAetE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,GAAG,OAAO,CASvE"}
|
package/dist/utility/on.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on.d.ts","sourceRoot":"","sources":["../../src/utility/on.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;
|
|
1
|
+
{"version":3,"file":"on.d.ts","sourceRoot":"","sources":["../../src/utility/on.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAuCH;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,GACjB,IAAI,CA4BN;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CAkCN;AAED;;GAEG;AACH,wBAAgB,EAAE,CAChB,CAAC,SAAS,MAAM,mBAAmB,EACnC,QAAQ,SAAS,cAAc,GAAG,cAAc,EAEhD,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,OAAO,EACjD,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEvB;;;;GAIG;AACH,wBAAgB,EAAE,CAChB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,KAAK,GAAG,KAAK,EACvB,QAAQ,SAAS,cAAc,GAAG,cAAc,EAEhD,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,OAAO,EAC5B,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,SAAS,CAAC,QAAQ,CAAC,CAAC"}
|
package/dist/when/runtime.d.ts
CHANGED
|
@@ -27,14 +27,16 @@ export interface WhenRuntime<TTagName extends ElementTagName = ElementTagName> {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function renderWhenContent<TTagName extends ElementTagName>(runtime: WhenRuntime<TTagName>): void;
|
|
29
29
|
/**
|
|
30
|
-
* Registers a when runtime for tracking and updates
|
|
30
|
+
* Registers a when runtime for tracking and updates.
|
|
31
|
+
* Uses WeakRef to prevent memory leaks when elements are removed.
|
|
32
|
+
* The runtime object itself is mutable and will be updated in place.
|
|
31
33
|
*/
|
|
32
34
|
export declare function registerWhenRuntime<TTagName extends ElementTagName>(runtime: WhenRuntime<TTagName>): void;
|
|
33
35
|
/**
|
|
34
36
|
* Updates all active when/else conditional runtimes.
|
|
35
37
|
*
|
|
36
38
|
* Re-evaluates all conditional branches and re-renders if the active branch has changed.
|
|
37
|
-
* Automatically cleans up runtimes that
|
|
39
|
+
* Automatically cleans up runtimes that are garbage collected or disconnected from DOM.
|
|
38
40
|
*
|
|
39
41
|
* This function should be called after state changes that affect conditional expressions.
|
|
40
42
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/when/runtime.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/when/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;AACtD,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,cAAc,GAAG,cAAc,IACtE,OAAO,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C,MAAM,WAAW,SAAS,CAAC,QAAQ,SAAS,cAAc,GAAG,cAAc;IACzE,SAAS,EAAE,aAAa,CAAC;IACzB,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,WAAW,CAAC,QAAQ,SAAS,cAAc,GAAG,cAAc;IAC3E,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC9B,WAAW,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;IACrC;;;;;OAKG;IACH,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,IAAI,IAAI,CAAC;CAChB;AA6BD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,SAAS,cAAc,EAC/D,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,GAC7B,IAAI,CAoBN;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,cAAc,EACjE,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,GAC7B,IAAI,CAKN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,CAiC5D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|