-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improved signature with xml, cache, and attribute
- Loading branch information
1 parent
39f52e8
commit 24f8e20
Showing
7 changed files
with
317 additions
and
160 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,97 +1,139 @@ | ||
'use strict'; | ||
/*! (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 = '<☠>'; | ||
const re = /^on([A-Z])/; | ||
const place = (_, $) => ('@' + $.toLowerCase()); | ||
|
||
/** | ||
* @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 {object} An object with `name` and `value` fields. | ||
* @returns {attr} the attribute facade to use in the template as `name=${value}`. | ||
*/ | ||
const attribute = (name, value) => { | ||
const type = typeof value; | ||
switch (type) { | ||
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 | ||
*/ | ||
const bind = value => new Bound(value); | ||
exports.bind = bind; | ||
|
||
/** | ||
* @typedef {object} config - optionally configure the pragma function | ||
* @property {function} [attribute=defaultAttribute] - a `callback(name, value)` to return a `{name, value}` literal. | ||
* @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 {Map<string,string[]>?} cache A cache to avoid passing along different arrays per same template / values. | ||
* @returns {function} The `h` / pragma function to use with JSX. | ||
* @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. | ||
*/ | ||
const createPragma = (tag, cache) => { | ||
if (!cache) | ||
cache = new Map; | ||
return function h(entry, attributes, ...children) { | ||
const component = typeof entry === 'function'; | ||
if (component && !('tagName' in entry)) { | ||
(attributes || (attributes = {})).children = children; | ||
return 'prototype' in entry ? new entry(attributes) : entry(attributes); | ||
const createPragma = ( | ||
tag, | ||
{ | ||
attribute = defaultAttribute, | ||
cache = new Map, | ||
xml = false | ||
} = {} | ||
) => function h(entry, attributes, ...children) { | ||
const component = typeof entry === 'function'; | ||
// avoid dealing with µbe classes | ||
if (component && !('tagName' in entry)) { | ||
// pass {...props, children} to the component | ||
(attributes || (attributes = {})).children = children; | ||
return 'prototype' in entry ? new entry(attributes) : entry(attributes); | ||
} | ||
const template = ['<']; | ||
const args = [template]; | ||
let i = 0; | ||
if (component) { | ||
args.push(entry); | ||
i = template.push('') - 1; | ||
} | ||
else | ||
template[i] += entry; | ||
for (const key in attributes) { | ||
const {name, value} = attribute(key, attributes[key]); | ||
template[i] += ` ${name}="`; | ||
args.push(value); | ||
i = template.push('"') - 1; | ||
} | ||
const {length} = children; | ||
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; | ||
} | ||
const template = ['<']; | ||
const args = [null]; | ||
let i = 0; | ||
} | ||
if ( | ||
length || ( | ||
!xml && ( | ||
(component && !empty.test(component.tagName)) || | ||
!empty.test(entry) | ||
) | ||
) | ||
) { | ||
if (component) { | ||
template[i] += '</'; | ||
args.push(entry); | ||
i = template.push('') - 1; | ||
template.push('>'); | ||
} | ||
else | ||
template[i] += entry; | ||
for (const key in attributes) { | ||
const {name, value} = attribute(key, attributes[key]); | ||
args.push(value); | ||
template[i] += ` ${name}="`; | ||
i = template.push('"') - 1; | ||
} | ||
template[i] += '>'; | ||
const {length} = children; | ||
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; | ||
} | ||
} | ||
if ( | ||
0 < length || | ||
(component && !empty.test(component.tagName)) || | ||
!empty.test(entry) | ||
) { | ||
if (component) { | ||
template[i] += '</'; | ||
args.push(entry); | ||
template.push('>'); | ||
} | ||
else | ||
template[i] += `</${entry}>`; | ||
} | ||
const whole = template.join(er); | ||
args[0] = cache.get(whole) || template; | ||
if (args[0] === template) | ||
cache.set(whole, template); | ||
return tag.apply(this, args); | ||
}; | ||
template[i] += `</${entry}>`; | ||
} | ||
const whole = template.join(er); | ||
args[0] = cache.get(whole) || template; | ||
if (args[0] === template) | ||
cache.set(whole, template); | ||
return tag.apply(this, args); | ||
}; | ||
exports.createPragma = createPragma; | ||
|
||
function Bound(_) { | ||
this._ = _; | ||
} |
Oops, something went wrong.