├── .npmrc ├── cjs ├── package.json ├── target.js ├── token.js ├── json.js └── index.js ├── test ├── package.json ├── benchmark.js └── index.js ├── esm ├── target.js ├── token.js ├── json.js └── index.js ├── .gitignore ├── .npmignore ├── rollup ├── es.config.js └── json.config.js ├── LICENSE ├── .github └── workflows │ └── node.js.yml ├── json.js ├── package.json ├── es.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /esm/target.js: -------------------------------------------------------------------------------- 1 | export default (nmsp, key) => nmsp[key]; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /cjs/target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = (nmsp, key) => nmsp[key]; 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | .github/ 6 | coverage/ 7 | node_modules/ 8 | rollup/ 9 | test/ 10 | -------------------------------------------------------------------------------- /rollup/es.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | file: './es.js', 12 | format: 'module' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /rollup/json.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default { 5 | input: './esm/json.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | file: './json.js', 12 | format: 'module' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /esm/token.js: -------------------------------------------------------------------------------- 1 | /** (c) Andrea Giammarchi - ISC */ 2 | 3 | export default class Token { 4 | static ATTRIBUTE = 1; 5 | static COMPONENT = 2; 6 | static ELEMENT = 3; 7 | static FRAGMENT = 4; 8 | static INTERPOLATION = 5; 9 | static STATIC = 6; 10 | get properties() { 11 | const {attributes} = this; 12 | if (attributes.length) { 13 | const properties = {}; 14 | for (const entry of attributes) { 15 | if (entry.type < 2) 16 | properties[entry.name] = entry.value; 17 | else 18 | Object.assign(properties, entry.value); 19 | } 20 | return properties; 21 | } 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cjs/token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** (c) Andrea Giammarchi - ISC */ 3 | 4 | module.exports = class Token { 5 | static ATTRIBUTE = 1; 6 | static COMPONENT = 2; 7 | static ELEMENT = 3; 8 | static FRAGMENT = 4; 9 | static INTERPOLATION = 5; 10 | static STATIC = 6; 11 | get properties() { 12 | const {attributes} = this; 13 | if (attributes.length) { 14 | const properties = {}; 15 | for (const entry of attributes) { 16 | if (entry.type < 2) 17 | properties[entry.name] = entry.value; 18 | else 19 | Object.assign(properties, entry.value); 20 | } 21 | return properties; 22 | } 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2022, 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 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm ci 25 | - run: npm run build --if-present 26 | - run: npm test 27 | - run: npm run coverage --if-present 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /json.js: -------------------------------------------------------------------------------- 1 | var t=(0,Object.freeze)([]);class e{static ATTRIBUTE=1;static COMPONENT=2;static ELEMENT=3;static FRAGMENT=4;static INTERPOLATION=5;static STATIC=6;get properties(){const{attributes:t}=this;if(t.length){const e={};for(const s of t)s.type<2?e[s.name]=s.value:Object.assign(e,s.value);return e}return null}}var s=(t,e)=>t[e];function i(t,e,...s){const i=(this||JSON).parse(t,...s);return a.call({ids:i,nmsp:e||globalThis},i.pop())}function n(t,...e){const s=[],i=c.call(s,t);return s.push(i),(this||JSON).stringify(s,...e)}function a(i){const{type:n}=i;switch(n){case e.COMPONENT:i.value=i.name.split(".").reduce(s,this.nmsp);case e.ELEMENT:case e.FRAGMENT:return i.attributes=(i.attributes||t).map(a,this),i.children=(i.children||t).map(a,this),i.hasOwnProperty("id")&&(i.id=this.ids[i.id]),Object.setPrototypeOf(i,e.prototype);case e.INTERPOLATION:{const{value:t}=i;return{type:e.INTERPOLATION,value:t&&(t.i||a.call(this,t))}}}return i}function c(t){const{type:s}=t;switch(s){case e.COMPONENT:case e.ELEMENT:case e.FRAGMENT:{const i=new e;if(i.type=s,t.hasOwnProperty("id")){const e=this.indexOf(t.id);i.id=e<0?this.push(t.id)-1:e}const{attributes:n,children:a}=t;return n.length&&(i.attributes=n),a.length&&(i.children=a.map(c,this)),t.name&&(i.name=t.name),i}case e.INTERPOLATION:const{value:i}=t;return{...t,value:i instanceof e?c.call(this,i):{i:i}}}return t}export{i as parse,n as stringify}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ungap/esx", 3 | "version": "0.3.2", 4 | "description": "", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "bench": "node test/benchmark.js", 8 | "build": "npm run cjs && npm run rollup:es && npm run rollup:json && npm run test && npm run bench && npm run size", 9 | "cjs": "ascjs --no-default esm cjs", 10 | "rollup:es": "rollup --config rollup/es.config.js", 11 | "rollup:json": "rollup --config rollup/json.config.js", 12 | "test": "c8 node test/index.js && c8 report -r html", 13 | "size": "echo -e \"\\e[1mes.js\\e[0m $(cat es.js | brotli | wc -c)\"; echo -e \"\\e[1mjson.js\\e[0m $(cat json.js | brotli | wc -c)\"", 14 | "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info" 15 | }, 16 | "keywords": [ 17 | "JSX", 18 | "ESX", 19 | "template literal", 20 | "tag" 21 | ], 22 | "author": "Andrea Giammarchi", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@rollup/plugin-node-resolve": "^15.0.1", 26 | "@rollup/plugin-terser": "^0.3.0", 27 | "ascjs": "^5.0.1", 28 | "c8": "^7.12.0", 29 | "rollup": "^3.9.1" 30 | }, 31 | "module": "./esm/index.js", 32 | "type": "module", 33 | "unpkg": "es.js", 34 | "exports": { 35 | ".": { 36 | "import": "./esm/index.js", 37 | "default": "./cjs/index.js" 38 | }, 39 | "./json": { 40 | "import": "./esm/json.js", 41 | "default": "./cjs/json.js" 42 | }, 43 | "./token": { 44 | "import": "./esm/token.js", 45 | "default": "./cjs/token.js" 46 | }, 47 | "./package.json": "./package.json" 48 | }, 49 | "dependencies": { 50 | "@webreflection/empty": "^0.2.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | var t=(0,Object.freeze)([]);class e{static ATTRIBUTE=1;static COMPONENT=2;static ELEMENT=3;static FRAGMENT=4;static INTERPOLATION=5;static STATIC=6;get properties(){const{attributes:t}=this;if(t.length){const e={};for(const s of t)s.type<2?e[s.name]=s.value:Object.assign(e,s.value);return e}return null}}var s=(t,e)=>t[e];const n="\0",c=t=>t.replace(/^[\r\n]\s*|\s*[\r\n]\s*$/g,""),a=(t,e)=>({type:t,value:e}),i=(t,s,n)=>({type:e.ATTRIBUTE,dynamic:t,name:s,value:n}),r=(e,s,c,a,i)=>({"\0p":n+0,type:e,attributes:s===t?n+1:s,children:c===t?n+1:c,name:a,value:i}),l=(l,u,p)=>{const f=({index:t},s)=>{const n=c(N.slice(d,t)),{length:i}=n;if(i){let t=0,s=t;do{if(s=n.indexOf("\0",t),s<0)T(c(n.slice(t)));else if(T(c(n.slice(t,s))),h(a(e.INTERPOLATION,"\0a"+ ++A)),t=s+1,t===i)break}while(~s)}d=t+s.length},T=t=>{t&&h(a(e.STATIC,t))},h=t=>{g[0].children.push(t)},O=t=>{g&&h(t),g=[t,g||t]},E=[e.prototype,t,{}],N=l.join("\0");let g,d=0,A=0;for(const c of N.matchAll(/(<(\/)?(\S*?)>)|(<(\S+)([^>/]*?)(\/)?>)/g)){const[l,o,T,h,N,d,I,y]=c;switch(l){case"<>":f(c,l),O(r(e.FRAGMENT,t,[]));break;case"":f(c,l),g=g[1];break;default:if(f(c,l),T)g=g[1];else{let c=t;if(I&&I.trim()){c=[];for(const[t,s,n,r,l,o,u]of I.matchAll(/((\S+)=((('|")([^\5]*?)\5)|\x00)|\x00)/g))if(o)c.push(i(!1,n,u));else{const t="\0a"+ ++A;c.push(n?i(!0,n,t):a(e.INTERPOLATION,t))}}const l=d||h,o=l.split("."),f=p.has(o[0]);let T=l;if(f){const t=o.reduce(s,u),e=E.indexOf(t);T=n+(e<0?E.push(t)-1:e)}O(r(f?e.COMPONENT:e.ELEMENT,c,y?t:[],l,T)),y&&(g=g[1])}}}g.id=n+2;const I={f:Function(`return ${JSON.stringify(g).replace(/"\\u0000([ap]?)(\d*)"/g,((t,e,s)=>"p"===e?"__proto__":`${e?"arguments":"this"}[${s}]`))}`),c:E};return o.set(l,I),I},o=new WeakMap,u=(t={})=>{const e=new Set(Object.keys(t));return function(s){const{f:n,c:c}=o.get(s)||l(s,t,e);return n.apply(c,arguments)}};export{u as ESX,e as Token}; 2 | -------------------------------------------------------------------------------- /esm/json.js: -------------------------------------------------------------------------------- 1 | /** (c) Andrea Giammarchi - ISC */ 2 | 3 | import EMPTY from '@webreflection/empty/array'; 4 | import Token from './token.js'; 5 | import target from './target.js'; 6 | 7 | export function parse(esx, nmsp, ...rest) { 8 | const ids = (this || JSON).parse(esx, ...rest); 9 | return fromJSON.call({ids, nmsp: nmsp || globalThis}, ids.pop()); 10 | } 11 | 12 | export function stringify(esx, ...rest) { 13 | const ids = []; 14 | const json = toJSON.call(ids, esx); 15 | ids.push(json); 16 | return (this || JSON).stringify(ids, ...rest); 17 | } 18 | 19 | function fromJSON(esx) { 20 | const {type} = esx; 21 | switch (type) { 22 | case Token.COMPONENT: 23 | esx.value = esx.name.split('.').reduce(target, this.nmsp); 24 | case Token.ELEMENT: 25 | case Token.FRAGMENT: { 26 | esx.attributes = (esx.attributes || EMPTY).map(fromJSON, this); 27 | esx.children = (esx.children || EMPTY).map(fromJSON, this); 28 | if (esx.hasOwnProperty('id')) 29 | esx.id = this.ids[esx.id]; 30 | return Object.setPrototypeOf(esx, Token.prototype); 31 | } 32 | case Token.INTERPOLATION: { 33 | const {value: v} = esx; 34 | return {type: Token.INTERPOLATION, value: v && (v.i || fromJSON.call(this, v))}; 35 | } 36 | } 37 | return esx; 38 | }; 39 | 40 | function toJSON(esx) { 41 | const {type} = esx; 42 | switch (type) { 43 | case Token.COMPONENT: 44 | case Token.ELEMENT: 45 | case Token.FRAGMENT: { 46 | const token = new Token; 47 | token.type = type; 48 | if (esx.hasOwnProperty('id')) { 49 | const i = this.indexOf(esx.id); 50 | token.id = i < 0 ? (this.push(esx.id) - 1) : i; 51 | } 52 | const {attributes, children} = esx; 53 | if (attributes.length) 54 | token.attributes = attributes; 55 | if (children.length) 56 | token.children = children.map(toJSON, this); 57 | if (esx.name) 58 | token.name = esx.name; 59 | return token; 60 | } 61 | case Token.INTERPOLATION: 62 | const {value} = esx; 63 | return {...esx, value: value instanceof Token ? toJSON.call(this, value) : {i: value}}; 64 | } 65 | return esx; 66 | }; 67 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | 3 | const {ESX} = await import('../esm/index.js'); 4 | const {stringify, parse} = await import('../esm/json.js'); 5 | 6 | const esx = ESX(); 7 | 8 | const program = (a, b, c, d, e) => esx` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | This is some static content. 26 | This is some ${'dynamic'} content. 27 | 28 | 29 | 30 |
31 | <> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | This is some static content. 44 | This is some ${'dynamic'} content. 45 | 46 | 47 | 48 |
49 | <> 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | `; 59 | 60 | console.time('cold start'); 61 | let output = program(1, 2, 3, 4, 5); 62 | console.timeEnd('cold start'); 63 | 64 | console.time('warm update'); 65 | output = program(6, 7, 8, 9, 0); 66 | console.timeEnd('warm update'); 67 | 68 | console.time('stringify'); 69 | output = stringify(output); 70 | console.timeEnd('stringify'); 71 | 72 | console.time('parse'); 73 | output = parse(output); 74 | console.timeEnd('parse'); 75 | })(); 76 | -------------------------------------------------------------------------------- /cjs/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** (c) Andrea Giammarchi - ISC */ 3 | 4 | const EMPTY = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/empty/array')); 5 | const Token = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./token.js')); 6 | const target = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('./target.js')); 7 | 8 | function parse(esx, nmsp, ...rest) { 9 | const ids = (this || JSON).parse(esx, ...rest); 10 | return fromJSON.call({ids, nmsp: nmsp || globalThis}, ids.pop()); 11 | } 12 | exports.parse = parse 13 | 14 | function stringify(esx, ...rest) { 15 | const ids = []; 16 | const json = toJSON.call(ids, esx); 17 | ids.push(json); 18 | return (this || JSON).stringify(ids, ...rest); 19 | } 20 | exports.stringify = stringify 21 | 22 | function fromJSON(esx) { 23 | const {type} = esx; 24 | switch (type) { 25 | case Token.COMPONENT: 26 | esx.value = esx.name.split('.').reduce(target, this.nmsp); 27 | case Token.ELEMENT: 28 | case Token.FRAGMENT: { 29 | esx.attributes = (esx.attributes || EMPTY).map(fromJSON, this); 30 | esx.children = (esx.children || EMPTY).map(fromJSON, this); 31 | if (esx.hasOwnProperty('id')) 32 | esx.id = this.ids[esx.id]; 33 | return Object.setPrototypeOf(esx, Token.prototype); 34 | } 35 | case Token.INTERPOLATION: { 36 | const {value: v} = esx; 37 | return {type: Token.INTERPOLATION, value: v && (v.i || fromJSON.call(this, v))}; 38 | } 39 | } 40 | return esx; 41 | }; 42 | 43 | function toJSON(esx) { 44 | const {type} = esx; 45 | switch (type) { 46 | case Token.COMPONENT: 47 | case Token.ELEMENT: 48 | case Token.FRAGMENT: { 49 | const token = new Token; 50 | token.type = type; 51 | if (esx.hasOwnProperty('id')) { 52 | const i = this.indexOf(esx.id); 53 | token.id = i < 0 ? (this.push(esx.id) - 1) : i; 54 | } 55 | const {attributes, children} = esx; 56 | if (attributes.length) 57 | token.attributes = attributes; 58 | if (children.length) 59 | token.children = children.map(toJSON, this); 60 | if (esx.name) 61 | token.name = esx.name; 62 | return token; 63 | } 64 | case Token.INTERPOLATION: 65 | const {value} = esx; 66 | return {...esx, value: value instanceof Token ? toJSON.call(this, value) : {i: value}}; 67 | } 68 | return esx; 69 | }; 70 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const {ESX, Token} = require('../cjs/index.js'); 2 | 3 | function Component() {} 4 | Component.Nested = function () {}; 5 | 6 | const assert = (value, expected, message = `expected: ${expected} - received: ${value}`) => { 7 | if (value !== expected) 8 | throw new Error(message); 9 | }; 10 | 11 | const assertFields = (value, expected) => { 12 | for (const key of Object.keys(expected)) { 13 | assert( 14 | value.hasOwnProperty(key) || 15 | Object.getPrototypeOf(value).hasOwnProperty(key), 16 | true, 17 | `${key} should exist` 18 | ); 19 | assert(value[key], expected[key], `${key} value: expected ${expected[key]} - received: ${value[key]}`); 20 | } 21 | }; 22 | 23 | const esx = ESX({Component}); 24 | 25 | var outcome = esx`
`; 26 | const {attributes: empty} = outcome; 27 | 28 | assertFields(outcome, { 29 | type: Token.ELEMENT, 30 | attributes: empty, 31 | properties: null 32 | }); 33 | 34 | 35 | var outcome = esx`
`; 36 | assert(JSON.stringify(outcome.properties), '{"a":"1","b":"2","c":3,"d":4}'); 37 | 38 | var outcome = esx`<>a ${'b'} c`; 39 | assertFields(outcome, { 40 | type: Token.FRAGMENT, 41 | attributes: empty, 42 | properties: null 43 | }); 44 | 45 | var outcome = esx` 46 | 47 |
48 | a 49 | ${'b'} 50 | c 51 |
52 |
53 | `; 54 | assertFields(outcome, { 55 | type: Token.COMPONENT, 56 | name: 'Component', 57 | value: Component 58 | }); 59 | 60 | assert(outcome.children[0].children.length, 3); 61 | assertFields(outcome.children[0].children[0], { 62 | type: Token.STATIC, 63 | value: 'a' 64 | }); 65 | assertFields(outcome.children[0].children[1], { 66 | type: Token.INTERPOLATION, 67 | value: 'b' 68 | }); 69 | assertFields(outcome.children[0].children[2], { 70 | type: Token.STATIC, 71 | value: 'c' 72 | }); 73 | 74 | var outcome = esx` 75 | 76 |
77 | a 78 | ${'b'} 79 |
80 |
81 | `; 82 | 83 | assert(outcome.children[0].children.length, 2); 84 | assertFields(outcome.children[0].children[0], { 85 | type: Token.STATIC, 86 | value: 'a' 87 | }); 88 | assertFields(outcome.children[0].children[1], { 89 | type: Token.INTERPOLATION, 90 | value: 'b' 91 | }); 92 | 93 | var outcome = esx` 94 | a ${'b'} c 95 | `; 96 | assertFields(outcome.children[0], { 97 | type: Token.STATIC, 98 | value: 'a ' 99 | }); 100 | assertFields(outcome.children[1], { 101 | type: Token.INTERPOLATION, 102 | value: 'b' 103 | }); 104 | assertFields(outcome.children[2], { 105 | type: Token.STATIC, 106 | value: ' c' 107 | }); 108 | 109 | var outcome = esx``; 110 | assertFields(outcome, { 111 | type: Token.COMPONENT, 112 | name: 'Component.Nested', 113 | value: Component.Nested 114 | }); 115 | 116 | var outcome = esx` 117 | <> 118 | 119 | 120 | 121 | `; 122 | 123 | assertFields(outcome.children[0], { 124 | type: Token.COMPONENT, 125 | value: Component 126 | }); 127 | assertFields(outcome.children[1], { 128 | type: Token.COMPONENT, 129 | value: Component 130 | }); 131 | 132 | const button = ESX({ 133 | Button() { 134 | return arguments; 135 | } 136 | })`