lib.js (8341B)
1 // @ts-check 2 3 /** 4 * @typedef {Object} ReactElement 5 * @typedef {function(Props=): ReactElement} ReactFunction 6 * @typedef {function(ReactFunction | string, Props?=, ...Object): ReactElement} ReactCreateFunction 7 * 8 * @typedef {TemplateStringsArray} Slices 9 * @typedef {any} Value 10 * @typedef {{[key: string]: any}} Refs 11 * @typedef {any} Props 12 * @typedef {{[key: string]: ReactFunction | Dict}} Dict 13 * @typedef {{ dict: DictFunction, dyn: DynFunction }} Figure 14 * 15 * @typedef {function(Dict=): HTMLFunction} DictFunction 16 * @typedef {function(Slices, ...Value): ReactElement[]} HTMLFunction 17 * @typedef {ReactCreateFunction} DynFunction 18 */ 19 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 ]; 102 103 /** 104 * Initializes the figure framework. 105 * 106 * @param {ReactCreateFunction} create The React createElement function 107 * @returns {Figure} The util functions collected in an object 108 */ 109 export default function figure(create) { 110 111 // the parser for interpreting HTML 112 const parser = new DOMParser(); 113 // the counter for creating unique references 114 let count = 0; 115 116 /** 117 * Returns the a function for rendering HTML. 118 * 119 * @param {Dict} [dict] The dictionary for resolving React components 120 * @returns {HTMLFunction} The function for rendering HTML 121 */ 122 function dict(dict) { 123 124 /** 125 * Converts the template literal HTML syntax into React elements. 126 * 127 * @param {Slices} slices The template literal slices 128 * @param {...Value} values The template literal values 129 * @returns {ReactElement[]} The converted HTML as React elements 130 */ 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 body 136 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 } 144 145 return html; 146 } 147 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 populated 151 * HTML string returned. 152 * 153 * @param {Slices} slices The template literal slices 154 * @param {Value[]} values The template literal values 155 * @returns {[string, Refs]} The joined HTML string and the values mapped to there references 156 */ 157 function compose(slices, values) { 158 if (slices == null) { 159 // handle dyn function without body 160 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 reference 168 const uid = `$fig-${count++}$`; 169 refs[uid] = values[i]; 170 slice += uid ?? ''; 171 } 172 } 173 return [slice.trim(), refs]; 174 } 175 176 /** 177 * Injects the values into the corresponding reference locations of the string. 178 * 179 * @param {string} slice The string containing references 180 * @param {Refs} refs The values mapped to there references 181 * @returns {ReactElement[]} The string populated with the passed values 182 */ 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 strings 193 if (before.length > 0) { 194 elements.push(before); 195 } 196 const value = refs[uid]; 197 // ignore empty values 198 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 strings 205 if (after.length > 0) { 206 elements.push(after); 207 } 208 return elements; 209 } 210 211 /** 212 * Converts a HTML node into a React element. 213 * 214 * @param {Node} node The HTML node 215 * @param {Refs} refs The values mapped to there references 216 * @param {Dict} dict The dictionary for resolving React components 217 * @returns {ReactElement[]} The converted HTML node as React element 218 */ 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 nodes 224 return []; 225 } 226 return feed(text.textContent, refs); 227 } 228 if (node.nodeType === Node.COMMENT_NODE) { 229 // ignore comments 230 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 props 236 for (const attribute of element.attributes) { 237 const key = attribute.name; 238 const slice = attribute.textContent; 239 if (slice == null) { 240 // ignore empty attribute values 241 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 name 249 const [, pre, name] = eventMatch; 250 for (const event of EVENTS) { 251 // find matching event name 252 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 $props 261 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 html 268 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 nodes 274 (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 dictionary 279 for (const domain of domains) { 280 if (layer && typeof layer === 'object' && layer[domain]) { 281 // traverse sublayer 282 layer = /** @type {Dict} */ (layer[domain]); 283 continue; 284 } 285 // domain is not in layer 286 layer = null; 287 break; 288 } 289 if (layer) { 290 // found component for tag 291 component = /** @type {ReactFunction} */ (layer); 292 } 293 // use React component or tag name 294 return [create(component ?? tag, props, ...children)]; 295 } 296 297 return { dict, dyn: create }; 298 }