export type Attributes = Omit< Partial, 'style' > & { style?: string; }; export const jsxAsString = ( tag: T, props: Attributes | null, ...children: (string | HTMLElement)[] ): string => { const attributes = props ? Object.entries(props) .map(([key, value]) => { if (value === true) return key; if (value === false || value === null || value === undefined) return ''; // Convert camelCase to kebab-case for attributes const attrName = key.replace(/([A-Z])/g, '-$1').toLowerCase(); return `${attrName}="${String(value).replace(/"/g, '"')}"`; }) .filter(Boolean) .join(' ') : ''; const attributesString = attributes ? ` ${attributes}` : ''; const childrenString = children.join(''); // Self-closing tags const selfClosing = [ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr', ]; if (selfClosing.includes(tag)) { return `<${tag}${attributesString} />`; } return `<${tag}${attributesString}>${childrenString}`; }; export const jsxAsElement = ( tag: T, props: Attributes | null, ...children: (string | HTMLElement)[] ) => { const element = document.createElement(tag); Object.entries(props || {}).forEach(([key, value]) => { const castKey = key as keyof HTMLElementTagNameMap[T]; if (typeof castKey === 'string' && castKey.includes('data-')) { element.dataset[castKey.replace('data-', '')] = value as string; } else if ( typeof castKey === 'string' && ![ 'className', 'style', 'onload', 'onerror', 'onclick', 'onchange', ].includes(castKey) ) { // Use setAttributeNS for custom elements to preserve unknown attributes (and maintain casing) element.setAttributeNS(null, key, value as string); } else { // Since we're replacing the style attribute this complains heaps // @ts-ignore element[castKey] = value; } }); if (Array.isArray(children)) { children.forEach((child) => { element.append(child); }); } else { element.append(children); } return element; };