UNPKG

compomint

Version:

A lightweight JavaScript component engine for building web applications with a focus on component-based architecture and template system.

988 lines (979 loc) 134 kB
/****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; const firstElementChild = function (ele) { if (ele.firstElementChild) return ele.firstElementChild; const children = ele.childNodes; for (let i = 0, size = children.length; i < size; i++) { if (children[i] instanceof Element) { return children[i]; } } return null; }; const childElementCount = function (ele) { return (ele.childElementCount || Array.prototype.filter.call(ele.childNodes || [], function (child) { return child instanceof Element; }).length); }; const cleanNode = function (node) { if (!node.childNodes) return; for (let n = 0; n < node.childNodes.length; n++) { const child = node.childNodes[n]; if (child.nodeType === 8 || // Comment node (child.nodeType === 3 && !/\S/.test(child.nodeValue || '')) // Text node with only whitespace ) { node.removeChild(child); n--; // Adjust index after removal } else if (child.nodeType === 1) { // Element node cleanNode(child); // Recurse } } }; const getDOMParser = () => { if (typeof DOMParser !== 'undefined') { return new DOMParser(); } // SSR fallback - create mock DOMParser return { parseFromString: (str, type) => { const mockDoc = { body: { childNodes: [], firstChild: null, appendChild: () => { }, removeChild: () => { } } }; return mockDoc; } }; }; const stringToElement = function (str) { if (typeof str === 'number' || !isNaN(Number(str))) { return document.createTextNode(String(str)); } else if (typeof str === 'string') { try { const domParser = getDOMParser(); const doc = domParser.parseFromString(str, "text/html"); const body = doc.body; if (body.childNodes.length === 1) { return body.firstChild; } else { const fragment = document.createDocumentFragment(); while (body.firstChild) { fragment.appendChild(body.firstChild); } return fragment; } } catch (e) { return document.createTextNode(str); } } else { return document.createTextNode(''); } }; const isPlainObject = function (value) { return Object.prototype.toString.call(value) === '[object Object]'; }; // // Default template settings // const defaultTemplateEngine = (compomint) => { const configs = compomint.configs; return { rules: { style: { pattern: /(\<style id=[\s\S]+?\>[\s\S]+?\<\/style\>)/g, exec: function (style) { var _a; // Create a temporary element to parse the style tag const dumy = document.createElement("template"); dumy.innerHTML = style; const styleNode = (dumy.content || dumy).querySelector("style"); if (!styleNode || !styleNode.id) return ""; // Skip if no style node or ID const oldStyleNode = document.getElementById(styleNode.id); if (oldStyleNode) (_a = oldStyleNode.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(oldStyleNode); document.head.appendChild(styleNode); return ""; }, }, commentArea: { pattern: /##\*([\s\S]+?)##/g, exec: function (commentArea) { // Return an empty string to remove the comment block return ``; }, }, preEvaluate: { pattern: /##!([\s\S]+?)##/g, exec: function (preEvaluate, tmplId) { try { // Execute the code in a new function context new Function("compomint", "tmplId", preEvaluate)(compomint, tmplId); } catch (e) { if (configs.throwError) { console.error(`Template preEvaluate error in "${tmplId}", ${e.name}: ${e.message}`); throw e; } else { console.warn(`Template preEvaluate error in "${tmplId}", ${e.name}: ${e.message}`); } } return ``; }, }, interpolate: { pattern: /##=([\s\S]+?)##/g, exec: function (interpolate) { // Construct JavaScript code to interpolate the value const interpolateSyntax = `typeof (interpolate)=='function' ? (interpolate)() : (interpolate)`; return `';\n{let __t, interpolate=${interpolate};\n__p+=((__t=(${interpolateSyntax}))==null ? '' : String(__t) );};\n__p+='`; // Ensure string conversion }, }, escape: { pattern: /##-([\s\S]+?)##/g, exec: function (escape) { const escapeSyntax = `compomint.tools.escapeHtml.escape(typeof (escape)=='function' ? (escape)() : (escape))`; // Construct JavaScript code to escape HTML characters in the value return `';\n{let __t, escape=${escape};\n__p+=((__t=(${escapeSyntax}))==null ? '' : String(__t) );};\n__p+='`; // Ensure string conversion before escape }, }, elementProps: { pattern: /data-co-props="##:([\s\S]+?)##"/g, exec: function (props) { const source = `';\n{const eventId = (__lazyScope.elementPropsArray.length);\n__p+='data-co-props="'+eventId+'"';\n __lazyScope.elementPropsArray[eventId] = ${props}};\n__p+='`; // Store props in lazy scope return source; }, lazyExec: function (data, lazyScope, component, wrapper) { // Iterate over stored props and apply them to elements lazyScope.elementPropsArray.forEach(function (props, eventId) { if (!props) return; // Find the element with the corresponding data-co-props attribute const $elementTrigger = wrapper.querySelector(`[data-co-props="${eventId}"]`); // Remove the temporary attribute and set the properties if (!$elementTrigger) return; delete $elementTrigger.dataset.coProps; Object.keys(props).forEach(function (key) { $elementTrigger.setAttribute(key, String(props[key])); // Ensure value is string }); }); }, }, namedElement: { pattern: /data-co-named-element="##:([\s\S]+?)##"/g, exec: function (key) { const source = `';\nconst eventId = (__lazyScope.namedElementArray.length);\n__p+='data-co-named-element="'+eventId+'"';\n __lazyScope.namedElementArray[eventId] = ${key};\n__p+='`; // Store the key in lazy scope return source; }, lazyExec: function (data, lazyScope, component, wrapper) { // Iterate over stored keys and assign elements to the component lazyScope.namedElementArray.forEach(function (key, eventId) { // Find the element with the corresponding data-co-named-element attribute const $elementTrigger = wrapper.querySelector(`[data-co-named-element="${eventId}"]`); // Assign the element to the component using the key if (!$elementTrigger) { if (configs.debug) console.warn(`Named element target not found for ID ${eventId} in template ${component.tmplId}`); return; } delete $elementTrigger.dataset.coNamedElement; component[key] = $elementTrigger; }); }, }, elementRef: { pattern: /data-co-element-ref="##:([\s\S]+?)##"/g, exec: function (key) { const source = `';\n{const eventId = (__lazyScope.elementRefArray.length);\n__p+='data-co-element-ref="'+eventId+'"'; var ${key} = null;\n__lazyScope.elementRefArray[eventId] = function(target) {${key} = target;}};\n__p+='`; // Store a function to assign the element return source; }, lazyExec: function (data, lazyScope, component, wrapper) { // Iterate over stored functions and call them with the corresponding elements lazyScope.elementRefArray.forEach(function (func, eventId) { // Find the element with the corresponding data-co-element-ref attribute const $elementTrigger = wrapper.querySelector(`[data-co-element-ref="${eventId}"]`); // Call the stored function with the element if (!$elementTrigger) { if (configs.debug) console.warn(`Element ref target not found for ID ${eventId} in template ${component.tmplId}`); return; } delete $elementTrigger.dataset.coElementRef; func.call($elementTrigger, $elementTrigger); }); }, }, elementLoad: { pattern: /data-co-load="##:([\s\S]+?)##"/g, exec: function (elementLoad) { const elementLoadSplitArray = elementLoad.split("::"); // Store the load function and custom data in lazy scope const source = `';\n{const eventId = (__lazyScope.elementLoadArray.length);\n__p+='data-co-load="'+eventId+'"'; __lazyScope.elementLoadArray[eventId] = {loadFunc: ${elementLoadSplitArray[0]}, customData: ${elementLoadSplitArray[1]}};}\n__p+='`; // 'customData' is determined when compiled, so it does not change even if refreshed. return source; }, lazyExec: function (data, lazyScope, component, wrapper) { // Iterate over stored load functions and execute them with the corresponding elements lazyScope.elementLoadArray.forEach(function (elementLoad, eventId) { // Find the element with the corresponding data-co-load attribute const $elementTrigger = wrapper.querySelector(`[data-co-load="${eventId}"]`); if (!$elementTrigger) { if (configs.debug) console.warn(`Element load target not found for ID ${eventId} in template ${component.tmplId}`); return; } // Execute the load function with the element and context delete $elementTrigger.dataset.coLoad; try { if (typeof elementLoad.loadFunc === "function") { const loadFuncParams = [ $elementTrigger, $elementTrigger, { data: data, element: $elementTrigger, customData: elementLoad.customData, component: component, compomint: compomint, }, ]; elementLoad.loadFunc.call(...loadFuncParams); } } catch (e) { console.error(`Error executing elementLoad function for ID ${eventId} in template ${component.tmplId}:`, e, elementLoad.loadFunc); if (configs.throwError) throw e; } }); }, }, event: { pattern: /data-co-event="##:([\s\S]+?)##"/g, exec: function (event) { const eventStrArray = event.split(":::"); // eventStrArray = ["eventFunc::customData", "eventFunc::customData"] // Store event handlers in lazy scope let source = `';\n{const eventId = (__lazyScope.eventArray.length);\n__p+='data-co-event="'+eventId+'"';\n`; const eventArray = []; for (let i = 0, size = eventStrArray.length; i < size; i++) { const eventSplitArray = eventStrArray[i].split("::"); eventArray.push(`{eventFunc: ${eventSplitArray[0]}, $parent: this, customData: ${eventSplitArray[1]}}`); } source += `__lazyScope.eventArray[eventId] = [${eventArray.join(",")}];}\n__p+='`; return source; }, lazyExec: function (data, lazyScope, component, wrapper) { const self = this; // Cast self to TemplateSettings const attacher = self.attacher; if (!attacher) return; // Guard against missing attacher // Iterate over stored event handlers and attach them to elements lazyScope.eventArray.forEach(function (selectedArray, eventId) { // Find the element with the corresponding data-co-event attribute const $elementTrigger = wrapper.querySelector(`[data-co-event="${eventId}"]`); if (!$elementTrigger) { if (configs.debug) console.warn(`Event target not found for ID ${eventId} in template ${component.tmplId}`); // Debugging: Log if target not found return; } delete $elementTrigger.dataset.coEvent; for (let i = 0, size = selectedArray.length; i < size; i++) { const selected = selectedArray[i]; if (selected.eventFunc) { if (Array.isArray(selected.eventFunc)) { selected.eventFunc.forEach(function (func) { attacher(self, data, lazyScope, component, wrapper, $elementTrigger, func, selected); }); } else { attacher(self, data, lazyScope, component, wrapper, $elementTrigger, selected.eventFunc, selected); } } } }); }, trigger: function (target, eventName) { const customEvent = new Event(eventName, { // Dispatch a custom event on the target element bubbles: true, cancelable: true, }); target.dispatchEvent(customEvent); }, attacher: function (self, // Type properly if possible data, lazyScope, component, wrapper, $elementTrigger, eventFunc, eventData) { const trigger = self.trigger; const $childTarget = firstElementChild(wrapper); const $targetElement = childElementCount(wrapper) === 1 ? $childTarget : null; // Attach event listeners based on the type of eventFunc if (!eventFunc) { return; } const eventFuncParams = [ $elementTrigger, null, { data: data, customData: eventData.customData, element: $elementTrigger, componentElement: $targetElement || ($childTarget === null || $childTarget === void 0 ? void 0 : $childTarget.parentElement), component: component, compomint: compomint, }, ]; // Basic case: eventFunc is a single function if (typeof eventFunc === "function") { const eventListener = function (event) { event.stopPropagation(); eventFuncParams[1] = event; try { eventFunc.call(...eventFuncParams); } catch (e) { console.error(`Error in event handler for template ${component.tmplId}:`, e, eventFunc); if (configs.throwError) throw e; } }; // Attach a click event listener for a single function $elementTrigger.addEventListener("click", eventListener); eventData.element = $elementTrigger; // For remove eventListener eventData.eventFunc = eventListener; // For remove eventListener return; } if (!isPlainObject(eventFunc)) { return; } // Advanced case: eventFunc is an object mapping event types to handlers const eventMap = eventFunc; // Handle event map with multiple event types const triggerName = eventMap.triggerName; // Optional key to store trigger functions if (triggerName) { component.trigger = component.trigger || {}; component.trigger[triggerName] = {}; } Object.keys(eventMap).forEach(function (eventType) { const selectedEventFunc = eventMap[eventType]; // Handle special event types like "load", "namedElement", and "triggerName" if (eventType === "load") { eventFuncParams[1] = $elementTrigger; try { selectedEventFunc.call(...eventFuncParams); } catch (e) { console.error(`Error in 'load' event handler for template ${component.tmplId}:`, e, selectedEventFunc); if (configs.throwError) throw e; } return; } else if (eventType === "namedElement") { component[selectedEventFunc] = $elementTrigger; return; } else if (eventType === "triggerName") { return; // Attach event listeners for other event types } const eventListener = function (event) { event.stopPropagation(); eventFuncParams[1] = event; try { selectedEventFunc.call(...eventFuncParams); } catch (e) { console.error(`Error in '${eventType}' event handler for template ${component.tmplId}:`, e, selectedEventFunc); if (configs.throwError) throw e; } }; $elementTrigger.addEventListener(eventType, eventListener); eventData.element = $elementTrigger; // For remove eventListener eventFunc[eventType] = eventListener; // For remove eventListener if (triggerName && trigger) { component.trigger[triggerName][eventType] = function () { trigger($elementTrigger, eventType); }; } }); }, }, element: { pattern: /##%([\s\S]+?)##/g, exec: function (target) { // Store element insertion information in lazy scope const elementSplitArray = target.split("::"); const source = `';\n{ const elementId = (__lazyScope.elementArray.length); __p+='<template data-co-tmpl-element-id="'+elementId+'"></template>'; __lazyScope.elementArray[elementId] = {childTarget: ${elementSplitArray[0]}, nonblocking: ${elementSplitArray[1] || false}};}; __p+='`; return source; }, lazyExec: function (data, lazyScope, component, wrapper) { lazyScope.elementArray.forEach(function (ele, elementId) { // Retrieve element insertion details from lazy scope const childTarget = ele.childTarget; const nonblocking = ele.nonblocking; // Find the placeholder element const $tmplElement = wrapper.querySelector(`template[data-co-tmpl-element-id="${elementId}"]`); // Perform the element insertion if (!$tmplElement) { if (configs.debug) console.warn(`Element insertion placeholder not found for ID ${elementId} in template ${component.tmplId}`); return; } if (!$tmplElement.parentNode) { if (configs.debug) console.warn(`Element insertion placeholder for ID ${elementId} in template ${component.tmplId} has no parent.`); return; } const doFunc = function () { if (!$tmplElement || !$tmplElement.parentNode) { if (configs.debug) console.warn(`Placeholder for ID ${elementId} removed before insertion in template ${component.tmplId}.`); return; } // Handle different types of childTarget for insertion try { if (childTarget instanceof Array) { const docFragment = document.createDocumentFragment(); childTarget.forEach(function (child) { if (!child) return; const childElement = child.element || child; let nodeToAppend = null; // Convert child to a DOM node if necessary if (typeof childElement === "string" || typeof childElement === "number") { nodeToAppend = stringToElement(childElement); } else if (typeof childElement === "function") { nodeToAppend = stringToElement(childElement()); } else if (childElement instanceof Node) { nodeToAppend = childElement; } else { if (configs.debug) console.warn(`Invalid item type in element array for ID ${elementId}, template ${component.tmplId}:`, childElement); return; } // Append the node to the document fragment if (child.beforeAppendTo) { try { child.beforeAppendTo(); } catch (e) { console.error("Error in beforeAppendTo (array item):", e); } } if (nodeToAppend) docFragment.appendChild(nodeToAppend); }); // Replace the placeholder with the document fragment $tmplElement.parentNode.replaceChild(docFragment, $tmplElement); // Call afterAppendTo for each child childTarget.forEach(function (child) { if (child && child.afterAppendTo) { setTimeout(() => { try { child.afterAppendTo(); } catch (e) { console.error("Error in afterAppendTo (array item):", e); } }, 0); } }); // Handle string, number, or function types } else if (typeof childTarget === "string" || typeof childTarget === "number") { $tmplElement.parentNode.replaceChild(stringToElement(childTarget), $tmplElement); // Handle function type } else if (typeof childTarget === "function") { $tmplElement.parentNode.replaceChild(stringToElement(childTarget()), $tmplElement); // Handle Node or ComponentScope types } else if (childTarget && (childTarget.element || childTarget) instanceof Node) { const childScope = childTarget; // Assume it might be a scope const childElement = childScope.element || childScope; // Replace the placeholder with the child element if (childScope.beforeAppendTo) { try { childScope.beforeAppendTo(); } catch (e) { console.error("Error in beforeAppendTo:", e); } } $tmplElement.parentNode.replaceChild(childElement, $tmplElement); // Call afterAppendTo if available if (childScope.afterAppendTo) { setTimeout(() => { try { if (childScope.afterAppendTo) childScope.afterAppendTo(); } catch (e) { console.error("Error in afterAppendTo:", e); } }, 0); } // Set parentComponent if it's a component if (childScope.tmplId) { childScope.parentComponent = component; } // Handle invalid target types } else { if (configs.debug) console.warn(`Invalid target for element insertion ID ${elementId}, template ${component.tmplId}:`, childTarget); $tmplElement.parentNode.removeChild($tmplElement); } } catch (e) { console.error(`Error during element insertion for ID ${elementId}, template ${component.tmplId}:`, e); if (configs.throwError) throw e; if ($tmplElement && $tmplElement.parentNode) { try { $tmplElement.parentNode.removeChild($tmplElement); } catch (removeError) { /* Ignore */ } } } // end try }; // end doFunc nonblocking === undefined || nonblocking === false ? // Execute immediately or with a delay based on nonblocking doFunc() : setTimeout(doFunc, typeof nonblocking === "number" ? nonblocking : 0); }); // end forEach }, }, lazyEvaluate: { pattern: /###([\s\S]+?)##/g, exec: function (lazyEvaluate) { const source = `';\n__lazyScope.lazyEvaluateArray.push(function(data) {${lazyEvaluate}});\n__p+='`; // Store the lazy evaluation function in lazy scope return source; }, lazyExec: function (data, lazyScope, component, wrapper) { // Execute stored lazy evaluation functions const $childTarget = firstElementChild(wrapper); const $targetElement = childElementCount(wrapper) === 1 ? $childTarget : null; lazyScope.lazyEvaluateArray.forEach(function (selectedFunc, idx) { // Call the function with the appropriate context try { selectedFunc.call($targetElement || wrapper, data); // Use wrapper if multiple elements } catch (e) { console.error(`Error in lazyEvaluate block ${idx} for template ${component.tmplId}:`, e, selectedFunc); if (configs.throwError) throw e; } }); return; }, }, evaluate: { pattern: /##([\s\S]+?)##/g, exec: (evaluate) => { // Insert arbitrary JavaScript code into the template function return "';\n" + evaluate + "\n__p+='"; }, }, escapeSyntax: { pattern: /#\\#([\s\S]+?)#\\#/g, exec: function (syntax) { return `'+\n'##${syntax}##'+\n'`; }, }, }, keys: { dataKeyName: "data", statusKeyName: "status", componentKeyName: "component", i18nKeyName: "i18n", }, }; }; const applyBuiltInTemplates = (addTmpl) => { // co-Ele is a shorthand for co-Element, it will generate a div element with the given props and event addTmpl("co-Ele", `<##=data[0]##></##=data[0]##>###compomint.tools.applyElementProps(this, data[1]);##`); addTmpl("co-Element", `## data.tag = data.tag || 'div'; ## &lt;##=data.tag## ##=data.id ? 'id="' + (data.id === true ? component._id : data.id) + '"' : ''## data-co-props="##:data.props##" data-co-event="##:data.event##"&gt; ##if (typeof data.content === "string") {## ##=data.content## ##} else {## ##%data.content## ##}## &lt;/##=data.tag##&gt;`); }; /* * Copyright (c) 2025-present, Choi Sungho * Code released under the MIT license */ /** * Environment detection utilities */ // Store original window state before SSR setup const _originalWindow = typeof window; const Environment = { // Check if we're in a server environment isServer() { return (_originalWindow === 'undefined' || globalThis.__SSR_ENVIRONMENT__) && typeof globalThis !== 'undefined' && typeof globalThis.process !== 'undefined' && typeof globalThis.process.versions !== 'undefined' && !!globalThis.process.versions.node; }, // Check if we're in a browser environment isBrowser() { return _originalWindow !== 'undefined' && typeof document !== 'undefined' && !globalThis.__SSR_ENVIRONMENT__; }, // Check if DOM APIs are available hasDOM() { return typeof document !== 'undefined' && typeof document.createElement === 'function'; }, // Check if we're in a Node.js environment isNode() { return typeof globalThis.process !== 'undefined' && typeof globalThis.process.versions !== 'undefined' && !!globalThis.process.versions.node; } }; /** * DOM Polyfills for Server-Side Rendering */ class SSRDOMPolyfill { constructor() { this.elements = new Map(); this.styleCollector = []; this.scriptCollector = []; } static getInstance() { if (!SSRDOMPolyfill.instance) { SSRDOMPolyfill.instance = new SSRDOMPolyfill(); } return SSRDOMPolyfill.instance; } /** * Create a minimal DOM-like element for SSR */ createElement(tagName) { const element = { nodeType: 1, // Element nodeType tagName: tagName.toUpperCase(), id: '', className: '', textContent: '', _innerHTML: '', attributes: new Map(), children: [], parentNode: null, style: {}, dataset: {}, firstChild: null, lastChild: null, childElementCount: 0, firstElementChild: null, content: null, // For template elements // Make childNodes iterable get childNodes() { return this.children; }, setAttribute(name, value) { this.attributes.set(name, value); if (name === 'id') this.id = value; if (name === 'class') this.className = value; }, // Override innerHTML setter to parse HTML set innerHTML(html) { this._innerHTML = html; // Clear existing children this.children = []; // Parse HTML and create child elements if (html) { this.parseAndCreateChildren(html); } }, get innerHTML() { return this._innerHTML || ''; }, parseAndCreateChildren(html) { // Simple HTML parsing for template elements - more flexible regex const templateRegex = /<template[^>]*?id\s*=\s*["']([^"']+)["'][^>]*?>([\s\S]*?)<\/template>/gi; const scriptRegex = /<script[^>]*?type\s*=\s*["']text\/template["'][^>]*?id\s*=\s*["']([^"']+)["'][^>]*?>([\s\S]*?)<\/script>/gi; const scriptCompomintRegex = /<script[^>]*?type\s*=\s*["']text\/compomint["'][^>]*?id\s*=\s*["']([^"']+)["'][^>]*?>([\s\S]*?)<\/script>/gi; let match; // Match template elements templateRegex.lastIndex = 0; // Reset regex while ((match = templateRegex.exec(html)) !== null) { const templateElement = this.createTemplateElement(match[1], match[2]); this.children.push(templateElement); templateElement.parentNode = this; } // Match script[type="text/template"] elements scriptRegex.lastIndex = 0; // Reset regex while ((match = scriptRegex.exec(html)) !== null) { const scriptElement = this.createScriptElement(match[1], match[2], 'text/template'); this.children.push(scriptElement); scriptElement.parentNode = this; } // Match script[type="text/compomint"] elements scriptCompomintRegex.lastIndex = 0; // Reset regex while ((match = scriptCompomintRegex.exec(html)) !== null) { const scriptElement = this.createScriptElement(match[1], match[2], 'text/compomint'); this.children.push(scriptElement); scriptElement.parentNode = this; } }, createTemplateElement(id, content) { const polyfill = SSRDOMPolyfill.getInstance(); const template = polyfill.createElement('template'); template.id = id; template.setAttribute('id', id); // Unescape HTML entities for template content const unescapedContent = content .replace(/&lt;/g, '<') .replace(/&gt;/g, '>') .replace(/&amp;/g, '&') .replace(/&quot;/g, '"') .replace(/&#x27;/g, "'"); template._innerHTML = unescapedContent; return template; }, createScriptElement(id, content, type) { const polyfill = SSRDOMPolyfill.getInstance(); const script = polyfill.createElement('script'); script.id = id; script.setAttribute('id', id); script.setAttribute('type', type); script._innerHTML = content; return script; }, getAttribute(name) { return this.attributes.get(name) || null; }, appendChild(child) { this.children.push(child); child.parentNode = this; this.firstChild = this.children[0] || null; this.lastChild = this.children[this.children.length - 1] || null; this.childElementCount = this.children.length; this.firstElementChild = this.children[0] || null; return child; }, removeChild(child) { const index = this.children.indexOf(child); if (index > -1) { this.children.splice(index, 1); child.parentNode = null; this.firstChild = this.children[0] || null; this.lastChild = this.children[this.children.length - 1] || null; this.childElementCount = this.children.length; this.firstElementChild = this.children[0] || null; } return child; }, normalize() { // Mock normalize function }, querySelector(selector) { // Simple implementation for basic selectors if (selector.startsWith('#')) { const id = selector.substring(1); return this.findById(id); } if (selector.startsWith('.')) { const className = selector.substring(1); return this.findByClass(className); } return this.findByTagName(selector); }, querySelectorAll(selector) { const results = []; // Handle comma-separated selectors const selectors = selector.split(',').map(s => s.trim()); for (const sel of selectors) { // Check self first if (this.matches && this.matches(sel)) { if (!results.includes(this)) { results.push(this); } } // Search children recursively for (const child of this.children) { if (child.querySelectorAll) { const childResults = child.querySelectorAll(sel); for (const result of childResults) { if (!results.includes(result)) { results.push(result); } } } } } return results; }, matches(selector) { const trimmedSelector = selector.trim(); if (trimmedSelector.startsWith('#')) { return this.id === trimmedSelector.substring(1); } if (trimmedSelector.startsWith('.')) { return this.className.includes(trimmedSelector.substring(1)); } // Handle attribute selectors like template[id], script[type="text/template"][id] if (trimmedSelector.includes('[') && trimmedSelector.includes(']')) { // Extract tag name if present const tagMatch = trimmedSelector.match(/^(\w+)(?:\[|$)/); if (tagMatch) { const expectedTag = tagMatch[1].toLowerCase(); if (this.tagName.toLowerCase() !== expectedTag) { return false; } } // Extract all attribute selectors const attrMatches = trimmedSelector.match(/\[([^\]]+)\]/g); if (attrMatches) { for (const attrMatch of attrMatches) { // Parse individual attribute selector const attrContent = attrMatch.slice(1, -1); // Remove [ and ] if (attrContent.includes('=')) { // Attribute with value like [type="text/template"] const parts = attrContent.split('='); const attrName = parts[0].trim(); const attrValue = parts[1].replace(/['"]/g, '').trim(); if (this.getAttribute(attrName) !== attrValue) { return false; } } else { // Attribute without value like [id] const attrName = attrContent.trim(); const hasAttr = this.getAttribute(attrName) !== null; if (!hasAttr) { return false; } } } } return true; } // Simple tag selector return this.tagName.toLowerCase() === trimmedSelector.toLowerCase(); }, findById(id) { if (this.id === id) return this; for (const child of this.children) { const found = child.findById && child.findById(id); if (found) return found; } return null; }, findByClass(className) { if (this.className.includes(className)) return this; for (const child of this.children) { const found = child.findByClass && child.findByClass(className); if (found) return found; } return null; }, findByTagName(tagName) { if (this.tagName === tagName.toUpperCase()) return this; for (const child of this.children) { const found = child.findByTagName && child.findByTagName(tagName); if (found) return found; } return null; }, // Convert to HTML string toHTML() { // Special handling for template elements - return their content if (this.tagName.toLowerCase() === 'template') { if (this.innerHTML) { return this.innerHTML; } else { // Return children content return this.children.map((child) => typeof child === 'string' ? child : child.toHTML ? child.toHTML() : '').join(''); } } let html = `<${this.tagName.toLowerCase()}`; // Add attributes for (const [name, value] of this.attributes) { html += ` ${name}="${value}"`; } // Self-closing tags if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(this.tagName.toLowerCase())) { html += ' />'; return html; } html += '>'; // Add content if (this.textContent) {