The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
1
*.js
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) {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
}
1
import {2
Dict,3
Figure,4
Props,5
ReactElement,6
HtmlFunction,7
ReactFunction,8
CreateFunction,9
Refs,10
Slices,11
Values,12
} from './types.ts';14
/**15
* Initializes the figure utility.16
* @param {CreateFunction} create - The React createElement function.17
* @return {Figure} The util functions collected in an object.18
*/19
export default function figure(create: CreateFunction): Figure {21
// the parser for interpreting HTML22
const parser = new DOMParser();23
// the counter for creating unique references24
let count = 0;26
/**27
* Returns the a function for rendering HTML.28
* @param {Dict} dict - The dictionary for resolving React components.29
* @return {Function} The function for rendering HTML.30
*/31
function dict(dict?: Dict): HtmlFunction {33
/**34
* Converts the template literal HTML syntax into React elements.35
* @param {Slices} slices - The template literal slices36
* @param {Values} values - The template literal values37
* @return {ReactElement[]} The converted HTML as React elements.38
*/39
function html(slices: Slices, ...values: Values): ReactElement[] {40
const [html, refs] = compose(slices, values);41
try {42
const dom = parser.parseFromString(html, 'text/html');43
// collect all nodes from head and body44
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];45
return nodes.map((node) => render(node, refs, dict ?? {}));46
}47
catch (error) {48
console.error(error);49
throw 'Invalid DOM structure!';50
}51
}53
return html;54
}56
/**57
* Joins the template literal slices together and replaces the values with references.58
* The values are being mapped to there corresponding references and with the populated59
* HTML string returned.60
*61
* @param {Slices} slices - The template literal slices62
* @param {Values} values - The template literal values63
* @return {[string, Refs]} The joined HTML string and the values mapped to there references.64
*/65
function compose(slices: Slices, values: Values): [string, Refs] {66
if (slices == null) {67
// handle dyn function without body68
return ['', {}];69
}70
const refs: Refs = {};71
let slice = '';72
for (let i = 0; i < slices.length; i++) {73
slice += slices[i];74
if (values[i] != null) {75
// create unique reference76
const uid = `$fig-${count++}`;77
refs[uid] = values[i];78
slice += uid ?? '';79
}80
}81
return [slice.trim(), refs];82
}84
/**85
* Injects the values into the corresponding reference locations of the string.86
*87
* @param {string} slice - The string containing references88
* @param {Refs} refs - The values mapped to there references89
* @return {ReactElement[]} The string populated with the passed values90
*/91
function feed(slice: string, refs: Refs): ReactElement[] {92
const expr = /\$fig-\d+/g;93
const elements: ReactElement[] = [];94
let match: RegExpExecArray | null = null;95
let last = 0;96
while ((match = expr.exec(slice)) !== null) {97
const index = match.index;98
const uid = match[0];99
const before = slice.substring(last, index);100
// ignore empty strings101
if (before.length > 0) {102
elements.push(before);103
}104
const value = refs[uid];105
// ignore empty values106
if (value) {107
elements.push(value);108
}109
last = index + uid.length;110
}111
const after = slice.substring(last);112
// ignore empty strings113
if (after.length > 0) {114
elements.push(after);115
}116
return elements;117
}119
/**120
* Converts a HTML node into a React element.121
*122
* @param {Node} node - The HTML node123
* @param {Refs} refs - The values mapped to there references124
* @return {ReactElement[]} The converted HTML node as React element125
*/126
function render(node: Node, refs: Refs, dict: Dict): ReactElement[] {127
if (node.nodeType === Node.TEXT_NODE) {128
const text = node as Text;129
if (text.textContent == null) {130
// ignore empty text nodes131
return [];132
}133
return feed(text.textContent, refs);134
}135
if (node.nodeType === Node.COMMENT_NODE) {136
// ignore comments137
return [];138
}139
const element = node as HTMLElement;140
const tag = element.tagName.toLowerCase();141
const props: Props = {};142
// iterate over each attribute and add it to the props143
for (const attribute of element.attributes) {144
const key = attribute.name;145
const slice = attribute.textContent;146
if (slice == null) {147
// ignore empty attribute values148
continue;149
}150
const values = feed(slice, refs);151
const value = values.length == 1 ? values[0] : values;152
let attr = key;153
const match = /^(\w+):(\w+)$/.exec(attr);154
if (match) {155
// camel case attribute name156
const [, pre, name] = match;157
attr = `${pre}${name.substring(0, 1).toUpperCase()}${name.substring(1)}`;158
}159
props[attr] = value instanceof Array ? value.join('') : value;160
}161
const children: ReactElement[] = [];162
// recursively render all child nodes163
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs, dict)));164
const domain = tag.split(':');165
// look up tag name in dictionary166
// deno-lint-ignore no-explicit-any167
const component: ReactFunction | null = domain.reduce((dict: any, level) => {168
return dict && dict[level] ? dict[level] : null;169
}, dict);170
// use React component or tag name171
return [create(component ?? tag, props, ...children)];172
}174
return { dict, dyn: create };175
}
1
export type ReactElement = unknown;2
export type ReactFunction = (props: unknown) => ReactElement;3
export type CreateFunction = (element: ReactFunction | string, props?: Props, ...children: ReactElement[]) => ReactElement;4
export type HtmlFunction = (slices: Slices, ...values: Values) => ReactElement[];5
export type Figure = { dict: (dict?: Dict) => HtmlFunction, dyn: CreateFunction };6
export type Refs = Record<string, unknown>;7
export type Props = Record<string, unknown>;8
export type Slices = TemplateStringsArray;9
export type Values = unknown[];11
export interface Dict {12
[key: string]: ReactFunction | Dict;13
}