The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
blob: 5f63656007a855cb2320fa8c37909c2d952a3820 (6056 bytes)[doc][blame][raw]
1
import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values, Dict } from './types.ts';3
export default function figure({ createElement }: Options) {5
// the parser for interpreting HTML6
const parser = new DOMParser();7
// the counter for creating unique references8
let count = 0;10
/**11
* Returns the a function for rendering HTML.12
* @param {Dict} dict - The dictionary for resolving React components.13
* @return {Function} The function for rendering HTML.14
*/15
function dict(dict?: Dict): (slices: Slices, ...values: Values) => ReactElement[] {17
/**18
* Converts the template literal HTML syntax into React elements.19
* @param {Slices} slices - The template literal slices20
* @param {Values} values - The template literal values21
* @return {ReactElement[]} The converted HTML as React elements.22
*/23
function html(slices: Slices, ...values: Values): ReactElement[] {24
const [html, refs] = compose(slices, values);25
let dom;26
try {27
dom = parser.parseFromString(html, 'text/html');28
}29
catch (error) {30
console.error(error);31
throw 'Invalid DOM structure!';32
}33
// collect all nodes from head and body34
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];35
return nodes.map((node) => render(node, refs, dict ?? {}));36
}38
return html;39
}41
/**42
* Joins the template literal slices together and replaces the values with references.43
* The values are being mapped to there corresponding references and with the populated44
* HTML string returned.45
*46
* @param {Slices} slices - The template literal slices47
* @param {Values} values - The template literal values48
* @return {[string, Refs]} The joined HTML string and the values mapped to there references.49
*/50
function compose(slices: Slices, values: Values): [string, Refs] {51
if (slices == null) {52
// handle dyn function without body53
return ['', {}];54
}55
const refs: Refs = {};56
let slice = '';57
for (let i = 0; i < slices.length; i++) {58
slice += slices[i];59
if (values[i] != null) {60
const uid = `$fig-${count++}`;61
refs[uid] = values[i];62
slice += uid ?? '';63
}64
}65
return [slice.trim(), refs];66
}68
/**69
* Injects the values into the corresponding reference locations of the string.70
*71
* @param {string} slice - The string containing references72
* @param {Refs} refs - The values mapped to there references73
* @return {ReactElement[]} The string populated with the passed values74
*/75
function feed(slice: string, refs: Refs): ReactElement[] {76
const expr = /\$fig-\d+/g;77
const elements: ReactElement[] = [];78
let match = null;79
let last = 0;80
while ((match = expr.exec(slice)) !== null) {81
const index = match.index;82
const uid = match[0];83
elements.push(slice.substring(last, index));84
let value = refs[uid];85
if (value instanceof Function) {86
value = value();87
}88
elements.push(value);89
last = index + uid.length;90
}91
elements.push(slice.substring(last));92
return elements;93
}95
/**96
* Converts a HTML node into a React element.97
*98
* @param {Node} node - The HTML node99
* @param {Refs} refs - The values mapped to there references100
* @return {ReactElement[]} The converted HTML node as React element101
*/102
function render(node: Node, refs: Refs, dict: Dict): ReactElement[] {103
if (node.nodeType === Node.TEXT_NODE) {104
const text = node as Text;105
if (text.textContent == null) {106
// ignore empty text nodes107
return [];108
}109
return feed(text.textContent, refs);110
}111
if (node.nodeType === Node.COMMENT_NODE) {112
// ignore comments113
return [];114
}115
const element = node as HTMLElement;116
const tag = element.tagName.toLowerCase();117
const props: Props = {};118
// iterate over each attribute and add it to the props119
for (const attribute of element.attributes) {120
const key = attribute.name;121
const slice = attribute.textContent;122
if (slice == null) {123
// ignore empty attribute values124
continue;125
}126
let isDynamic = false;127
for (const ref in refs) {128
if (slice === ref) {129
let match = null;130
if ((match = /^on:(\w+)$/.exec(key)) !== null) {131
// add event to props132
const event = match[1];133
props[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];134
}135
else {136
// add attribute to props137
props[key] = refs[ref];138
}139
isDynamic = true;140
break;141
}142
}143
if (!isDynamic) {144
if (key === 'style') {145
// convert style attribute into object format146
const styles: Record<string, string> = {};147
for (const item of slice.split(';')) {148
if (item.trim().length === 0) {149
break;150
}151
let [key, value] = item.split(':');152
key = key.trim();153
let match = null;154
if ((match = /-(\w)/.exec(key)) !== null) {155
const char = match[1];156
key = key.replace(`-${char}`, char.toUpperCase());157
}158
value = value.trim();159
styles[key] = feed(value, refs).join('');160
}161
props[key] = styles;162
}163
else {164
props[key] = feed(slice, refs).join('');165
}166
}167
}168
const children: ReactElement[] = [];169
// recursively render all child nodes170
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs, dict)));171
const domain = tag.split(':');172
// look up tag name in dictionary173
// deno-lint-ignore no-explicit-any174
const component: ReactFunction | undefined = domain.reduce((dict: any, level) => {175
return dict?.[level];176
}, dict);177
// use React component or tag name178
return [createElement(component ?? tag, props, ...children)];179
}181
return { dict };182
}