├── .gitignore ├── example ├── deku │ ├── index.html │ ├── package.json │ └── main.js ├── main_loop │ ├── index.html │ ├── package.json │ └── main.js ├── react_browser │ ├── index.html │ ├── package.json │ └── main.js ├── hyperscript.js ├── vdom.js └── react.js ├── test ├── title.js ├── multi_elem_error.js ├── style.js ├── z_hyperscript.js ├── ignore_surounding_whitespace.js ├── svg.js ├── vdom.js ├── esc.js ├── types.js ├── children.js ├── value.js ├── concat.js ├── key.js └── attr.js ├── bench ├── raw.js └── loop.js ├── package.json ├── LICENSE ├── readme.markdown └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/*/bundle.js 3 | -------------------------------------------------------------------------------- /example/deku/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/main_loop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/react_browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/deku/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "watch": "watchify main.js -o bundle.js -dv", 4 | "build": "browserify main.js > bundle.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/main_loop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "watch": "watchify main.js -o bundle.js -dv", 4 | "build": "browserify main.js > bundle.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/react_browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "watch": "watchify main.js -o bundle.js -dv", 4 | "build": "browserify main.js > bundle.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/title.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('title html tag', function (t) { 7 | var tree = hx`hello` 8 | t.equal(vdom.create(tree).toString(), 'hello') 9 | t.end() 10 | }) 11 | -------------------------------------------------------------------------------- /test/multi_elem_error.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('multiple element error', function (t) { 7 | t.plan(1) 8 | t.throws(function () { 9 | var tree = hx`
one
two
` 10 | }, 'exception') 11 | t.end() 12 | }) 13 | -------------------------------------------------------------------------------- /example/hyperscript.js: -------------------------------------------------------------------------------- 1 | var h = require('hyperscript') 2 | var hyperx = require('../') 3 | var hx = hyperx(h) 4 | 5 | var title = 'world' 6 | var wow = [1,2,3] 7 | var tree = hx`
8 |

hello ${title}!

9 | ${hx`cool`} 10 | wow 11 | ${wow.map(function (w) { 12 | return hx`${w}\n` 13 | })} 14 |
` 15 | console.log(tree.outerHTML) 16 | -------------------------------------------------------------------------------- /example/vdom.js: -------------------------------------------------------------------------------- 1 | var vdom = require('virtual-dom') 2 | var hyperx = require('../') 3 | var hx = hyperx(vdom.h) 4 | 5 | var title = 'world' 6 | var wow = [1,2,3] 7 | var tree = hx`
8 |

hello ${title}!

9 | ${hx`cool`} 10 | wow 11 | ${wow.map(function (w, i) { 12 | return hx`${w}\n` 13 | })} 14 |
` 15 | console.log(vdom.create(tree).toString()) 16 | -------------------------------------------------------------------------------- /example/react.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var toString = require('react-dom/server').renderToString 3 | var hyperx = require('../') 4 | var hx = hyperx(React.createElement) 5 | 6 | var title = 'world' 7 | var wow = [1,2,3] 8 | var tree = hx`
9 |

hello ${title}!

10 | ${hx`cool`} 11 | wow 12 | ${wow.map(function (w, i) { 13 | return hx`${w}\n` 14 | })} 15 |
` 16 | console.log(toString(tree)) 17 | -------------------------------------------------------------------------------- /test/style.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('style', function (t) { 7 | var key = 'type' 8 | var value = 'text' 9 | var tree = hx`` 12 | t.equal( 13 | vdom.create(tree).toString(), 14 | '' 15 | ) 16 | t.end() 17 | }) 18 | -------------------------------------------------------------------------------- /example/deku/main.js: -------------------------------------------------------------------------------- 1 | var deku = require('deku') 2 | var hyperx = require('../..') 3 | var hx = hyperx(deku.element) 4 | 5 | var render = deku.createApp(document.querySelector('#content')) 6 | 7 | var state = { 8 | times: 0 9 | } 10 | 11 | rerender() 12 | 13 | function rerender () { 14 | render(hx`
15 |

clicked ${state.times} times

16 | 17 |
`) 18 | 19 | function increment () { 20 | state.times += 1 21 | rerender() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/main_loop/main.js: -------------------------------------------------------------------------------- 1 | var vdom = require('virtual-dom') 2 | var hyperx = require('../../') 3 | var hx = hyperx(vdom.h) 4 | 5 | var main = require('main-loop') 6 | var loop = main({ times: 0 }, render, vdom) 7 | document.querySelector('#content').appendChild(loop.target) 8 | 9 | function render (state) { 10 | return hx`
11 |

clicked ${state.times} times

12 | 13 |
` 14 | 15 | function onclick () { 16 | loop.update({ times: state.times + 1 }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bench/raw.js: -------------------------------------------------------------------------------- 1 | var vdom = require('virtual-dom') 2 | var h = vdom.h 3 | 4 | function render (state) { 5 | return h('div', [ 6 | h('h1', { y: 'ab' + (1+2) + 'cd' }, [ 7 | 'hello ' + state.title + '!' 8 | ]), 9 | h('i', 'cool'), 10 | 'wow' 11 | ].concat(state.wow.map(function (w, i) { 12 | return h('b', {}, w) 13 | }))) 14 | } 15 | 16 | var start = Date.now() 17 | var N = 10000 18 | for (var i = 0; i < N; i++) { 19 | render({ title: 'hello' + i, wow: [1,2,3] }) 20 | } 21 | var elapsed = Date.now() - start 22 | console.log(elapsed / N, 'ms') 23 | -------------------------------------------------------------------------------- /bench/loop.js: -------------------------------------------------------------------------------- 1 | var vdom = require('virtual-dom') 2 | var hyperx = require('../') 3 | var hx = hyperx(vdom.h) 4 | 5 | function render (state) { 6 | return hx`
7 |

hello ${state.title}!

8 | cool 9 | wow 10 | ${state.wow.map(function (w, i) { 11 | return hx`${w}\n` 12 | })} 13 |
` 14 | } 15 | 16 | var start = Date.now() 17 | var N = 10000 18 | for (var i = 0; i < N; i++) { 19 | render({ title: 'hello' + i, wow: [1,2,3] }) 20 | } 21 | var elapsed = Date.now() - start 22 | console.log(elapsed / N, 'ms') 23 | -------------------------------------------------------------------------------- /example/react_browser/main.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var render = require('react-dom').render 3 | var hyperx = require('../../') 4 | var hx = hyperx(React.createElement) 5 | 6 | var App = React.createClass({ 7 | getInitialState: function () { return { n: 0 } }, 8 | render: function () { 9 | return hx`
10 |

clicked ${this.state.n} times

11 | 12 |
` 13 | }, 14 | handleClick: function () { 15 | this.setState({ n: this.state.n + 1 }) 16 | } 17 | }) 18 | render(React.createElement(App), document.querySelector('#content')) 19 | -------------------------------------------------------------------------------- /test/z_hyperscript.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var h = require('hyperscript') 3 | var hyperx = require('../') 4 | var hx = hyperx(h) 5 | 6 | var expected = `
7 |

hello world!

8 | cool 9 | wow 10 | 123 11 |
` 12 | 13 | test('hyperscript', function (t) { 14 | var title = 'world' 15 | var wow = [1,2,3] 16 | var tree = hx`
17 |

hello ${title}!

18 | ${hx`cool`} 19 | wow 20 | ${wow.map(function (w) { 21 | return hx`${w}\n` 22 | })} 23 |
` 24 | t.equal(tree.outerHTML, expected) 25 | t.end() 26 | }) 27 | -------------------------------------------------------------------------------- /test/ignore_surounding_whitespace.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('ignore whitespace surrounding an element', function (t) { 7 | var tree = hx`
`; 8 | t.equal(vdom.create(tree).toString(), '
') 9 | tree = hx` 10 |
`; 11 | t.equal(vdom.create(tree).toString(), '
') 12 | tree = hx` 13 |
14 | `; 15 | t.equal(vdom.create(tree).toString(), '
') 16 | // It shouldn't strip whitespace from a text node 17 | t.equal(hx` hello world `, ' hello world ') 18 | t.end() 19 | }) 20 | -------------------------------------------------------------------------------- /test/svg.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('svg mixed with html', function (t) { 7 | var expected = `
8 |

test

9 | 10 | 11 | 12 |
` 13 | var tree = hx`
14 |

test

15 | 16 | 17 | 18 |
` 19 | t.equal(vdom.create(tree).toString(), expected) 20 | t.end() 21 | }) 22 | -------------------------------------------------------------------------------- /test/vdom.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | var expected = `
7 |

hello world!

8 | cool 9 | wow 10 | 123 11 |
` 12 | 13 | test('vdom', function (t) { 14 | var title = 'world' 15 | var wow = [1,2,3] 16 | var tree = hx`
17 |

hello ${title}!

18 | ${hx`cool`} 19 | wow 20 | ${wow.map(function (w, i) { 21 | return hx`${w}\n` 22 | })} 23 |
` 24 | t.equal(vdom.create(tree).toString(), expected) 25 | t.end() 26 | }) 27 | -------------------------------------------------------------------------------- /test/esc.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('escape double quotes', function (t) { 7 | var value = '">' 8 | var tree = hx`` 9 | t.equal( 10 | vdom.create(tree).toString(), 11 | `` 12 | ) 13 | t.end() 14 | }) 15 | 16 | test('escape single quotes', function (t) { 17 | var value = "'>" 18 | var tree = hx`` 19 | t.equal( 20 | vdom.create(tree).toString(), 21 | `` 22 | ) 23 | t.end() 24 | }) 25 | -------------------------------------------------------------------------------- /test/types.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('undefined value (empty)', function (t) { 7 | var tree = hx`
${undefined}
` 8 | t.equal(vdom.create(tree).toString(), '
') 9 | t.end() 10 | }) 11 | 12 | test('null value (empty)', function (t) { 13 | var tree = hx`
${null}
` 14 | t.equal(vdom.create(tree).toString(), '
') 15 | t.end() 16 | }) 17 | 18 | test('boolean value', function (t) { 19 | var tree = hx`
${false}
` 20 | t.equal(vdom.create(tree).toString(), '
false
') 21 | t.end() 22 | }) 23 | 24 | test('numeric value', function (t) { 25 | var tree = hx`
${555}
` 26 | t.equal(vdom.create(tree).toString(), '
555
') 27 | t.end() 28 | }) 29 | -------------------------------------------------------------------------------- /test/children.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('1 child', function (t) { 7 | var tree = hx`
foobar
` 8 | t.equal(vdom.create(tree).toString(), '
foobar
') 9 | t.end() 10 | }) 11 | 12 | test('no children', function (t) { 13 | var tree = hx`` 14 | t.equal(vdom.create(tree).toString(), '') 15 | t.end() 16 | }) 17 | 18 | test('multiple children', function (t) { 19 | var html = `
20 |

title

21 |
22 | 27 |
28 |
` 29 | var tree = hx` 30 |
31 |

title

32 |
33 | 38 |
39 |
` 40 | t.equal(vdom.create(tree).toString(), html) 41 | t.end() 42 | }) 43 | -------------------------------------------------------------------------------- /test/value.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('value', function (t) { 7 | var key = 'type' 8 | var value = 'text' 9 | var tree = hx`` 10 | t.equal(vdom.create(tree).toString(), '') 11 | t.end() 12 | }) 13 | 14 | test('pre value', function (t) { 15 | var key = 'type' 16 | var value = 'ext' 17 | var tree = hx`` 18 | t.equal(vdom.create(tree).toString(), '') 19 | t.end() 20 | }) 21 | 22 | test('post key', function (t) { 23 | var key = 'type' 24 | var value = 'tex' 25 | var tree = hx`` 26 | t.equal(vdom.create(tree).toString(), '') 27 | t.end() 28 | }) 29 | 30 | test('pre post key', function (t) { 31 | var key = 'type' 32 | var value = 'ex' 33 | var tree = hx`` 34 | t.equal(vdom.create(tree).toString(), '') 35 | t.end() 36 | }) 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperx", 3 | "version": "2.0.4", 4 | "description": "tagged template string virtual dom builder", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test/*.js", 8 | "coverage": "covert test/*.js" 9 | }, 10 | "keywords": [ 11 | "jsx", 12 | "virtual-dom", 13 | "vdom", 14 | "react", 15 | "hyperscript", 16 | "template string", 17 | "template", 18 | "es6" 19 | ], 20 | "author": "substack", 21 | "license": "BSD", 22 | "devDependencies": { 23 | "covert": "^1.1.0", 24 | "hyperscript": "^1.4.7", 25 | "tape": "^4.4.0", 26 | "virtual-dom": "^2.1.1" 27 | }, 28 | "dependencies": { 29 | "hyperscript-attribute-to-property": "^1.0.0" 30 | }, 31 | "directories": { 32 | "example": "example", 33 | "test": "test" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/substack/hyperx.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/substack/hyperx/issues" 41 | }, 42 | "homepage": "https://github.com/substack/hyperx#readme" 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, James Halliday 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list 8 | of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /test/concat.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(function (tagName, opts, children) { 5 | return { 6 | expr: 'h(' + JSON.stringify(tagName) 7 | + ',' + JSON.stringify(opts) 8 | + ',[' + children.map(child).join(',') 9 | + '])' 10 | } 11 | function child (c) { 12 | if (c.expr) return c.expr 13 | else if (Array.isArray(c)) return c.map(child).join(',') 14 | else return JSON.stringify(c) 15 | } 16 | }, { concat: concat }) 17 | 18 | function concat (a, b) { 19 | if (!a.expr && !b.expr) return String(a) + String(b) 20 | var aexpr, bexpr 21 | if (a.expr) aexpr = '(' + a.expr + ')' 22 | else aexpr = JSON.stringify(a) 23 | if (b.expr) bexpr = '(' + b.expr + ')' 24 | else bexpr = JSON.stringify(b) 25 | return { expr: aexpr + '+' + bexpr } 26 | } 27 | 28 | var expected = `
29 |

hello world!

30 | cool 31 | wow 32 | 123 33 |
` 34 | 35 | test('vdom', function (t) { 36 | var title = 'world' 37 | var wow = [1,2,3] 38 | var str = hx`
39 |

hello ${title}!

40 | ${hx`cool`} 41 | wow 42 | ${wow.map(function (w, i) { 43 | return hx`${w}\n` 44 | })} 45 |
`.expr 46 | var tree = Function(['h'],'return ' + str)(vdom.h) 47 | t.equal(vdom.create(tree).toString(), expected) 48 | t.end() 49 | }) 50 | -------------------------------------------------------------------------------- /test/key.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('key', function (t) { 7 | var key = 'type' 8 | var value = 'text' 9 | var tree = hx`` 10 | t.equal(vdom.create(tree).toString(), '') 11 | t.end() 12 | }) 13 | 14 | test('pre key', function (t) { 15 | var key = 'ype' 16 | var value = 'text' 17 | var tree = hx`` 18 | t.equal(vdom.create(tree).toString(), '') 19 | t.end() 20 | }) 21 | 22 | test('post key', function (t) { 23 | var key = 'typ' 24 | var value = 'text' 25 | var tree = hx`` 26 | t.equal(vdom.create(tree).toString(), '') 27 | t.end() 28 | }) 29 | 30 | test('pre post key', function (t) { 31 | var key = 'yp' 32 | var value = 'text' 33 | var tree = hx`` 34 | t.equal(vdom.create(tree).toString(), '') 35 | t.end() 36 | }) 37 | 38 | test('boolean key', function (t) { 39 | var key = 'checked' 40 | var tree = hx`` 41 | t.equal(vdom.create(tree).toString(), 42 | '') 43 | t.end() 44 | }) 45 | 46 | test('multiple keys', function (t) { 47 | var props = { 48 | type: 'text', 49 | 'data-special': 'true' 50 | } 51 | var key = 'data-' 52 | var value = 'bar' 53 | var tree = hx`` 54 | t.equal(vdom.create(tree).toString(), '') 55 | t.end() 56 | }) 57 | 58 | test('multiple keys dont overwrite existing ones', function (t) { 59 | var props = { 60 | type: 'text' 61 | } 62 | var tree = hx`` 63 | t.equal(vdom.create(tree).toString(), '') 64 | t.end() 65 | }) 66 | -------------------------------------------------------------------------------- /test/attr.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var vdom = require('virtual-dom') 3 | var hyperx = require('../') 4 | var hx = hyperx(vdom.h) 5 | 6 | test('class', function (t) { 7 | var tree = hx`
` 8 | t.equal(vdom.create(tree).toString(), '
') 9 | t.end() 10 | }) 11 | 12 | test('boolean attribute', function (t) { 13 | var tree = hx`` 14 | t.equal(vdom.create(tree).toString(), '') 15 | t.end() 16 | }) 17 | 18 | test('boolean attribute followed by normal attribute', function (t) { 19 | var tree = hx`` 20 | t.equal(vdom.create(tree).toString(), '') 21 | t.end() 22 | }) 23 | 24 | test('boolean attribute preceded by normal attribute', function (t) { 25 | var tree = hx`` 26 | t.equal(vdom.create(tree).toString(), '') 27 | t.end() 28 | }) 29 | 30 | test('unquoted attribute', function (t) { 31 | var tree = hx`
` 32 | t.equal(vdom.create(tree).toString(), '
') 33 | t.end() 34 | }) 35 | 36 | test('unquoted attribute preceded by boolean attribute', function (t) { 37 | var tree = hx`` 38 | t.equal(vdom.create(tree).toString(), '') 39 | t.end() 40 | }) 41 | 42 | test('unquoted attribute succeeded by boolean attribute', function (t) { 43 | var tree = hx`` 44 | t.equal(vdom.create(tree).toString(), '') 45 | t.end() 46 | }) 47 | 48 | test('unquoted attribute preceded by normal attribute', function (t) { 49 | var tree = hx`
` 50 | t.equal(vdom.create(tree).toString(), '
') 51 | t.end() 52 | }) 53 | 54 | test('unquoted attribute succeeded by normal attribute', function (t) { 55 | var tree = hx`
` 56 | t.equal(vdom.create(tree).toString(), '
') 57 | t.end() 58 | }) 59 | 60 | test('consecutive unquoted attributes', function (t) { 61 | var tree = hx`
` 62 | t.equal(vdom.create(tree).toString(), '
') 63 | t.end() 64 | }) 65 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # hyperx 2 | 3 | tagged template string virtual dom builder 4 | 5 | This module is similar to JSX, but provided as a standards-compliant ES6 tagged 6 | template string function. 7 | 8 | hyperx works with [virtual-dom](https://npmjs.com/package/virtual-dom), 9 | [react](https://npmjs.com/package/react), 10 | [hyperscript](https://npmjs.com/package/hyperscript), or any DOM builder with a 11 | hyperscript-style API: `h(tagName, attrs, children)`. 12 | 13 | You might also want to check out the [hyperxify][2] browserify transform to 14 | statically compile hyperx into javascript expressions to save sending the hyperx 15 | parser down the wire. 16 | 17 | [2]: https://npmjs.com/package/hyperxify 18 | 19 | # compatibility 20 | 21 | [Template strings][1] are available in: 22 | node 4+, chrome 41, firefox 34, edge, opera 28, safari 9 23 | 24 | If you're targeting these platforms, there's no need to use a transpiler! 25 | 26 | [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings 27 | 28 | # examples 29 | 30 | ## virtual-dom node example 31 | 32 | ``` js 33 | var vdom = require('virtual-dom') 34 | var hyperx = require('hyperx') 35 | var hx = hyperx(vdom.h) 36 | 37 | var title = 'world' 38 | var wow = [1,2,3] 39 | var tree = hx`
40 |

hello ${title}!

41 | ${hx`cool`} 42 | wow 43 | ${wow.map(function (w, i) { 44 | return hx`${w}\n` 45 | })} 46 |
` 47 | console.log(vdom.create(tree).toString()) 48 | ``` 49 | 50 | output: 51 | 52 | ``` 53 | $ node vdom.js 54 |
55 |

hello world!

56 | cool 57 | wow 58 | 123 59 |
60 | ``` 61 | 62 | ## react node example 63 | 64 | ``` js 65 | var React = require('react') 66 | var toString = require('react-dom/server').renderToString 67 | var hyperx = require('hyperx') 68 | var hx = hyperx(React.createElement) 69 | 70 | var title = 'world' 71 | var wow = [1,2,3] 72 | var tree = hx`
73 |

hello ${title}!

74 | ${hx`cool`} 75 | wow 76 | ${wow.map(function (w, i) { 77 | return hx`${w}\n` 78 | })} 79 |
` 80 | console.log(toString(tree)) 81 | ``` 82 | 83 | ## hyperscript node example 84 | 85 | ``` js 86 | var h = require('hyperscript') 87 | var hyperx = require('hyperx') 88 | var hx = hyperx(h) 89 | 90 | var title = 'world' 91 | var wow = [1,2,3] 92 | var tree = hx`
93 |

hello ${title}!

94 | ${hx`cool`} 95 | wow 96 | ${wow.map(function (w) { 97 | return hx`${w}\n` 98 | })} 99 |
` 100 | console.log(tree.outerHTML) 101 | ``` 102 | 103 | ## virtual-dom/main-loop browser example 104 | 105 | ``` js 106 | var vdom = require('virtual-dom') 107 | var hyperx = require('hyperx') 108 | var hx = hyperx(vdom.h) 109 | 110 | var main = require('main-loop') 111 | var loop = main({ times: 0 }, render, vdom) 112 | document.querySelector('#content').appendChild(loop.target) 113 | 114 | function render (state) { 115 | return hx`
116 |

clicked ${state.times} times

117 | 118 |
` 119 | 120 | function onclick () { 121 | loop.update({ times: state.times + 1 }) 122 | } 123 | } 124 | ``` 125 | 126 | ## react browser example 127 | 128 | ``` js 129 | var React = require('react') 130 | var render = require('react-dom').render 131 | var hyperx = require('hyperx') 132 | var hx = hyperx(React.createElement) 133 | 134 | var App = React.createClass({ 135 | getInitialState: function () { return { n: 0 } }, 136 | render: function () { 137 | return hx`
138 |

clicked ${this.state.n} times

139 | 140 |
` 141 | }, 142 | handleClick: function () { 143 | this.setState({ n: this.state.n + 1 }) 144 | } 145 | }) 146 | render(React.createElement(App), document.querySelector('#content')) 147 | ``` 148 | 149 | ## console.log example 150 | 151 | ``` js 152 | var hyperx = require('hyperx') 153 | 154 | var convertTaggedTemplateOutputToDomBuilder = hyperx(function (tagName, attrs, children) { 155 | console.log(tagName, attrs, children) 156 | }) 157 | 158 | convertTaggedTemplateOutputToDomBuilder`

hello world

` 159 | 160 | // Running this produces: h1 {} [ 'hello world' ] 161 | ``` 162 | 163 | 164 | # api 165 | 166 | ``` 167 | var hyperx = require('hyperx') 168 | ``` 169 | 170 | ## var hx = hyperx(h, opts={}) 171 | 172 | Return a tagged template function `hx` from a hyperscript-style factory function 173 | `h`. 174 | 175 | Values to use for `h`: 176 | 177 | * virtual-dom - `vdom.h` 178 | * react - `React.createElement` 179 | * hyperscript - hyperscript 180 | 181 | Optionally provide: 182 | 183 | * `opts.concat(a, b)` - custom concatenation function to combine quasiliteral 184 | strings with expressions. The `h` factory function will receive the objects 185 | returned by the concatenation function and can make specific use of them. This 186 | is useful if you want to implement a pre-processor to generate javascript from 187 | hyperx syntax. 188 | 189 | # prior art 190 | 191 | * http://www.2ality.com/2014/07/jsx-template-strings.html?m=1 192 | * http://facebook.github.io/jsx/#why-not-template-literals (respectfully disagree) 193 | 194 | # license 195 | 196 | BSD 197 | 198 | # install 199 | 200 | ``` 201 | npm install hyperx 202 | ``` 203 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var attrToProp = require('hyperscript-attribute-to-property') 2 | 3 | var VAR = 0, TEXT = 1, OPEN = 2, CLOSE = 3, ATTR = 4 4 | var ATTR_KEY = 5, ATTR_KEY_W = 6 5 | var ATTR_VALUE_W = 7, ATTR_VALUE = 8 6 | var ATTR_VALUE_SQ = 9, ATTR_VALUE_DQ = 10 7 | var ATTR_EQ = 11, ATTR_BREAK = 12 8 | 9 | module.exports = function (h, opts) { 10 | h = attrToProp(h) 11 | if (!opts) opts = {} 12 | var concat = opts.concat || function (a, b) { 13 | return String(a) + String(b) 14 | } 15 | 16 | return function (strings) { 17 | var state = TEXT, reg = '' 18 | var arglen = arguments.length 19 | var parts = [] 20 | 21 | for (var i = 0; i < strings.length; i++) { 22 | if (i < arglen - 1) { 23 | var arg = arguments[i+1] 24 | var p = parse(strings[i]) 25 | var xstate = state 26 | if (xstate === ATTR_VALUE_DQ) xstate = ATTR_VALUE 27 | if (xstate === ATTR_VALUE_SQ) xstate = ATTR_VALUE 28 | if (xstate === ATTR_VALUE_W) xstate = ATTR_VALUE 29 | if (xstate === ATTR) xstate = ATTR_KEY 30 | p.push([ VAR, xstate, arg ]) 31 | parts.push.apply(parts, p) 32 | } else parts.push.apply(parts, parse(strings[i])) 33 | } 34 | 35 | var tree = [null,{},[]] 36 | var stack = [[tree,-1]] 37 | for (var i = 0; i < parts.length; i++) { 38 | var cur = stack[stack.length-1][0] 39 | var p = parts[i], s = p[0] 40 | if (s === OPEN && /^\//.test(p[1])) { 41 | var ix = stack[stack.length-1][1] 42 | if (stack.length > 1) { 43 | stack.pop() 44 | stack[stack.length-1][0][2][ix] = h( 45 | cur[0], cur[1], cur[2].length ? cur[2] : undefined 46 | ) 47 | } 48 | } else if (s === OPEN) { 49 | var c = [p[1],{},[]] 50 | cur[2].push(c) 51 | stack.push([c,cur[2].length-1]) 52 | } else if (s === ATTR_KEY || (s === VAR && p[1] === ATTR_KEY)) { 53 | var key = '' 54 | var copyKey 55 | for (; i < parts.length; i++) { 56 | if (parts[i][0] === ATTR_KEY) { 57 | key = concat(key, parts[i][1]) 58 | } else if (parts[i][0] === VAR && parts[i][1] === ATTR_KEY) { 59 | if (typeof parts[i][2] === 'object' && !key) { 60 | for (copyKey in parts[i][2]) { 61 | if (parts[i][2].hasOwnProperty(copyKey) && !cur[1][copyKey]) { 62 | cur[1][copyKey] = parts[i][2][copyKey] 63 | } 64 | } 65 | } else { 66 | key = concat(key, parts[i][2]) 67 | } 68 | } else break 69 | } 70 | if (parts[i][0] === ATTR_EQ) i++ 71 | var j = i 72 | for (; i < parts.length; i++) { 73 | if (parts[i][0] === ATTR_VALUE || parts[i][0] === ATTR_KEY) { 74 | if (!cur[1][key]) cur[1][key] = strfn(parts[i][1]) 75 | else cur[1][key] = concat(cur[1][key], parts[i][1]) 76 | } else if (parts[i][0] === VAR 77 | && (parts[i][1] === ATTR_VALUE || parts[i][1] === ATTR_KEY)) { 78 | if (!cur[1][key]) cur[1][key] = strfn(parts[i][2]) 79 | else cur[1][key] = concat(cur[1][key], parts[i][2]) 80 | } else { 81 | if (key.length && !cur[1][key] && i === j 82 | && (parts[i][0] === CLOSE || parts[i][0] === ATTR_BREAK)) { 83 | // https://html.spec.whatwg.org/multipage/infrastructure.html#boolean-attributes 84 | // empty string is falsy, not well behaved value in browser 85 | cur[1][key] = key.toLowerCase() 86 | } 87 | break 88 | } 89 | } 90 | } else if (s === ATTR_KEY) { 91 | cur[1][p[1]] = true 92 | } else if (s === VAR && p[1] === ATTR_KEY) { 93 | cur[1][p[2]] = true 94 | } else if (s === CLOSE) { 95 | if (selfClosing(cur[0]) && stack.length) { 96 | var ix = stack[stack.length-1][1] 97 | stack.pop() 98 | stack[stack.length-1][0][2][ix] = h( 99 | cur[0], cur[1], cur[2].length ? cur[2] : undefined 100 | ) 101 | } 102 | } else if (s === VAR && p[1] === TEXT) { 103 | if (p[2] === undefined || p[2] === null) p[2] = '' 104 | else if (!p[2]) p[2] = concat('', p[2]) 105 | if (Array.isArray(p[2][0])) { 106 | cur[2].push.apply(cur[2], p[2]) 107 | } else { 108 | cur[2].push(p[2]) 109 | } 110 | } else if (s === TEXT) { 111 | cur[2].push(p[1]) 112 | } else if (s === ATTR_EQ || s === ATTR_BREAK) { 113 | // no-op 114 | } else { 115 | throw new Error('unhandled: ' + s) 116 | } 117 | } 118 | 119 | if (tree[2].length > 1 && /^\s*$/.test(tree[2][0])) { 120 | tree[2].shift() 121 | } 122 | 123 | if (tree[2].length > 2 124 | || (tree[2].length === 2 && /\S/.test(tree[2][1]))) { 125 | throw new Error( 126 | 'multiple root elements must be wrapped in an enclosing tag' 127 | ) 128 | } 129 | if (Array.isArray(tree[2][0]) && typeof tree[2][0][0] === 'string' 130 | && Array.isArray(tree[2][0][2])) { 131 | tree[2][0] = h(tree[2][0][0], tree[2][0][1], tree[2][0][2]) 132 | } 133 | return tree[2][0] 134 | 135 | function parse (str) { 136 | var res = [] 137 | if (state === ATTR_VALUE_W) state = ATTR 138 | for (var i = 0; i < str.length; i++) { 139 | var c = str.charAt(i) 140 | if (state === TEXT && c === '<') { 141 | if (reg.length) res.push([TEXT, reg]) 142 | reg = '' 143 | state = OPEN 144 | } else if (c === '>' && !quot(state)) { 145 | if (state === OPEN) { 146 | res.push([OPEN,reg]) 147 | } else if (state === ATTR_KEY) { 148 | res.push([ATTR_KEY,reg]) 149 | } else if (state === ATTR_VALUE && reg.length) { 150 | res.push([ATTR_VALUE,reg]) 151 | } 152 | res.push([CLOSE]) 153 | reg = '' 154 | state = TEXT 155 | } else if (state === TEXT) { 156 | reg += c 157 | } else if (state === OPEN && /\s/.test(c)) { 158 | res.push([OPEN, reg]) 159 | reg = '' 160 | state = ATTR 161 | } else if (state === OPEN) { 162 | reg += c 163 | } else if (state === ATTR && /[\w-]/.test(c)) { 164 | state = ATTR_KEY 165 | reg = c 166 | } else if (state === ATTR && /\s/.test(c)) { 167 | if (reg.length) res.push([ATTR_KEY,reg]) 168 | res.push([ATTR_BREAK]) 169 | } else if (state === ATTR_KEY && /\s/.test(c)) { 170 | res.push([ATTR_KEY,reg]) 171 | reg = '' 172 | state = ATTR_KEY_W 173 | } else if (state === ATTR_KEY && c === '=') { 174 | res.push([ATTR_KEY,reg],[ATTR_EQ]) 175 | reg = '' 176 | state = ATTR_VALUE_W 177 | } else if (state === ATTR_KEY) { 178 | reg += c 179 | } else if ((state === ATTR_KEY_W || state === ATTR) && c === '=') { 180 | res.push([ATTR_EQ]) 181 | state = ATTR_VALUE_W 182 | } else if ((state === ATTR_KEY_W || state === ATTR) && !/\s/.test(c)) { 183 | res.push([ATTR_BREAK]) 184 | if (/[\w-]/.test(c)) { 185 | reg += c 186 | state = ATTR_KEY 187 | } else state = ATTR 188 | } else if (state === ATTR_VALUE_W && c === '"') { 189 | state = ATTR_VALUE_DQ 190 | } else if (state === ATTR_VALUE_W && c === "'") { 191 | state = ATTR_VALUE_SQ 192 | } else if (state === ATTR_VALUE_DQ && c === '"') { 193 | res.push([ATTR_VALUE,reg],[ATTR_BREAK]) 194 | reg = '' 195 | state = ATTR 196 | } else if (state === ATTR_VALUE_SQ && c === "'") { 197 | res.push([ATTR_VALUE,reg],[ATTR_BREAK]) 198 | reg = '' 199 | state = ATTR 200 | } else if (state === ATTR_VALUE_W && !/\s/.test(c)) { 201 | state = ATTR_VALUE 202 | i-- 203 | } else if (state === ATTR_VALUE && /\s/.test(c)) { 204 | res.push([ATTR_VALUE,reg],[ATTR_BREAK]) 205 | reg = '' 206 | state = ATTR 207 | } else if (state === ATTR_VALUE || state === ATTR_VALUE_SQ 208 | || state === ATTR_VALUE_DQ) { 209 | reg += c 210 | } 211 | } 212 | if (state === TEXT && reg.length) { 213 | res.push([TEXT,reg]) 214 | reg = '' 215 | } else if (state === ATTR_VALUE && reg.length) { 216 | res.push([ATTR_VALUE,reg]) 217 | reg = '' 218 | } else if (state === ATTR_VALUE_DQ && reg.length) { 219 | res.push([ATTR_VALUE,reg]) 220 | reg = '' 221 | } else if (state === ATTR_VALUE_SQ && reg.length) { 222 | res.push([ATTR_VALUE,reg]) 223 | reg = '' 224 | } else if (state === ATTR_KEY) { 225 | res.push([ATTR_KEY,reg]) 226 | reg = '' 227 | } 228 | return res 229 | } 230 | } 231 | 232 | function strfn (x) { 233 | if (typeof x === 'function') return x 234 | else if (typeof x === 'string') return x 235 | else if (x && typeof x === 'object') return x 236 | else return concat('', x) 237 | } 238 | } 239 | 240 | function quot (state) { 241 | return state === ATTR_VALUE_SQ || state === ATTR_VALUE_DQ 242 | } 243 | 244 | var hasOwn = Object.prototype.hasOwnProperty 245 | function has (obj, key) { return hasOwn.call(obj, key) } 246 | 247 | var closeRE = RegExp('^(' + [ 248 | 'area', 'base', 'basefont', 'bgsound', 'br', 'col', 'command', 'embed', 249 | 'frame', 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', 250 | 'source', 'track', 'wbr', 251 | // SVG TAGS 252 | 'animate', 'animateTransform', 'circle', 'cursor', 'desc', 'ellipse', 253 | 'feBlend', 'feColorMatrix', 'feComposite', 254 | 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 255 | 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 256 | 'feGaussianBlur', 'feImage', 'feMergeNode', 'feMorphology', 257 | 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 258 | 'feTurbulence', 'font-face-format', 'font-face-name', 'font-face-uri', 259 | 'glyph', 'glyphRef', 'hkern', 'image', 'line', 'missing-glyph', 'mpath', 260 | 'path', 'polygon', 'polyline', 'rect', 'set', 'stop', 'tref', 'use', 'view', 261 | 'vkern' 262 | ].join('|') + ')(?:[\.#][a-zA-Z0-9\u007F-\uFFFF_:-]+)*$') 263 | function selfClosing (tag) { return closeRE.test(tag) } 264 | --------------------------------------------------------------------------------