The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
blob: d33c712231b4c1431a066a40b9d89df97b24bb70 (5611 bytes)[doc][blame][raw]
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
}