UNPKG

jsx2tag

Version:

Enable JSX for Template Literal Tags based projects

166 lines (151 loc) 5.14 kB
/*! (c) Andrea Giammarchi - ISC */ /** * A value wrap/placeholder to easily find "bound" properties. * The `bind(any)` export will result into `.name=${value}` in the template. */ class Bound { /** * @param {any} _ a "protected" value to carry along for further `instanceof` checks. */ constructor(_) { this._ = _; } } const empty = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; const er = '\x01'; const re = /^on([A-Z])/; const place = (_, $) => ('@' + $.toLowerCase()); const fragment = ['', '']; /** * @typedef {object} attr - a DOM attribute facade with a `name` and a `value`. * @property {string} name - the attribute name as shown in the template literal. * @property {any} value - the attribute value to pass along to the attribute. */ /** * Return the `name` and `value` to use with an attribute. * * boolean values transform the `name` as `"?" + name`. * * bound values transform the `name` as `"." + name` * * events like `onX` transform the `name` as `"@" + name` * * strings, numbers, `null`, or `undefined` values don't change the `name` * @param {string} name the attribute name. * @param {any} value the attribute value. * @returns {attr} the attribute facade to use in the template as `name=${value}`. */ export const defaultAttribute = (name, value) => { switch (typeof value) { case 'string': case 'number': return {name, value}; case 'boolean': return {name: '?' + name, value}; case 'object': case 'undefined': if (value == null) return {name, value}; if (value instanceof Bound) return {name: '.' + name, value: value._}; } return {name: name.replace(re, place), value}; }; /** * Allow binding values directly to nodes via `name={bind(value)}`. * @param {any} value the value expected, in the template, as `.name=${value}`. * @returns */ export const bind = value => new Bound(value); /** * @typedef {object} config - optionally configure the pragma function * @property {function} [attribute=defaultAttribute] - a `callback(name, value)` to return a `{name, value}` literal. * If `null` or `undefined` is returned, the attribute is skipped all along. * @property {function} [keyed=()=>tag] - a `callback(entry, props)` to return a keyed version of the `tag`. * @property {Map<string,string[]>} [cache=new Map()] - a cache for already known/parsed templates. * @property {boolean} [xml=false] - treat nodes as XML with self-closing tags. */ /** * Return an `h` / pragma function usable with JSX transformation. * @param {function} tag a template literal tag function to invoke. * @param {config} [config={}] an optional configuration object. * @returns {function} the `h` / `React.createElement` like pragma function to use with JSX. */ export const createPragma = ( tag, { attribute = defaultAttribute, keyed = () => tag, cache = new Map, xml = false } = {} ) => function h(entry, attributes, ...children) { const component = typeof entry === 'function'; // handle fragments as tag array if (component && entry === h) return tag.call(this, fragment, children); // handle classes and component callbacks (keep µbe classes around) if (component && !('tagName' in entry)) { // pass {...props, children} to the component (attributes || (attributes = {})).children = children; return 'prototype' in entry ? new entry(attributes) : entry(attributes); } // handler µbe classes or regular HTML/SVG/XML cases const template = ['<']; const args = [template]; let isKeyed = false; let i = 0; if (component) { args.push(entry); i = template.push('') - 1; } else template[i] += entry; for (const key in attributes) { if (key.length) { if (key[0] === er) template[i] += ` ${key.slice(1)}="${attributes[key]}"`; else if (key === 'key') isKeyed = !isKeyed; else { const attr = attribute(key, attributes[key]); if (attr != null) { template[i] += ` ${attr.name}="`; args.push(attr.value); i = template.push('"') - 1; } } } } // handle children or self-closing XML tags const {length} = (children = children.flat()); template[i] += (length || !xml) ? '>' : ' />'; for (let child, j = 0; j < length; j++) { child = children[j]; if (typeof child === 'string') template[i] += child; else { args.push(child); i = template.push('') - 1; } } // handle closing tag if ( length || ( !xml && ( (component && !empty.test(component.tagName)) || !empty.test(entry) ) ) ) { if (component) { template[i] += '</'; args.push(entry); template.push('>'); } else template[i] += `</${entry}>`; } // be sure the template argument is always the same const whole = template.join(er); args[0] = cache.get(whole) || template; if (args[0] === template) cache.set(whole, template); return (isKeyed ? keyed(entry, attributes) : tag).apply(this, args); };