The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
blob: 208c3e449ff96af34daadf3502d9aaec2e737bb9 (8341 bytes)[doc][blame][raw]
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
const EVENTS = [21
'onCopy',22
'onCut',23
'onPaste',24
'onCompositionEnd',25
'onCompositionStart',26
'onCompositionUpdate',27
'onKeyDown',28
'onKeyPress',29
'onKeyUp',30
'onFocus',31
'onBlur',32
'onChange',33
'onInput',34
'onInvalid',35
'onReset',36
'onSubmit',37
'onError',38
'onLoad',39
'onClick',40
'onContextMenu',41
'onDoubleClick',42
'onDrag',43
'onDragEnd',44
'onDragEnter',45
'onDragExit',46
'onDragLeave',47
'onDragOver',48
'onDragStart',49
'onDrop',50
'onMouseDown',51
'onMouseEnter',52
'onMouseLeave',53
'onMouseMove',54
'onMouseOut',55
'onMouseOver',56
'onMouseUp',57
'onPointerDown',58
'onPointerMove',59
'onPointerUp',60
'onPointerCancel',61
'onGotPointerCapture',62
'onLostPointerCapture',63
'onPointerEnter',64
'onPointerLeave',65
'onPointerOver',66
'onPointerOut',67
'onSelect',68
'onTouchCancel',69
'onTouchEnd',70
'onTouchMove',71
'onTouchStart',72
'onScroll',73
'onWheel',74
'onAbort',75
'onCanPlay',76
'onCanPlayThrough',77
'onDurationChange',78
'onEmptied',79
'onEncrypted',80
'onEnded',81
'onLoadedData',82
'onLoadedMetadata',83
'onLoadStart',84
'onPause',85
'onPlay',86
'onPlaying',87
'onProgress',88
'onRateChange',89
'onSeeked',90
'onSeeking',91
'onStalled',92
'onSuspend',93
'onTimeUpdate',94
'onVolumeChange',95
'onWaiting',96
'onAnimationStart',97
'onAnimationEnd',98
'onAnimationIteration',99
'onTransitionEnd',100
'onToggle',101
];103
/**104
* Initializes the figure framework.105
*106
* @param {ReactCreateFunction} create The React createElement function107
* @returns {Figure} The util functions collected in an object108
*/109
export default function figure(create) {111
// the parser for interpreting HTML112
const parser = new DOMParser();113
// the counter for creating unique references114
let count = 0;116
/**117
* Returns the a function for rendering HTML.118
*119
* @param {Dict} [dict] The dictionary for resolving React components120
* @returns {HTMLFunction} The function for rendering HTML121
*/122
function dict(dict) {124
/**125
* Converts the template literal HTML syntax into React elements.126
*127
* @param {Slices} slices The template literal slices128
* @param {...Value} values The template literal values129
* @returns {ReactElement[]} The converted HTML as React elements130
*/131
function html(slices, ...values) {132
const [html, refs] = compose(slices, values);133
try {134
const dom = parser.parseFromString(html, 'text/html');135
// collect all nodes from head and body136
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];137
return nodes.map((node) => render(node, refs, dict ?? {}));138
}139
catch (error) {140
console.error(error);141
throw 'Invalid DOM structure!';142
}143
}145
return html;146
}148
/**149
* Joins the template literal slices together and replaces the values with references.150
* The values are being mapped to there corresponding references and with the populated151
* HTML string returned.152
*153
* @param {Slices} slices The template literal slices154
* @param {Value[]} values The template literal values155
* @returns {[string, Refs]} The joined HTML string and the values mapped to there references156
*/157
function compose(slices, values) {158
if (slices == null) {159
// handle dyn function without body160
return ['', {}];161
}162
const refs = /** @type {Refs} */ ({});163
let slice = '';164
for (let i = 0; i < slices.length; i++) {165
slice += slices[i];166
if (values[i] != null) {167
// create unique reference168
const uid = `$fig-${count++}$`;169
refs[uid] = values[i];170
slice += uid ?? '';171
}172
}173
return [slice.trim(), refs];174
}176
/**177
* Injects the values into the corresponding reference locations of the string.178
*179
* @param {string} slice The string containing references180
* @param {Refs} refs The values mapped to there references181
* @returns {ReactElement[]} The string populated with the passed values182
*/183
function feed(slice, refs) {184
const expr = /\$fig-\d+\$/g;185
const elements = /** @type {ReactElement[]} */ ([]);186
let match = null;187
let last = 0;188
while ((match = expr.exec(slice)) !== null) {189
const index = match.index;190
const uid = match[0];191
const before = slice.substring(last, index);192
// ignore empty strings193
if (before.length > 0) {194
elements.push(before);195
}196
const value = refs[uid];197
// ignore empty values198
if (value !== undefined && value !== null) {199
elements.push(value);200
}201
last = index + uid.length;202
}203
const after = slice.substring(last);204
// ignore empty strings205
if (after.length > 0) {206
elements.push(after);207
}208
return elements;209
}211
/**212
* Converts a HTML node into a React element.213
*214
* @param {Node} node The HTML node215
* @param {Refs} refs The values mapped to there references216
* @param {Dict} dict The dictionary for resolving React components217
* @returns {ReactElement[]} The converted HTML node as React element218
*/219
function render(node, refs, dict) {220
if (node.nodeType === Node.TEXT_NODE) {221
const text = /** @type {Text} */ (node);222
if (text.textContent == null) {223
// ignore empty text nodes224
return [];225
}226
return feed(text.textContent, refs);227
}228
if (node.nodeType === Node.COMMENT_NODE) {229
// ignore comments230
return [];231
}232
const element = /** @type {HTMLElement} */ (node);233
const tag = element.tagName.toLowerCase();234
const props = /** @type {Props} */ ({});235
// iterate over each attribute and add it to the props236
for (const attribute of element.attributes) {237
const key = attribute.name;238
const slice = attribute.textContent;239
if (slice == null) {240
// ignore empty attribute values241
continue;242
}243
const values = feed(slice, refs);244
const value = values.length == 1 ? values[0] : values;245
let attr = key;246
const eventMatch = /^(\w+):(\w+)$/.exec(attr);247
if (eventMatch) {248
// camel case attribute name249
const [, pre, name] = eventMatch;250
for (const event of EVENTS) {251
// find matching event name252
if (`${pre}${name}` === event.toLowerCase()) {253
attr = event;254
break;255
}256
}257
}258
const propsMatch = /^\$props$/.exec(attr);259
if (propsMatch) {260
// bind all props which are passed to $props261
for (const [key, prop] of Object.entries(value)) {262
props[key] = prop;263
}264
}265
const htmlMatch = /^\$html$/.exec(attr);266
if (htmlMatch) {267
// bind inner html268
props['dangerouslySetInnerHTML'] = { __html: value };269
}270
props[attr] = value instanceof Array ? value.join('') : value;271
}272
const children = /** @type {ReactElement[]} */ ([]);273
// recursively render all child nodes274
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs, dict)));275
const domains = tag.split(':');276
let component = /** @type {ReactFunction | null} */ (null);277
let layer = /** @type {Dict | ReactFunction | null} */ (dict);278
// look up tag name in dictionary279
for (const domain of domains) {280
if (layer && typeof layer === 'object' && layer[domain]) {281
// traverse sublayer282
layer = /** @type {Dict} */ (layer[domain]);283
continue;284
}285
// domain is not in layer286
layer = null;287
break;288
}289
if (layer) {290
// found component for tag291
component = /** @type {ReactFunction} */ (layer);292
}293
// use React component or tag name294
return [create(component ?? tag, props, ...children)];295
}297
return { dict, dyn: create };298
}