UNPKG

frontend-hamroun

Version:

A lightweight frontend JavaScript framework with React-like syntax

119 lines (118 loc) 4.07 kB
import { prepareRender, finishRender } from './hooks.js'; export async function renderToString(element) { const renderId = prepareRender(element); // Pass the element to establish render context try { const html = await renderNodeToString(element); return html; } finally { finishRender(); } } async function renderNodeToString(node) { // Handle null, undefined, boolean if (node == null || typeof node === 'boolean') { return ''; } // Handle primitives if (typeof node === 'string' || typeof node === 'number') { return escapeHtml(String(node)); } // Handle arrays if (Array.isArray(node)) { const results = await Promise.all(node.map(child => renderNodeToString(child))); return results.join(''); } // Handle objects with type and props (React-like elements) if (node && typeof node === 'object' && 'type' in node) { const { type, props = {} } = node; // Handle function components if (typeof type === 'function') { try { // Set up a new render context for this component const componentRenderId = prepareRender(type); try { const result = await type(props); return await renderNodeToString(result); } finally { finishRender(); } } catch (error) { console.error('Error rendering component:', error); return `<!-- Error rendering component: ${error.message} -->`; } } // Handle DOM elements if (typeof type === 'string') { return await renderDOMElement(type, props); } } // Fallback for other objects if (typeof node === 'object') { return escapeHtml(JSON.stringify(node)); } return escapeHtml(String(node)); } async function renderDOMElement(tagName, props) { const { children, ...attrs } = props; // Self-closing tags const voidElements = new Set([ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' ]); // Build attributes string const attributeString = Object.entries(attrs) .filter(([key, value]) => { // Filter out React-specific props and event handlers if (key.startsWith('on') || key === 'key' || key === 'ref') return false; if (value == null || value === false) return false; return true; }) .map(([key, value]) => { // Handle className -> class if (key === 'className') key = 'class'; // Handle boolean attributes if (value === true) return key; // Handle style objects if (key === 'style' && typeof value === 'object' && value !== null) { const styleString = Object.entries(value) .map(([prop, val]) => `${kebabCase(prop)}:${val}`) .join(';'); return `style="${escapeHtml(styleString)}"`; } return `${key}="${escapeHtml(String(value))}"`; }) .join(' '); const openTag = `<${tagName}${attributeString ? ' ' + attributeString : ''}>`; // Self-closing elements if (voidElements.has(tagName)) { return openTag.slice(0, -1) + '/>'; } // Elements with children const closeTag = `</${tagName}>`; if (children != null) { const childrenString = await renderNodeToString(children); return openTag + childrenString + closeTag; } return openTag + closeTag; } function escapeHtml(text) { const htmlEscapes = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;' }; return text.replace(/[&<>"'/]/g, (match) => htmlEscapes[match]); } function kebabCase(str) { return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`); }