From 24f8e20d8bb0c793732a995559426b6f68d994ca Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Thu, 10 Jun 2021 18:08:37 +0200 Subject: [PATCH] Improved signature with xml, cache, and attribute --- README.md | 14 ++--- cjs/index.js | 162 ++++++++++++++++++++++++++++++------------------ esm/index.js | 164 ++++++++++++++++++++++++++++++------------------- index.d.ts | 45 ++++++++++++++ package.json | 7 ++- test/index.js | 41 ++++++++----- test/index.jsx | 44 +++++++++---- 7 files changed, 317 insertions(+), 160 deletions(-) create mode 100644 index.d.ts diff --git a/README.md b/README.md index 1f5aa42..a11252e 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,10 @@ Enable JSX for Template Literal Tags based projects. ### Features - * a `createPragma(tag, cache = new Map)` utility to have a `React.createElement` like function to use as *pragma* - * a `bind` utility to mimic `.prop=${value}` + * a `createPragma(tag, config?)` utility to have a `React.createElement` like function to use as *pragma* + * a `bind` utility to mimic `.prop=${value}` in the template * automatic `onEventName` to `@eventName` conversion - * automatic `?prop=${value}` conversion, when the property is boolean - -**TODO** - -- [ ] the pragma currently understands common `html` and `svg` template literal tag library, but it's not clear how to have both simultaneously + * automatic `?prop=${value}` conversion in the template, when the property is boolean ### Example @@ -33,9 +29,9 @@ const {render, html} = require('uhtml-ssr'); const {bind, createPragma} = require('jsx2tag'); // create your `h` / pragma function -// pass the tag, and optionally a cache (Map), -// so you can clear it when/if ever needed. const h = createPragma(html); +// if your env works already with `React.createElement`, use: +// const React = {createElement: createPragma(html)}; // any component (passed as template value) const Bold = ({children}) => html`${children}`; diff --git a/cjs/index.js b/cjs/index.js index 060522c..ed0b11b 100644 --- a/cjs/index.js +++ b/cjs/index.js @@ -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} [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?} 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] += ''); } 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] += ''); - } - else - template[i] += ``; - } - 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] += ``; + } + 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._ = _; -} diff --git a/esm/index.js b/esm/index.js index a77bde9..b4ff717 100644 --- a/esm/index.js +++ b/esm/index.js @@ -1,96 +1,136 @@ /*! (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 + */ 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. + * @property {Map} [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?} 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. */ -export const createPragma = (tag, cache) => { - if (!cache) - cache = new Map; - return function h(entry, attributes, ...children) { - const component = typeof entry === 'function'; - // avoid dealing with ube 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); +export 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] += ''); } 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] += ''); - } - else - template[i] += ``; - } - 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] += ``; + } + const whole = template.join(er); + args[0] = cache.get(whole) || template; + if (args[0] === template) + cache.set(whole, template); + return tag.apply(this, args); }; - -function Bound(_) { - this._ = _; -} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..7c4c830 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,45 @@ +export function bind(value: any): Bound; +export function createPragma(tag: Function, { attribute, cache, xml }?: config): Function; +/** + * - a DOM attribute facade with a `name` and a `value`. + */ +export type attr = { + /** + * - the attribute name as shown in the template literal. + */ + name: string; + /** + * - the attribute value to pass along to the attribute. + */ + value: any; +}; +/** + * - optionally configure the pragma function + */ +export type config = { + /** + * - a `callback(name, value)` to return a `{name, value}` literal. + */ + attribute?: Function; + /** + * - a cache for already known/parsed templates. + */ + cache?: any; + /** + * - treat nodes as XML with self-closing tags. + */ + xml?: boolean; +}; +/*! (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. + */ +declare class Bound { + /** + * @param {any} _ a "protected" value to carry along for further `instanceof` checks. + */ + constructor(_: any); + _: any; +} +export {}; diff --git a/package.json b/package.json index 357dd70..866d5ff 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "version": "0.0.4", "description": "Enable JSX for Template Literal Tags based projects", "main": "./cjs/index.js", + "types": "./index.d.ts", "scripts": { - "build": "npm run cjs && npm run test", + "build": "npm run cjs && npm run test && npm run types", "cjs": "ascjs --no-default esm cjs", "coveralls": "c8 report --reporter=text-lcov | coveralls", - "test": "cd test; npx babel index.jsx -d ./ && c8 node index.js && rm -rf ../coverage && mv coverage ../" + "test": "cd test; npx babel index.jsx -d ./ && c8 node index.js && rm -rf ../coverage && mv coverage ../", + "types": "tsc --declaration --allowJs --emitDeclarationOnly --outDir ./ esm/index.js" }, "keywords": [ "JSX", @@ -24,6 +26,7 @@ "ascjs": "^5.0.1", "c8": "^7.7.2", "coveralls": "^3.1.0", + "typescript": "^4.3.2", "uhtml-ssr": "^0.6.3" }, "module": "./esm/index.js", diff --git a/test/index.js b/test/index.js index 04a9724..b9fb4a2 100644 --- a/test/index.js +++ b/test/index.js @@ -1,17 +1,29 @@ // your template literal library of choice const { render, - html + html, + svg } = require('uhtml-ssr'); // this module const { bind, createPragma -} = require('../cjs/index.js'); // create your `h` / pragma function +} = require('../cjs/index.js'); +const assert = (value, expected) => { + /* c8 ignore start */ + if (expected !== render(String, value)) { + console.error('got ', render(String, value)); + console.error('expected', expected); + process.exit(1); + } + /* c8 ignore stop */ + +}; // create your `h` / pragma function -const h = createPragma(html); // any component (passed as template value) + +let h = createPragma(html); // any component (passed as template value) const Bold = ({ children @@ -39,6 +51,7 @@ const test = 123; // test it! const myDocument = h("p", { class: "what", + nope: null, test: bind(test), onClick: console.log }, h(Bold, null, "Hello"), ", ", h("input", { @@ -47,15 +60,13 @@ const myDocument = h("p", { }), h(Span, { id: "greetings" }, "Hello"), " ", h(World, null)); -/* c8 ignore start */ - -const expected = `

Hello, Hello

`; - -if (expected !== render(String, myDocument)) { - console.error('got ', render(String, myDocument)); - console.error('expected', expected); - process.exit(1); -} - -console.log('\x1b[1mOK\x1b[0m'); -/* c8 ignore stop */ \ No newline at end of file +assert(myDocument, `

Hello, Hello

`); +h = createPragma(svg, { + xml: true +}); +const svgDocument = h("rect", { + x: 10, + y: "20" +}); +assert(svgDocument, ''); +console.log('Test: \x1b[1mOK\x1b[0m'); \ No newline at end of file diff --git a/test/index.jsx b/test/index.jsx index 8ca9696..47d7134 100644 --- a/test/index.jsx +++ b/test/index.jsx @@ -1,11 +1,21 @@ // your template literal library of choice -const {render, html} = require('uhtml-ssr'); +const {render, html, svg} = require('uhtml-ssr'); // this module const {bind, createPragma} = require('../cjs/index.js'); +const assert = (value, expected) => { + /* c8 ignore start */ + if (expected !== render(String, value)) { + console.error('got ', render(String, value)); + console.error('expected', expected); + process.exit(1); + } + /* c8 ignore stop */ +}; + // create your `h` / pragma function -const h = createPragma(html); +let h = createPragma(html); // any component (passed as template value) const Bold = ({children}) => html`${children}`; @@ -26,18 +36,28 @@ const test = 123; // test it! const myDocument = ( -

+

Hello, Hello

); -/* c8 ignore start */ -const expected = `

Hello, Hello

`; -if (expected !== render(String, myDocument)) { - console.error('got ', render(String, myDocument)); - console.error('expected', expected); - process.exit(1); -} -console.log('\x1b[1mOK\x1b[0m'); -/* c8 ignore stop */ +assert( + myDocument, + `

Hello, Hello

` +); + +h = createPragma(svg, { + xml: true +}); + +const svgDocument = ( + +); + +assert( + svgDocument, + '' +); + +console.log('Test: \x1b[1mOK\x1b[0m');