The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
blob: c6c8850be58f5acc3278e509ae9d282ebdfe170a (6446 bytes)[doc][blame][raw]
1
// @ts-check3
/**4
* @typedef {Object} ReactElement5
* @typedef {function(Props=): ReactElement} ReactFunction6
* @typedef {function(ReactFunction | string, Props?=, ...Object): ReactElement} ReactCreateFunction7
*8
* @typedef {TemplateStringsArray} Slices9
* @typedef {any} Value10
* @typedef {{[key: string]: any}} Refs11
* @typedef {any} Props12
* @typedef {{[key: string]: ReactFunction | Dict}} Dict13
* @typedef {{ dict: DictFunction, dyn: DynFunction }} Figure14
*15
* @typedef {function(Dict=): HTMLFunction} DictFunction16
* @typedef {function(Slices, ...Value): ReactElement[]} HTMLFunction17
* @typedef {ReactCreateFunction} DynFunction18
*/20
/**21
* Initializes the figure framework.22
*23
* @param {ReactCreateFunction} create The React createElement function24
* @returns {Figure} The util functions collected in an object25
*/26
export default function figure(create) {28
// the parser for interpreting HTML29
const parser = new DOMParser();30
// the counter for creating unique references31
let count = 0;33
/**34
* Returns the a function for rendering HTML.35
*36
* @param {Dict} [dict] The dictionary for resolving React components37
* @returns {HTMLFunction} The function for rendering HTML38
*/39
function dict(dict) {41
/**42
* Converts the template literal HTML syntax into React elements.43
*44
* @param {Slices} slices The template literal slices45
* @param {...Value} values The template literal values46
* @returns {ReactElement[]} The converted HTML as React elements47
*/48
function html(slices, ...values) {49
const [html, refs] = compose(slices, values);50
try {51
const dom = parser.parseFromString(html, 'text/html');52
// collect all nodes from head and body53
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];54
return nodes.map((node) => render(node, refs, dict ?? {}));55
}56
catch (error) {57
console.error(error);58
throw 'Invalid DOM structure!';59
}60
}62
return html;63
}65
/**66
* Joins the template literal slices together and replaces the values with references.67
* The values are being mapped to there corresponding references and with the populated68
* HTML string returned.69
*70
* @param {Slices} slices The template literal slices71
* @param {Value[]} values The template literal values72
* @returns {[string, Refs]} The joined HTML string and the values mapped to there references73
*/74
function compose(slices, values) {75
if (slices == null) {76
// handle dyn function without body77
return ['', {}];78
}79
const refs = /** @type {Refs} */ ({});80
let slice = '';81
for (let i = 0; i < slices.length; i++) {82
slice += slices[i];83
if (values[i] != null) {84
// create unique reference85
const uid = `$fig-${count++}`;86
refs[uid] = values[i];87
slice += uid ?? '';88
}89
}90
return [slice.trim(), refs];91
}93
/**94
* Injects the values into the corresponding reference locations of the string.95
*96
* @param {string} slice The string containing references97
* @param {Refs} refs The values mapped to there references98
* @returns {ReactElement[]} The string populated with the passed values99
*/100
function feed(slice, refs) {101
const expr = /\$fig-\d+/g;102
const elements = /** @type {ReactElement[]} */ ([]);103
let match = null;104
let last = 0;105
while ((match = expr.exec(slice)) !== null) {106
const index = match.index;107
const uid = match[0];108
const before = slice.substring(last, index);109
// ignore empty strings110
if (before.length > 0) {111
elements.push(before);112
}113
const value = refs[uid];114
// ignore empty values115
if (value !== undefined && value !== null) {116
elements.push(value);117
}118
last = index + uid.length;119
}120
const after = slice.substring(last);121
// ignore empty strings122
if (after.length > 0) {123
elements.push(after);124
}125
return elements;126
}128
/**129
* Converts a HTML node into a React element.130
*131
* @param {Node} node The HTML node132
* @param {Refs} refs The values mapped to there references133
* @param {Dict} dict The dictionary for resolving React components134
* @returns {ReactElement[]} The converted HTML node as React element135
*/136
function render(node, refs, dict) {137
if (node.nodeType === Node.TEXT_NODE) {138
const text = /** @type {Text} */ (node);139
if (text.textContent == null) {140
// ignore empty text nodes141
return [];142
}143
return feed(text.textContent, refs);144
}145
if (node.nodeType === Node.COMMENT_NODE) {146
// ignore comments147
return [];148
}149
const element = /** @type {HTMLElement} */ (node);150
const tag = element.tagName.toLowerCase();151
const props = /** @type {Props} */ ({});152
// iterate over each attribute and add it to the props153
for (const attribute of element.attributes) {154
const key = attribute.name;155
const slice = attribute.textContent;156
if (slice == null) {157
// ignore empty attribute values158
continue;159
}160
const values = feed(slice, refs);161
const value = values.length == 1 ? values[0] : values;162
let attr = key;163
const match = /^(\w+):(\w+)$/.exec(attr);164
if (match) {165
// camel case attribute name166
const [, pre, name] = match;167
attr = `${pre}${name.substring(0, 1).toUpperCase()}${name.substring(1)}`;168
}169
props[attr] = value instanceof Array ? value.join('') : value;170
}171
const children = /** @type {ReactElement[]} */ ([]);172
// recursively render all child nodes173
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs, dict)));174
const domains = tag.split(':');175
let component = /** @type {ReactFunction | null} */ (null);176
let layer = /** @type {Dict | ReactFunction | null} */ (dict);177
// look up tag name in dictionary178
for (const domain of domains) {179
if (layer && typeof layer === 'object' && layer[domain]) {180
// traverse sublayer181
layer = /** @type {Dict} */ (layer[domain]);182
continue;183
}184
// domain is not in layer185
layer = null;186
break;187
}188
if (layer) {189
// found component for tag190
component = /** @type {ReactFunction} */ (layer);191
}192
// use React component or tag name193
return [create(component ?? tag, props, ...children)];194
}196
return { dict, dyn: create };197
}