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
JavaScript
/******************************************************************************
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';
##
<##=data.tag##
##=data.id ? 'id="' + (data.id === true ? component._id : data.id) + '"' : ''##
data-co-props="##:data.props##"
data-co-event="##:data.event##">
##if (typeof data.content === "string") {##
##=data.content##
##} else {##
##%data.content##
##}##
</##=data.tag##>`);
};
/*
* 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(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/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) {