├── .npmrc ├── cjs ├── package.json └── index.js ├── test ├── package.json ├── .babelrc ├── jsx.js ├── index.jsx └── index.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── index.d.ts ├── package.json ├── README.md └── esm └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | node_modules/ 7 | rollup/ 8 | test/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - main 9 | after_success: 10 | - "npm run coveralls" 11 | -------------------------------------------------------------------------------- /test/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@babel/plugin-transform-react-jsx", 5 | { 6 | "pragma": "h", 7 | "pragmaFrag": "h" 8 | } 9 | ], 10 | ["module:@ungap/plugin-transform-static-jsx"] 11 | ], 12 | "comments": true 13 | } 14 | -------------------------------------------------------------------------------- /test/jsx.js: -------------------------------------------------------------------------------- 1 | const {render, html, svg} = require('uhtml-ssr'); 2 | const {bind, createPragma} = require('../cjs/index.js'); 3 | 4 | module.exports = { 5 | render, html, svg, 6 | bind, 7 | jsx2: { 8 | html: createPragma(html), 9 | svg: createPragma(svg, {xml: true}) 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export function defaultAttribute(name: string, value: any): attr; 2 | export function bind(value: any): Bound; 3 | export function createPragma(tag: Function, { attribute, keyed, cache, xml }?: config): Function; 4 | /** 5 | * - a DOM attribute facade with a `name` and a `value`. 6 | */ 7 | export type attr = { 8 | /** 9 | * - the attribute name as shown in the template literal. 10 | */ 11 | name: string; 12 | /** 13 | * - the attribute value to pass along to the attribute. 14 | */ 15 | value: any; 16 | }; 17 | /** 18 | * - optionally configure the pragma function 19 | */ 20 | export type config = { 21 | /** 22 | * - a `callback(name, value)` to return a `{name, value}` literal. 23 | * If `null` or `undefined` is returned, the attribute is skipped all along. 24 | */ 25 | attribute?: Function; 26 | /** 27 | * - a `callback(entry, props)` to return a keyed version of the `tag`. 28 | */ 29 | keyed?: Function; 30 | /** 31 | * - a cache for already known/parsed templates. 32 | */ 33 | cache?: Map; 34 | /** 35 | * - treat nodes as XML with self-closing tags. 36 | */ 37 | xml?: boolean; 38 | }; 39 | /*! (c) Andrea Giammarchi - ISC */ 40 | /** 41 | * A value wrap/placeholder to easily find "bound" properties. 42 | * The `bind(any)` export will result into `.name=${value}` in the template. 43 | */ 44 | declare class Bound { 45 | /** 46 | * @param {any} _ a "protected" value to carry along for further `instanceof` checks. 47 | */ 48 | constructor(_: any); 49 | _: any; 50 | } 51 | export {}; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx2tag", 3 | "version": "0.3.1", 4 | "description": "Enable JSX for Template Literal Tags based projects", 5 | "main": "./cjs/index.js", 6 | "types": "./index.d.ts", 7 | "scripts": { 8 | "build": "npm run cjs && npm run test && npm run types", 9 | "cjs": "ascjs --no-default esm cjs", 10 | "coveralls": "c8 report --reporter=text-lcov | coveralls", 11 | "test": "cd test; npx babel index.jsx -d ./ && c8 node index.js && rm -rf ../coverage && mv coverage ../ && cd .. && c8 report --reporter=text", 12 | "types": "tsc --declaration --allowJs --emitDeclarationOnly --outDir ./ esm/index.js" 13 | }, 14 | "keywords": [ 15 | "JSX", 16 | "template", 17 | "tag", 18 | "transformer" 19 | ], 20 | "author": "Andrea Giammarchi", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@babel/cli": "^7.14.5", 24 | "@babel/core": "^7.14.6", 25 | "@babel/plugin-transform-react-jsx": "^7.14.5", 26 | "@ungap/plugin-transform-static-jsx": "^0.2.0", 27 | "ascjs": "^5.0.1", 28 | "c8": "^7.7.3", 29 | "coveralls": "^3.1.0", 30 | "typescript": "^4.3.4", 31 | "uhtml-ssr": "^0.7.1" 32 | }, 33 | "module": "./esm/index.js", 34 | "type": "module", 35 | "exports": { 36 | ".": { 37 | "import": "./esm/index.js", 38 | "default": "./cjs/index.js" 39 | }, 40 | "./package.json": "./package.json" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/WebReflection/jsx2tag.git" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/WebReflection/jsx2tag/issues" 48 | }, 49 | "homepage": "https://github.com/WebReflection/jsx2tag#readme" 50 | } 51 | -------------------------------------------------------------------------------- /test/index.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx h *//** @jsxFrag h */ 2 | 3 | const {render, html, bind, jsx2} = require('./jsx.js'); 4 | 5 | const assert = (value, expected) => { 6 | /* c8 ignore start */ 7 | if (expected !== render(String, value)) { 8 | console.error('got ', render(String, value)); 9 | console.error('expected', expected); 10 | process.exit(1); 11 | } 12 | /* c8 ignore stop */ 13 | }; 14 | 15 | let h = jsx2.html; 16 | 17 | // any component (passed as template value) 18 | const Bold = ({children}) => html`${children}`; 19 | 20 | class Span { 21 | constructor({id, children}) { 22 | return html`${children}`; 23 | } 24 | } 25 | 26 | // This is specific for ube or classes with a `tagName` 27 | // these well be used as interpolations values 28 | function World() { return World.tagName; } 29 | World.tagName = 'div'; 30 | 31 | // any generic value 32 | const test = 123; 33 | 34 | // test it! 35 | const myDocument = ( 36 |

37 | Hello, 38 | Hello 39 |

40 | ); 41 | 42 | assert( 43 | myDocument, 44 | `

Hello, Hello

` 45 | ); 46 | 47 | h = jsx2.svg; 48 | 49 | const svgDocument = ( 50 | 51 | ); 52 | 53 | assert( 54 | svgDocument, 55 | '' 56 | ); 57 | 58 | const fragment = ( 59 | <> 60 | 61 | 62 | OK 63 | 64 | ); 65 | 66 | assert( 67 | fragment, 68 | 'OK' 69 | ); 70 | 71 | console.log('Test: \x1b[1mOK\x1b[0m'); 72 | 73 | const svg = ( 74 | 75 | 76 | hello 77 | 78 | 79 | ); 80 | 81 | function MyElement({ children }) { 82 | return
{children}
; 83 | } 84 | 85 | const me = ( 86 | 87 | Some Text and some more text 88 | 89 | ); 90 | 91 | assert( 92 | me, 93 | '
Some Text and some more text
' 94 | ); 95 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | 3 | /** @jsxFrag h */ 4 | const { 5 | render, 6 | html, 7 | bind, 8 | jsx2 9 | } = require('./jsx.js'); 10 | 11 | const assert = (value, expected) => { 12 | /* c8 ignore start */ 13 | if (expected !== render(String, value)) { 14 | console.error('got ', render(String, value)); 15 | console.error('expected', expected); 16 | process.exit(1); 17 | } 18 | /* c8 ignore stop */ 19 | 20 | }; 21 | 22 | let h = jsx2.html; // any component (passed as template value) 23 | 24 | const Bold = ({ 25 | children 26 | }) => html`${children}`; 27 | 28 | class Span { 29 | constructor({ 30 | id, 31 | children 32 | }) { 33 | return html`${children}`; 34 | } 35 | 36 | } // This is specific for ube or classes with a `tagName` 37 | // these well be used as interpolations values 38 | 39 | 40 | function World() { 41 | return World.tagName; 42 | } 43 | 44 | World.tagName = 'div'; // any generic value 45 | 46 | const test = 123; // test it! 47 | 48 | const myDocument = h("p", { 49 | "\x01class": "what", 50 | nope: null, 51 | test: bind(test), 52 | onClick: console.log 53 | }, h(Bold, null, "Hello"), ", ", h("input", { 54 | "\x01type": "password", 55 | disabled: true 56 | }), h(Span, { 57 | id: "greetings" 58 | }, "Hello"), " ", h(World, null)); 59 | assert(myDocument, `

Hello, Hello

`); 60 | h = jsx2.svg; 61 | const svgDocument = h("rect", { 62 | x: 10, 63 | "\x01y": "20" 64 | }); 65 | assert(svgDocument, ''); 66 | const fragment = h(h, null, h("rect", { 67 | key: Math.random(), 68 | x: '1', 69 | "\x01y": "2" 70 | }), h("rect", { 71 | "\x01x": "3", 72 | y: 4 73 | }), "OK"); 74 | assert(fragment, 'OK'); 75 | console.log('Test: \x1b[1mOK\x1b[0m'); 76 | const svg = h("svg", null, h("g", { 77 | "\x01transform": "translate(20,20)" 78 | }, h("text", null, "hello"))); 79 | 80 | function MyElement({ 81 | children 82 | }) { 83 | return h("div", null, children); 84 | } 85 | 86 | const me = h(MyElement, null, "Some Text ", h("b", null, "and some more text")); 87 | assert(me, '
Some Text and some more text
'); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSX2TAG 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/jsx2tag.svg?branch=main)](https://travis-ci.com/WebReflection/jsx2tag) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/jsx2tag/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/jsx2tag?branch=main) 4 | 5 | **Social Media Photo by [Andre Taissin](https://unsplash.com/@andretaissin) on [Unsplash](https://unsplash.com/)** 6 | 7 | 8 | Enable JSX for Template Literal Tags based projects. 9 | 10 | * [µhtml live demo](https://codepen.io/WebReflection/pen/KKWxXYY?editors=0010) 11 | * [µland live demo](https://codepen.io/WebReflection/pen/bGqxYpZ?editors=0010) 12 | * [µbe live demo](https://codepen.io/WebReflection/pen/BaWqNpd?editors=0010) 13 | * [lit-html live demo](https://codepen.io/WebReflection/pen/abJaVzm?editors=0010) 14 | 15 | 16 | ### Features 17 | 18 | * a `createPragma(tag, config?)` utility to have a `React.createElement` like function to use as *pragma* 19 | * a `bind` utility to mimic `.prop=${value}` in the template 20 | * automatic `onEventName` to `@eventName` conversion 21 | * automatic `?prop=${value}` conversion in the template, when the property is boolean 22 | * optionally boost performance via [@ungap/plugin-transform-static-jsx](https://github.com/ungap/plugin-transform-static-jsx#readme), able to create best template literals tags' arguments 23 | 24 | 25 | ### Example 26 | 27 | See [test/index.jsx](./test/index.jsx) to see all features applied. 28 | 29 | ```js 30 | /** @jsx h *//** @jsxFrag h */ 31 | 32 | // your template literal library of choice 33 | const {render, html} = require('uhtml-ssr'); 34 | 35 | // this module utils 36 | const {bind, createPragma} = require('jsx2tag'); 37 | 38 | // create your `h` / pragma function 39 | const h = createPragma(html); 40 | // if your env works already with `React.createElement`, use: 41 | // const React = {createElement: createPragma(html)}; 42 | 43 | // any component (passed as template value) 44 | const Bold = ({children}) => html`${children}`; 45 | 46 | // any generic value 47 | const test = 123; 48 | 49 | // test it! 50 | const myDocument = ( 51 |

52 | Hello, 53 | Hello 54 |

55 | ); 56 | 57 | render(String, myDocument); 58 | //

Hello, Hello

59 | ``` 60 | 61 | ## How To Transpile JSX 62 | 63 | Specify `pragma` and `pragmaFrag` or use this syntax on top: 64 | 65 | ```js 66 | /** @jsx h */ 67 | /** @jsxFrag h */ 68 | ``` 69 | 70 | Otherwise, follow [@Robbb_J](https://twitter.com/Robbb_J) post [about minimal requirements](https://blog.r0b.io/post/using-jsx-without-react/) and you'll be good. 71 | 72 | A huge thanks to him for writing such simple, step by step, guide. 73 | 74 | ## How to render keyed components 75 | 76 | The `config` object accepts a `keyed(tagName, props)` callback that can return a keyed version of the component. 77 | 78 | ```js 79 | /** @jsx h *//** @jsxFrag h */ 80 | import {createPragma} from '//unpkg.com/jsx2tag?module'; 81 | import {render, html} from '//unpkg.com/uhtml?module'; 82 | 83 | // used as weakMap key for global keyed references 84 | const refs = {}; 85 | const h = createPragma(html, { 86 | // invoked when a key={value} is found in the node 87 | // to render regular elements (or µbe classes) 88 | keyed(tagName, {key}) { 89 | const ref = refs[tagName] || (refs[tagName] = {}); 90 | return html.for(ref, key); 91 | } 92 | }); 93 | 94 | render(document.body,
); 95 | 96 | ``` 97 | 98 | Alternatively, each library might have its own way, but the gist of this feature, whenever available, is that the `key` property is all we're after: 99 | 100 | ```js 101 | /** @jsx h *//** @jsxFrag h */ 102 | 103 | import {createPragma} from '//unpkg.com/jsx2tag?module'; 104 | import {render, html} from '//unpkg.com/uhtml?module'; 105 | 106 | const h = createPragma(html); 107 | 108 | const App = ({name, key}) => html.for(App, key)`Hello ${name} 👋`; 109 | 110 | render(document.body, ); 111 | ``` 112 | 113 | Conditional *keyed* components are also possible. 114 | 115 | Here another *uhtml* example: 116 | 117 | ```js 118 | /** @jsx h *//** @jsxFrag h */ 119 | 120 | import {createPragma} from '//unpkg.com/jsx2tag?module'; 121 | import {render, html} from '//unpkg.com/uhtml?module'; 122 | 123 | const h = createPragma(html); 124 | 125 | const App = ({name, key}) => { 126 | const tag = key ? html.for(App, key) : html; 127 | return tag`Hello ${name} 👋`; 128 | }; 129 | 130 | render(document.body, ); 131 | ``` 132 | 133 | In few words, there's literally *nothing* stopping template literal tags libraries to be *keyed* compatible. 134 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | /** 4 | * A value wrap/placeholder to easily find "bound" properties. 5 | * The `bind(any)` export will result into `.name=${value}` in the template. 6 | */ 7 | class Bound { 8 | /** 9 | * @param {any} _ a "protected" value to carry along for further `instanceof` checks. 10 | */ 11 | constructor(_) { 12 | this._ = _; 13 | } 14 | } 15 | 16 | const empty = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; 17 | const er = '\x01'; 18 | const re = /^on([A-Z])/; 19 | const place = (_, $) => ('@' + $.toLowerCase()); 20 | const fragment = ['', '']; 21 | 22 | /** 23 | * @typedef {object} attr - a DOM attribute facade with a `name` and a `value`. 24 | * @property {string} name - the attribute name as shown in the template literal. 25 | * @property {any} value - the attribute value to pass along to the attribute. 26 | */ 27 | 28 | /** 29 | * Return the `name` and `value` to use with an attribute. 30 | * * boolean values transform the `name` as `"?" + name`. 31 | * * bound values transform the `name` as `"." + name` 32 | * * events like `onX` transform the `name` as `"@" + name` 33 | * * strings, numbers, `null`, or `undefined` values don't change the `name` 34 | * @param {string} name the attribute name. 35 | * @param {any} value the attribute value. 36 | * @returns {attr} the attribute facade to use in the template as `name=${value}`. 37 | */ 38 | export const defaultAttribute = (name, value) => { 39 | switch (typeof value) { 40 | case 'string': 41 | case 'number': 42 | return {name, value}; 43 | case 'boolean': 44 | return {name: '?' + name, value}; 45 | case 'object': 46 | case 'undefined': 47 | if (value == null) 48 | return {name, value}; 49 | if (value instanceof Bound) 50 | return {name: '.' + name, value: value._}; 51 | } 52 | return {name: name.replace(re, place), value}; 53 | }; 54 | 55 | /** 56 | * Allow binding values directly to nodes via `name={bind(value)}`. 57 | * @param {any} value the value expected, in the template, as `.name=${value}`. 58 | * @returns 59 | */ 60 | export const bind = value => new Bound(value); 61 | 62 | /** 63 | * @typedef {object} config - optionally configure the pragma function 64 | * @property {function} [attribute=defaultAttribute] - a `callback(name, value)` to return a `{name, value}` literal. 65 | * If `null` or `undefined` is returned, the attribute is skipped all along. 66 | * @property {function} [keyed=()=>tag] - a `callback(entry, props)` to return a keyed version of the `tag`. 67 | * @property {Map} [cache=new Map()] - a cache for already known/parsed templates. 68 | * @property {boolean} [xml=false] - treat nodes as XML with self-closing tags. 69 | */ 70 | 71 | /** 72 | * Return an `h` / pragma function usable with JSX transformation. 73 | * @param {function} tag a template literal tag function to invoke. 74 | * @param {config} [config={}] an optional configuration object. 75 | * @returns {function} the `h` / `React.createElement` like pragma function to use with JSX. 76 | */ 77 | export const createPragma = ( 78 | tag, 79 | { 80 | attribute = defaultAttribute, 81 | keyed = () => tag, 82 | cache = new Map, 83 | xml = false 84 | } = {} 85 | ) => function h(entry, attributes, ...children) { 86 | const component = typeof entry === 'function'; 87 | 88 | // handle fragments as tag array 89 | if (component && entry === h) 90 | return tag.call(this, fragment, children); 91 | 92 | // handle classes and component callbacks (keep µbe classes around) 93 | if (component && !('tagName' in entry)) { 94 | // pass {...props, children} to the component 95 | (attributes || (attributes = {})).children = children; 96 | return 'prototype' in entry ? new entry(attributes) : entry(attributes); 97 | } 98 | 99 | // handler µbe classes or regular HTML/SVG/XML cases 100 | const template = ['<']; 101 | const args = [template]; 102 | let isKeyed = false; 103 | let i = 0; 104 | if (component) { 105 | args.push(entry); 106 | i = template.push('') - 1; 107 | } 108 | else 109 | template[i] += entry; 110 | for (const key in attributes) { 111 | if (key.length) { 112 | if (key[0] === er) 113 | template[i] += ` ${key.slice(1)}="${attributes[key]}"`; 114 | else if (key === 'key') 115 | isKeyed = !isKeyed; 116 | else { 117 | const attr = attribute(key, attributes[key]); 118 | if (attr != null) { 119 | template[i] += ` ${attr.name}="`; 120 | args.push(attr.value); 121 | i = template.push('"') - 1; 122 | } 123 | } 124 | } 125 | } 126 | 127 | // handle children or self-closing XML tags 128 | const {length} = (children = children.flat()); 129 | template[i] += (length || !xml) ? '>' : ' />'; 130 | for (let child, j = 0; j < length; j++) { 131 | child = children[j]; 132 | if (typeof child === 'string') 133 | template[i] += child; 134 | else { 135 | args.push(child); 136 | i = template.push('') - 1; 137 | } 138 | } 139 | 140 | // handle closing tag 141 | if ( 142 | length || ( 143 | !xml && ( 144 | (component && !empty.test(component.tagName)) || 145 | !empty.test(entry) 146 | ) 147 | ) 148 | ) { 149 | if (component) { 150 | template[i] += ''); 153 | } 154 | else 155 | template[i] += ``; 156 | } 157 | 158 | // be sure the template argument is always the same 159 | const whole = template.join(er); 160 | args[0] = cache.get(whole) || template; 161 | if (args[0] === template) 162 | cache.set(whole, template); 163 | 164 | return (isKeyed ? keyed(entry, attributes) : tag).apply(this, args); 165 | }; 166 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! (c) Andrea Giammarchi - ISC */ 3 | 4 | /** 5 | * A value wrap/placeholder to easily find "bound" properties. 6 | * The `bind(any)` export will result into `.name=${value}` in the template. 7 | */ 8 | class Bound { 9 | /** 10 | * @param {any} _ a "protected" value to carry along for further `instanceof` checks. 11 | */ 12 | constructor(_) { 13 | this._ = _; 14 | } 15 | } 16 | 17 | const empty = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i; 18 | const er = '\x01'; 19 | const re = /^on([A-Z])/; 20 | const place = (_, $) => ('@' + $.toLowerCase()); 21 | const fragment = ['', '']; 22 | 23 | /** 24 | * @typedef {object} attr - a DOM attribute facade with a `name` and a `value`. 25 | * @property {string} name - the attribute name as shown in the template literal. 26 | * @property {any} value - the attribute value to pass along to the attribute. 27 | */ 28 | 29 | /** 30 | * Return the `name` and `value` to use with an attribute. 31 | * * boolean values transform the `name` as `"?" + name`. 32 | * * bound values transform the `name` as `"." + name` 33 | * * events like `onX` transform the `name` as `"@" + name` 34 | * * strings, numbers, `null`, or `undefined` values don't change the `name` 35 | * @param {string} name the attribute name. 36 | * @param {any} value the attribute value. 37 | * @returns {attr} the attribute facade to use in the template as `name=${value}`. 38 | */ 39 | const defaultAttribute = (name, value) => { 40 | switch (typeof value) { 41 | case 'string': 42 | case 'number': 43 | return {name, value}; 44 | case 'boolean': 45 | return {name: '?' + name, value}; 46 | case 'object': 47 | case 'undefined': 48 | if (value == null) 49 | return {name, value}; 50 | if (value instanceof Bound) 51 | return {name: '.' + name, value: value._}; 52 | } 53 | return {name: name.replace(re, place), value}; 54 | }; 55 | exports.defaultAttribute = defaultAttribute; 56 | 57 | /** 58 | * Allow binding values directly to nodes via `name={bind(value)}`. 59 | * @param {any} value the value expected, in the template, as `.name=${value}`. 60 | * @returns 61 | */ 62 | const bind = value => new Bound(value); 63 | exports.bind = bind; 64 | 65 | /** 66 | * @typedef {object} config - optionally configure the pragma function 67 | * @property {function} [attribute=defaultAttribute] - a `callback(name, value)` to return a `{name, value}` literal. 68 | * If `null` or `undefined` is returned, the attribute is skipped all along. 69 | * @property {function} [keyed=()=>tag] - a `callback(entry, props)` to return a keyed version of the `tag`. 70 | * @property {Map} [cache=new Map()] - a cache for already known/parsed templates. 71 | * @property {boolean} [xml=false] - treat nodes as XML with self-closing tags. 72 | */ 73 | 74 | /** 75 | * Return an `h` / pragma function usable with JSX transformation. 76 | * @param {function} tag a template literal tag function to invoke. 77 | * @param {config} [config={}] an optional configuration object. 78 | * @returns {function} the `h` / `React.createElement` like pragma function to use with JSX. 79 | */ 80 | const createPragma = ( 81 | tag, 82 | { 83 | attribute = defaultAttribute, 84 | keyed = () => tag, 85 | cache = new Map, 86 | xml = false 87 | } = {} 88 | ) => function h(entry, attributes, ...children) { 89 | const component = typeof entry === 'function'; 90 | 91 | // handle fragments as tag array 92 | if (component && entry === h) 93 | return tag.call(this, fragment, children); 94 | 95 | // handle classes and component callbacks (keep µbe classes around) 96 | if (component && !('tagName' in entry)) { 97 | // pass {...props, children} to the component 98 | (attributes || (attributes = {})).children = children; 99 | return 'prototype' in entry ? new entry(attributes) : entry(attributes); 100 | } 101 | 102 | // handler µbe classes or regular HTML/SVG/XML cases 103 | const template = ['<']; 104 | const args = [template]; 105 | let isKeyed = false; 106 | let i = 0; 107 | if (component) { 108 | args.push(entry); 109 | i = template.push('') - 1; 110 | } 111 | else 112 | template[i] += entry; 113 | for (const key in attributes) { 114 | if (key.length) { 115 | if (key[0] === er) 116 | template[i] += ` ${key.slice(1)}="${attributes[key]}"`; 117 | else if (key === 'key') 118 | isKeyed = !isKeyed; 119 | else { 120 | const attr = attribute(key, attributes[key]); 121 | if (attr != null) { 122 | template[i] += ` ${attr.name}="`; 123 | args.push(attr.value); 124 | i = template.push('"') - 1; 125 | } 126 | } 127 | } 128 | } 129 | 130 | // handle children or self-closing XML tags 131 | const {length} = (children = children.flat()); 132 | template[i] += (length || !xml) ? '>' : ' />'; 133 | for (let child, j = 0; j < length; j++) { 134 | child = children[j]; 135 | if (typeof child === 'string') 136 | template[i] += child; 137 | else { 138 | args.push(child); 139 | i = template.push('') - 1; 140 | } 141 | } 142 | 143 | // handle closing tag 144 | if ( 145 | length || ( 146 | !xml && ( 147 | (component && !empty.test(component.tagName)) || 148 | !empty.test(entry) 149 | ) 150 | ) 151 | ) { 152 | if (component) { 153 | template[i] += ''); 156 | } 157 | else 158 | template[i] += ``; 159 | } 160 | 161 | // be sure the template argument is always the same 162 | const whole = template.join(er); 163 | args[0] = cache.get(whole) || template; 164 | if (args[0] === template) 165 | cache.set(whole, template); 166 | 167 | return (isKeyed ? keyed(entry, attributes) : tag).apply(this, args); 168 | }; 169 | exports.createPragma = createPragma; 170 | --------------------------------------------------------------------------------