NEWS: Welcome to my new homepage! <3

lib.js - figure - The vanilla alternative for writing JSX-based React applications

figure

The vanilla alternative for writing JSX-based React applications
git clone git://192.168.2.2/figure
Log | Files | Refs | README | LICENSE

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 }