The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
blob: e64ec40990a10d1786923a1a536f61aa6f4a9840 (6592 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
* Creates a dynamic component with its own state.43
* Should be used if the component is statefull (contains hooks).44
*45
* @param {ReactFunction} element - The string containing references46
* @param {Props} props - The component properties47
* @param {ReactElement[]} children - The child elements48
* @return {ReactElement} The created React component49
*/50
function dyn(element: ReactFunction, props?: Props, children?: ReactElement[]): ReactElement {51
return createElement(element, props, children);52
}54
/**55
* Joins the template literal slices together and replaces the values with references.56
* The values are being mapped to there corresponding references and with the populated57
* HTML string returned.58
*59
* @param {Slices} slices - The template literal slices60
* @param {Values} values - The template literal values61
* @return {[string, Refs]} The joined HTML string and the values mapped to there references.62
*/63
function compose(slices: Slices, values: Values): [string, Refs] {64
if (slices == null) {65
// handle dyn function without body66
return ['', {}];67
}68
const refs: Refs = {};69
let slice = '';70
for (let i = 0; i < slices.length; i++) {71
slice += slices[i];72
if (values[i] != null) {73
const uid = `$fig-${count++}`;74
refs[uid] = values[i];75
slice += uid ?? '';76
}77
}78
return [slice.trim(), refs];79
}81
/**82
* Injects the values into the corresponding reference locations of the string.83
*84
* @param {string} slice - The string containing references85
* @param {Refs} refs - The values mapped to there references86
* @return {ReactElement[]} The string populated with the passed values87
*/88
function feed(slice: string, refs: Refs): ReactElement[] {89
const expr = /\$fig-\d+/g;90
const elements: ReactElement[] = [];91
let match = null;92
let last = 0;93
while ((match = expr.exec(slice)) !== null) {94
const index = match.index;95
const uid = match[0];96
elements.push(slice.substring(last, index));97
let value = refs[uid];98
if (value instanceof Function) {99
value = value();100
}101
elements.push(value);102
last = index + uid.length;103
}104
elements.push(slice.substring(last));105
return elements;106
}108
/**109
* Converts a HTML node into a React element.110
*111
* @param {Node} node - The HTML node112
* @param {Refs} refs - The values mapped to there references113
* @return {ReactElement[]} The converted HTML node as React element114
*/115
function render(node: Node, refs: Refs, dict: Dict): ReactElement[] {116
if (node.nodeType === Node.TEXT_NODE) {117
const text = node as Text;118
if (text.textContent == null) {119
// ignore empty text nodes120
return [];121
}122
return feed(text.textContent, refs);123
}124
if (node.nodeType === Node.COMMENT_NODE) {125
// ignore comments126
return [];127
}128
const element = node as HTMLElement;129
const tag = element.tagName.toLowerCase();130
const props: Props = {};131
// iterate over each attribute and add it to the props132
for (const attribute of element.attributes) {133
const key = attribute.name;134
const slice = attribute.textContent;135
if (slice == null) {136
// ignore empty attribute values137
continue;138
}139
let isDynamic = false;140
for (const ref in refs) {141
if (slice === ref) {142
let match = null;143
if ((match = /^on:(\w+)$/.exec(key)) !== null) {144
// add event to props145
const event = match[1];146
props[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];147
}148
else {149
// add attribute to props150
props[key] = refs[ref];151
}152
isDynamic = true;153
break;154
}155
}156
if (!isDynamic) {157
if (key === 'style') {158
// convert style attribute into object format159
const styles: Record<string, string> = {};160
for (const item of slice.split(';')) {161
if (item.trim().length === 0) {162
break;163
}164
let [key, value] = item.split(':');165
key = key.trim();166
let match = null;167
if ((match = /-(\w)/.exec(key)) !== null) {168
const char = match[1];169
key = key.replace(`-${char}`, char.toUpperCase());170
}171
value = value.trim();172
styles[key] = feed(value, refs).join('');173
}174
props[key] = styles;175
}176
else {177
props[key] = feed(slice, refs).join('');178
}179
}180
}181
const children: ReactElement[] = [];182
// recursively render all child nodes183
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs, dict)));184
const domain = tag.split(':');185
// look up tag name in dictionary186
// deno-lint-ignore no-explicit-any187
const component: ReactFunction | undefined = domain.reduce((dict: any, level) => {188
return dict?.[level];189
}, dict);190
// use React component or tag name191
return [createElement(component ?? tag, props, ...children)];192
}194
return { dict, dyn };195
}