MIRROR: javascript for 馃悳's, a tiny runtime with big ambitions
1const VOID = new Set(['br', 'hr', 'img', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr']);
2
3const ESCAPE_MAP = {
4 '&': '&',
5 '<': '<',
6 '>': '>',
7 '"': '"',
8 "'": '''
9};
10
11const ATTR_MAP = {
12 className: 'class',
13 htmlFor: 'for',
14 tabIndex: 'tabindex',
15 readOnly: 'readonly',
16 maxLength: 'maxlength',
17 cellSpacing: 'cellspacing',
18 cellPadding: 'cellpadding',
19 rowSpan: 'rowspan',
20 colSpan: 'colspan',
21 encType: 'enctype',
22 contentEditable: 'contenteditable',
23 crossOrigin: 'crossorigin',
24 accessKey: 'accesskey',
25 autoComplete: 'autocomplete',
26 autoFocus: 'autofocus',
27 autoPlay: 'autoplay',
28 formAction: 'formaction',
29 noValidate: 'novalidate'
30};
31
32const UNITLESS = new Set([
33 'animationIterationCount',
34 'columns',
35 'columnCount',
36 'flex',
37 'flexGrow',
38 'flexShrink',
39 'fontWeight',
40 'gridColumn',
41 'gridRow',
42 'lineHeight',
43 'opacity',
44 'order',
45 'orphans',
46 'tabSize',
47 'widows',
48 'zIndex'
49]);
50
51function camelToKebab(str) {
52 return str.replace(/[A-Z]/g, ch => `-${ch.toLowerCase()}`);
53}
54
55function escapeHTML(str) {
56 return String(str).replace(/[&<>"']/g, ch => ESCAPE_MAP[ch]);
57}
58
59function formatValue(key, val) {
60 if (typeof val === 'number' && val !== 0 && !UNITLESS.has(key)) return `${val}px`;
61 return val;
62}
63
64function styleToString(style) {
65 return Object.entries(style)
66 .filter(([, v]) => v != null && v !== '')
67 .map(([k, v]) => `${camelToKebab(k)}:${formatValue(k, v)}`)
68 .join(';');
69}
70
71function renderAttrs(props) {
72 let result = '';
73 for (const [key, val] of Object.entries(props)) {
74 if (key === 'children' || key === 'dangerouslySetInnerHTML') continue;
75 if (key.startsWith('on') && key[2] >= 'A' && key[2] <= 'Z') continue;
76 if (val == null || val === false) continue;
77
78 if (key === 'style' && typeof val === 'object') {
79 result += ` style="${escapeHTML(styleToString(val))}"`;
80 continue;
81 }
82
83 const attr = ATTR_MAP[key] || key;
84 if (val === true) result += ` ${attr}`;
85 else result += ` ${attr}="${escapeHTML(val)}"`;
86 }
87 return result;
88}
89
90export function renderToHTML(element) {
91 if (element == null || typeof element === 'boolean') return '';
92 if (typeof element === 'string') return escapeHTML(element);
93 if (typeof element === 'number') return String(element);
94 if (Array.isArray(element)) return element.map(renderToHTML).join('');
95
96 const { type, props } = element;
97
98 if (type === Symbol.for('react.fragment') || type === undefined) {
99 return renderChildren(props?.children);
100 }
101
102 if (typeof type === 'function') {
103 if (type.prototype && type.prototype.isReactComponent) {
104 const instance = new type(props);
105 return renderToHTML(instance.render());
106 }
107 return renderToHTML(type(props));
108 }
109
110 const attrStr = renderAttrs(props || {});
111 if (VOID.has(type)) return `<${type}${attrStr}>`;
112
113 if (props?.dangerouslySetInnerHTML) {
114 return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;
115 }
116
117 return `<${type}${attrStr}>${renderChildren(props?.children)}</${type}>`;
118}
119
120function renderChildren(children) {
121 if (children == null) return '';
122 if (Array.isArray(children)) return children.map(renderToHTML).join('');
123 return renderToHTML(children);
124}