frontend-hamroun
Version:
A lightweight frontend JavaScript framework with React-like syntax
119 lines (118 loc) • 4.07 kB
JavaScript
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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return text.replace(/[&<>"'/]/g, (match) => htmlEscapes[match]);
}
function kebabCase(str) {
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
}