The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
blob: f5c3698be5743c3c6aeed14c0e741342f675712f (5909 bytes)[doc][blame][raw]
1
import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values } 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
* Converts the template literal HTML syntax into React elements.12
* @param {Slices} slices - The template literal slices13
* @param {Values} values - The template literal values14
* @return {ReactElement[]} The converted HTML as React elements.15
*/16
function html(slices: Slices, ...values: Values): ReactElement[] {17
const [html, refs] = compose(slices, values);18
let dom;19
try {20
dom = parser.parseFromString(html, 'text/html');21
}22
catch (error) {23
console.error(error);24
throw 'Invalid DOM structure!';25
}26
// collect all nodes from head and body27
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];28
return nodes.map((node) => render(node, refs));29
}31
/**32
* Joins the template literal slices together and replaces the values with references.33
* The values are being mapped to there corresponding references and with the populated34
* HTML string returned.35
*36
* @param {Slices} slices - The template literal slices37
* @param {Values} values - The template literal values38
* @return {[string, Refs]} The joined HTML string and the values mapped to there references.39
*/40
function compose(slices: Slices, values: Values): [string, Refs] {41
if (slices == null) {42
// handle dyn function without body43
return ['', {}];44
}45
const refs: Refs = {};46
let slice = '';47
for (let i = 0; i < slices.length; i++) {48
slice += slices[i];49
if (values[i] != null) {50
const uid = `$fig-${count++}`;51
refs[uid] = values[i];52
slice += uid ?? '';53
}54
}55
return [slice.trim(), refs];56
}58
/**59
* Injects the values into the corresponding reference locations of the string.60
*61
* @param {string} slice - The string containing references62
* @param {Refs} refs - The values mapped to there references63
* @return {ReactElement[]} The string populated with the passed values64
*/65
function feed(slice: string, refs: Refs): ReactElement[] {66
const expr = /\$fig-\d+/g;67
const elements: ReactElement[] = [];68
let match = null;69
let last = 0;70
while ((match = expr.exec(slice)) !== null) {71
const index = match.index;72
const uid = match[0];73
elements.push(slice.substring(last, index));74
let value = refs[uid];75
if (value instanceof Function) {76
value = value();77
}78
elements.push(value);79
last = index + uid.length;80
}81
elements.push(slice.substring(last));82
return elements;83
}85
/**86
* Converts a HTML node into a React element.87
*88
* @param {Node} node - The HTML node89
* @param {Refs} refs - The values mapped to there references90
* @return {ReactElement[]} The converted HTML node as React element91
*/92
function render(node: Node, refs: Refs): ReactElement[] {93
if (node.nodeType === Node.TEXT_NODE) {94
const text = node as Text;95
if (text.textContent == null) {96
// ignore empty text nodes97
return [];98
}99
return feed(text.textContent, refs);100
}101
if (node.nodeType === Node.COMMENT_NODE) {102
// ignore comments103
return [];104
}105
const element = node as HTMLElement;106
const tag = element.tagName;107
const props: Props = {};108
// iterate over each attribute and add it to the props109
for (const attribute of element.attributes) {110
const key = attribute.name;111
const slice = attribute.textContent;112
if (slice == null) {113
// ignore empty attribute values114
continue;115
}116
let isDynamic = false;117
for (const ref in refs) {118
if (slice === ref) {119
let match = null;120
if ((match = /^on:(\w+)$/.exec(key)) !== null) {121
// add event to props122
const event = match[1];123
props[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];124
}125
else {126
// add attribute to props127
props[key] = refs[ref];128
}129
isDynamic = true;130
break;131
}132
}133
if (!isDynamic) {134
if (key === 'style') {135
// convert style attribute into object format136
const styles: Record<string, string> = {};137
for (const item of slice.split(';')) {138
if (item.trim().length === 0) {139
break;140
}141
let [key, value] = item.split(':');142
key = key.trim();143
let match = null;144
if ((match = /-(\w)/.exec(key)) !== null) {145
const char = match[1];146
key = key.replace(`-${char}`, char.toUpperCase());147
}148
value = value.trim();149
styles[key] = feed(value, refs).join('');150
}151
props[key] = styles;152
}153
else {154
props[key] = feed(slice, refs).join('');155
}156
}157
}158
const children: ReactElement[] = [];159
// recursively render all child nodes160
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));161
return [createElement(tag, props, ...children)];162
}164
/**165
* Creates a dynamic component with its own state.166
* Should be used if the component is statefull (contains hooks).167
*168
* @param {ReactFunction} element - The string containing references169
* @param {Props} props - The component properties170
* @param {ReactElement[]} children - The child elements171
* @return {ReactElement} The created React component172
*/173
function dyn(element: ReactFunction, props?: Props, children?: ReactElement[]): ReactElement {174
return createElement(element, props, children);175
}177
return { html, dyn };178
}