├── .gitignore ├── README.md ├── bin ├── build └── cmd.js ├── docs └── readme.js ├── examples ├── hyperapp │ ├── demo │ │ ├── index.html │ │ ├── view.html │ │ └── view.js │ └── todo │ │ ├── index.html │ │ ├── view.html │ │ └── view.js └── preact │ ├── demo │ ├── component.html │ ├── component.js │ ├── index.html │ ├── view.html │ └── view.js │ └── todo │ ├── index.html │ ├── todos.html │ └── todos.js ├── index.js ├── package-lock.json ├── package.json └── test ├── basics.js ├── cli.js ├── components.js ├── conditionals.js ├── events.js ├── function.js ├── index.js ├── interpolation.js ├── iteration.js └── literal.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperviews 2 | 3 | `hyperviews` is a template language that transforms to hyperscript. 4 | 5 | Use it as a build tool with any `h(tag, props, children)` compliant framework e.g. React, preact or hyperapp. 6 | 7 | ```js 8 | const hv = require('hyperviews') 9 | 10 | hv("
{state.name}
") 11 | // => h('div', { id: 'foo' }, (state.name)) 12 | ``` 13 | 14 | ### Installation 15 | 16 | `npm i hyperviews` 17 | 18 | ### API 19 | 20 | `hyperviews(tmpl, mode, name, argstr)` 21 | 22 | - `tmpl` (required) - The template string. 23 | - `mode` - The output format. Can be one of [`raw`, `esm`, `cjs`, `browser`], or if any other value is passed the function is exported as a variable with that name. The default is `raw`. 24 | - `name` - The default output function name. The default is `view`. 25 | - `args` - The default function arguments. The default is `props state`. 26 | 27 | 28 | ### CLI 29 | 30 | Reads the template from stdin, 31 | 32 | `cat examples/test.html | hyperviews --mode esm --name foo --args bar > examples/test.js` 33 | 34 | See [more CLI examples](./test/cli.js) 35 | 36 | 37 | 38 | ## Template language 39 | 40 | 41 | ### Interpolation 42 | 43 | Use curly braces in attributes and text. 44 | 45 | ```html 46 |
47 | 48 | My name is {state.name} my age is {state.age} and I live at {state.address} 49 |
50 | ``` 51 | 52 | See [more interpolation examples](./test/interpolation.js) 53 | 54 | 55 | 56 | ### Conditionals 57 | 58 | There are two forms of conditional. 59 | 60 | Using an `if` attribute. 61 | 62 | ```html 63 | Show Me! 64 | ``` 65 | 66 | Or using tags ``, `` and `` 67 | 68 | ```html 69 |
70 | 71 | 1 72 | 73 | 2 74 | 75 | bar is neither 1 or 2, it's {state.bar}! 76 | 77 |
78 | ``` 79 | 80 | `if` tags can be [nested](./test/conditionals.js#L84). 81 | 82 | See [more conditional examples](./test/conditionals.js) 83 | 84 | 85 | 86 | ### Iteration 87 | 88 | The `each` attribute can be used to repeat over items in an Array. 89 | Three additional variables are available during each iteration: `$value`, `$index` and `$target`. 90 | 91 | It supports keyed elements as shown here. 92 | 93 | ```html 94 |
    95 |
  • 96 | {post.title} {$index} 97 |
  • 98 |
99 | ``` 100 | 101 | produces 102 | 103 | ```js 104 | h('ul', {}, (state.posts || []).map(function ($value, $index, $target) { 105 | const post = $value 106 | return h('li', { key: (post.id) }, h('span', {}, (post.title) + ' ' + ($index))) 107 | }, this)) 108 | ``` 109 | 110 | See [more iteration examples](./test/iteration.js) 111 | 112 | 113 | 114 | ### Events 115 | 116 | ```html 117 | {state.foo} 118 | ``` 119 | 120 | produces this output 121 | 122 | 123 | ```js 124 | h('a', { href: 'http://example.com', onclick: this.onClick, (state.foo)) 125 | ``` 126 | 127 | See [more event examples](./test/events.js) 128 | 129 | ### Style 130 | 131 | The `style` attribute expects an object 132 | 133 | ```html 134 |

135 | ``` 136 | 137 | produces this output 138 | 139 | ```js 140 | h('p', { style: { color: state.color, fontSize: '12px' } }) 141 | ``` 142 | 143 | 144 | 145 | ### Literal 146 | 147 | The `script` tag literally outputs it's contents. 148 | 149 | ```html 150 | 154 | ``` 155 | 156 | This is also useful for recursive nodes, e.g. a tree 157 | 158 | ```html 159 | 160 |
161 | {state.name} 162 |
    163 |
  • 164 | 165 |
  • 166 |
167 |
168 | 169 | {state.name} 170 |
171 | ``` 172 | 173 | produces this output 174 | 175 | ```js 176 | function view (props, state) { 177 | return (function () { 178 | if (state.children) { 179 | return h('div', {}, [ 180 | h('a', { href: '#' + (state.path) }, (state.name)), 181 | h('ul', {}, (state.children || []).map(function ($value, $index, $target) { 182 | var child = $value 183 | return h('li', {}, view(props, child)) 184 | })) 185 | ]) 186 | } else { 187 | return h('a', { href: '#' + (state.path) }, (state.name)) 188 | } 189 | })() 190 | } 191 | ``` 192 | 193 | See [more literal examples](./test/literal.js) 194 | 195 | 196 | 197 | ### Function 198 | 199 | The `function` tag outputs a function, returning it's contents. 200 | Supports `name` and `args` attributes. 201 | 202 | ```html 203 | 204 |
{x}
205 | 206 | ``` 207 | 208 | produces this output 209 | 210 | ```js 211 | function MyComponent (x, y, z) { 212 | return h('div', null, (x)) 213 | } 214 | ``` 215 | 216 | 217 | 218 | ### Components 219 | 220 | Components are declared with if the tag starts with a capital letter. 221 | 222 | ```html 223 |
224 | 225 |
226 | ``` 227 | 228 | produces this output 229 | 230 | ```js 231 | h('div', null, h(MyComponent, { foo: 'bar' })) 232 | ``` 233 | 234 | 235 | ### Module example 236 | 237 | How you structure your app is down to you. 238 | I like to keep js and html in separate files so a component might look like this: 239 | 240 | - MyComponent 241 | - view.html (The template file e.g. `
{state.name}
`) 242 | - view.html.js (The transformed `h` output of the file above) 243 | - index.js (Imports the transformed view and exports the component) 244 | 245 | but if you want you could build entire modules in a html file like this: 246 | 247 | ```html 248 | 265 | 266 | 267 |
268 |
269 | 270 | 271 |
272 |
273 |
274 | ``` 275 | 276 | Compiles to 277 | 278 | ```js 279 | import { h, Component } from 'preact' 280 | 281 | export default class MyComponent extends Component { 282 | constructor (props) { 283 | super(props) 284 | this.render = view 285 | 286 | this.onSubmit = e => { 287 | e.preventDefault() 288 | // ... 289 | } 290 | } 291 | } 292 | 293 | function view (props, state) { 294 | return h('section', null, h('form', { onsubmit: this.onSubmit }, [ 295 | h('input', { type: 'text', name: 'text', value: (state.text) }), 296 | h('input', { type: 'text', name: 'description', value: (state.description) }) 297 | ])) 298 | } 299 | ``` 300 | 301 | More examples [here](https://github.com/davidjamesstone/hyperviews/tree/master/examples) 302 | 303 | 304 | Using `browserify`? 305 | Then install the `hyperviewify` transform so you can simply require templates. 306 | 307 | `const view = require('./my-view.html')` 308 | 309 | `npm i hyperviewify` 310 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cat examples/hyperapp/demo/view.html | bin/cmd.js --mode browser --args "state actions" > examples/hyperapp/demo/view.js 4 | cat examples/hyperapp/todo/view.html | bin/cmd.js --mode browser --args "state actions" > examples/hyperapp/todo/view.js 5 | 6 | cat examples/preact/demo/view.html | bin/cmd.js > examples/preact/demo/view.js 7 | cat examples/preact/demo/component.html | bin/cmd.js --mode esm > examples/preact/demo/component.js 8 | cat examples/preact/todo/todos.html | bin/cmd.js > examples/preact/todo/todos.js -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const minimist = require('minimist') 4 | const stdin = process.stdin 5 | const stdout = process.stdout 6 | const argv = minimist(process.argv.slice(2)) 7 | 8 | stdin.setEncoding('utf8') 9 | 10 | const hyperviews = require('../') 11 | let tmpl = '' 12 | 13 | stdin.on('data', (text) => { 14 | tmpl += text 15 | }) 16 | 17 | stdin.on('end', () => { 18 | stdout.write(hyperviews(tmpl, argv.mode, argv.name, argv.args)) 19 | }) 20 | 21 | stdin.resume() 22 | -------------------------------------------------------------------------------- /docs/readme.js: -------------------------------------------------------------------------------- 1 | const hv = require('..') 2 | const ex = `
3 |

{state.title} by {state.author}

4 |

Name: {state.name}

5 | 6 | 7 | Logout 8 | 13 | 14 | Login 15 | 16 |
` 17 | 18 | console.log(hv(ex)) 19 | 20 | const ex1 = ` 21 |
{x}
22 | ` 23 | 24 | console.log(hv(ex1)) 25 | 26 | const ex2 = `
27 | 28 |
` 29 | 30 | console.log(hv(ex2)) 31 | 32 | const ex3 = ` 33 | 50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 |
58 |
` 59 | 60 | console.log(hv(ex3)) 61 | -------------------------------------------------------------------------------- /examples/hyperapp/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | -------------------------------------------------------------------------------- /examples/hyperapp/demo/view.html: -------------------------------------------------------------------------------- 1 |
2 |

{state.count}

3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /examples/hyperapp/demo/view.js: -------------------------------------------------------------------------------- 1 | window.view = function view (state, actions) { 2 | return h('main', null, [ 3 | h('h1', null, (state.count)), 4 | h('button', { 'onclick': actions.down, 'disabled': (state.count <= 0) }, '-'), 5 | h('button', { 'onclick': actions.up }, '+') 6 | ]) 7 | } 8 | -------------------------------------------------------------------------------- /examples/hyperapp/todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/hyperapp/todo/view.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 8 | 9 |
10 | 11 | 12 |
    13 |
  • 14 | {$index + 1}. 15 | 18 | 20 |
  • 21 |
22 | 23 | 24 | Total {state.todos.length} 25 | 29 | 30 | 31 |
{JSON.stringify(state, null, 2)}
32 |
33 | -------------------------------------------------------------------------------- /examples/hyperapp/todo/view.js: -------------------------------------------------------------------------------- 1 | window.view = function view (state, actions) { 2 | return h('section', null, [ 3 | h('form', { 'onsubmit': actions.add }, [ 4 | h('input', { 'type': 'text', 'name': 'text', 'class': 'form-control', 'placeholder': 'Enter new todo', 'value': (state.input), 'required': 'required', 'autocomplete': 'off', 'onchange': actions.updateInput }), 5 | h('input', { 'type': 'checkbox', 'onchange': actions.toggleAll }) 6 | ]), 7 | h('ul', null, (state.todos || []).map(function ($value, $index, $target) { 8 | var todo = $value 9 | return h('li', { 'key': (todo.id) }, [ 10 | ($index + 1) + '.', 11 | h('input', { 'type': 'text', 'value': (todo.text), 'onchange': e => actions.updateText({ id: todo.id, text: this.value }), 'class': 'form-control', 'style': { borderColor: todo.text ? '' : 'red', textDecoration: todo.done ? 'line-through' : '' } }), 12 | h('input', { 'type': 'checkbox', 'checked': (todo.done), 'onchange': e => actions.toggleDone(todo.id) }) 13 | ]) 14 | }, this)), 15 | h('span', null, 'Total ' + (state.todos.length)), 16 | state.todos.find(t => t.done) ? h('button', { 'onclick': actions.clearCompleted }, 'Clear completed') : undefined, 17 | h('pre', null, h('code', null, (JSON.stringify(state, null, 2)))) 18 | ]) 19 | } 20 | -------------------------------------------------------------------------------- /examples/preact/demo/component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello world! 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/preact/demo/component.js: -------------------------------------------------------------------------------- 1 | export default function factoryMyComponent (h) { 2 | return function MyComponent (props, state) { 3 | return h('span', null, [ 4 | 'Hello world!', 5 | h('span') 6 | ]) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/preact/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 35 | 36 | -------------------------------------------------------------------------------- /examples/preact/demo/view.html: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 |
33 |

{state.count}

34 | 35 | 36 | 37 |
38 |
-------------------------------------------------------------------------------- /examples/preact/demo/view.js: -------------------------------------------------------------------------------- 1 | import MyComponent from './component.js' 2 | const { h, Component } = window.preact 3 | 4 | export default class MyCounter extends Component { 5 | constructor (props) { 6 | super(props) 7 | this.render = view 8 | 9 | this.state = { 10 | count: 0 11 | } 12 | 13 | this.onClickUp = () => { 14 | this.setState({ count: this.state.count + 1 }) 15 | } 16 | 17 | this.onClickDown = (e) => { 18 | this.setState({ count: this.state.count - 1 }) 19 | } 20 | } 21 | 22 | componentDidMount () { 23 | 24 | } 25 | } 26 | 27 | function view (props, state) { 28 | return h('div', null, [ 29 | h('h1', null, (state.count)), 30 | h('button', { 'onclick': this.onClickDown, 'disabled': (state.count <= 0) }, '-'), 31 | h('button', { 'onclick': this.onClickUp }, '+'), 32 | h(MyComponent) 33 | ]) 34 | } 35 | -------------------------------------------------------------------------------- /examples/preact/todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /examples/preact/todo/todos.html: -------------------------------------------------------------------------------- 1 | 72 | 73 | 74 |
75 | 76 |
77 | 81 | 82 |
83 | 84 | 85 |
    86 |
  • 87 | {$index + 1}. 88 | 91 | 93 |
  • 94 |
95 | 96 | 97 | Total {state.todos.length} 98 | 99 | 100 | 101 |
{JSON.stringify(state, null, 2)}
102 |
103 |
-------------------------------------------------------------------------------- /examples/preact/todo/todos.js: -------------------------------------------------------------------------------- 1 | const { h, Component } = window.preact 2 | 3 | export default class Todos extends Component { 4 | constructor (props) { 5 | super(props) 6 | this.render = view 7 | 8 | this.state = { 9 | todos: [{ id: 1, text: 'Buy milk' }] 10 | } 11 | 12 | this.onSubmit = e => { 13 | e.preventDefault() 14 | this.setState({ 15 | input: '', 16 | todos: this.state.todos.concat({ 17 | id: Date.now(), 18 | text: this.state.input 19 | }) 20 | }) 21 | } 22 | 23 | this.onChangeInput = e => { 24 | this.setState({ 25 | input: e.target.value.trim() 26 | }) 27 | } 28 | 29 | this.onChangeDoneAll = e => { 30 | const done = e.target.checked 31 | this.setState({ 32 | todos: this.state.todos.map(todo => { 33 | todo.done = done 34 | return todo 35 | }) 36 | }) 37 | } 38 | 39 | this.onChangeDone = (e, todo) => { 40 | this.setState({ 41 | todos: this.state.todos.map(item => { 42 | if (item.id === todo.id) { 43 | item.done = !item.done 44 | } 45 | return item 46 | }) 47 | }) 48 | } 49 | 50 | this.onChangeText = (e, todo) => { 51 | this.setState({ 52 | todos: this.state.todos.map(item => { 53 | if (item.id === todo.id) { 54 | item.text = e.target.value 55 | } 56 | return item 57 | }) 58 | }) 59 | } 60 | 61 | this.onClickClear = e => { 62 | this.setState({ 63 | todos: this.state.todos.filter(t => !t.done) 64 | }) 65 | } 66 | } 67 | } 68 | 69 | function view (props, state) { 70 | return h('section', null, [ 71 | h('form', { 'onsubmit': this.onSubmit }, [ 72 | h('input', { 'type': 'text', 'name': 'text', 'class': 'form-control', 'placeholder': 'Enter new todo', 'value': (state.input), 'required': 'required', 'autocomplete': 'off', 'onchange': this.onChangeInput }), 73 | h('input', { 'type': 'checkbox', 'onchange': this.onChangeDoneAll }) 74 | ]), 75 | h('ul', null, (state.todos || []).map(function ($value, $index, $target) { 76 | var todo = $value 77 | return h('li', { 'key': (todo.id) }, [ 78 | ($index + 1) + '.', 79 | h('input', { 'type': 'text', 'value': (todo.text), 'onchange': e => this.onChangeText(e, todo), 'class': 'form-control', 'style': { borderColor: todo.text ? '' : 'red', textDecoration: todo.done ? 'line-through' : '' } }), 80 | h('input', { 'type': 'checkbox', 'checked': (todo.done), 'onchange': e => this.onChangeDone(e, todo) }) 81 | ]) 82 | }, this)), 83 | h('span', null, 'Total ' + (state.todos.length)), 84 | state.todos.find(t => t.done) ? h('button', { 'onclick': this.onClickClear }, 'Clear done') : undefined, 85 | h('pre', null, h('code', null, (JSON.stringify(state, null, 2)))) 86 | ]) 87 | } 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const standard = require('standard') 2 | const htmlparser = require('htmlparser2') 3 | const delim = ['{', '}'] 4 | const startDelim = delim[0] 5 | const specialEls = ['elseif', 'else'] 6 | const specialAttrs = ['if', 'each'] 7 | let root, buffer, curr, defaultFnName, defaultFnArgs 8 | 9 | function strify (str) { 10 | str = str 11 | ? str.replace('\n', '\\\n') 12 | : '' 13 | 14 | return '"' + str + '"' 15 | } 16 | 17 | function interpolate (text) { 18 | const parts = text.split(/({.*?})/) 19 | 20 | text = parts.filter(p => p).map((part, index) => { 21 | if (part.startsWith('{')) { 22 | return `(${part.slice(1, -1)})` 23 | } else { 24 | return strify(part) 25 | } 26 | }).join(' + ') 27 | 28 | return text 29 | } 30 | 31 | function getIterator (target) { 32 | return `(${target} || [])` 33 | } 34 | 35 | function getAttrs (target) { 36 | const attributes = [] 37 | 38 | for (const name in target.attribs) { 39 | if (specialAttrs.indexOf(name) === -1) { 40 | const value = target.attribs[name] 41 | let val = '' 42 | if (name === 'style' || name.startsWith('on')) { 43 | val = value 44 | } else if (value.indexOf(startDelim) > -1) { 45 | val = interpolate(value) 46 | } else { 47 | val = strify(value) 48 | } 49 | 50 | attributes.push({ 51 | name: name, 52 | value: val 53 | }) 54 | } 55 | } 56 | 57 | const attribs = attributes.length 58 | ? `{ ${attributes.map(a => `'${a.name}': ${a.value}`).join(', ')} }` 59 | : null 60 | 61 | return attribs 62 | } 63 | 64 | function getBranches (node, nodeOutput) { 65 | const branches = [] 66 | 67 | if (node.name === 'if') { 68 | // Element based `if` 69 | let n = node 70 | 71 | do { 72 | branches.push({ 73 | condition: n.attribs['condition'], 74 | rtn: n.childrenToString(true) 75 | }) 76 | 77 | n = n.children.find(c => c.name === 'elseif' || c.name === 'else') 78 | } while (n) 79 | } else { 80 | // Attribute based `if` 81 | branches.push({ 82 | condition: node.attribs['if'], 83 | rtn: nodeOutput 84 | }) 85 | } 86 | 87 | return branches 88 | } 89 | 90 | class Node { 91 | constructor (parent, name, attribs) { 92 | this.parent = parent 93 | this.name = name 94 | this.attribs = attribs 95 | this.children = [] 96 | } 97 | 98 | get isSpecial () { 99 | return specialEls.indexOf(this.name) > -1 100 | } 101 | 102 | childrenToString (filterSpecial) { 103 | const children = (filterSpecial 104 | ? this.children.filter(c => !c.isSpecial) 105 | : this.children).map(c => c.toString()) 106 | 107 | let childstr = '' 108 | if (children.length) { 109 | childstr = children.length === 1 110 | ? children[0] 111 | : '[\n' + children.join(',\n') + '\n]' 112 | } 113 | 114 | return childstr 115 | } 116 | 117 | toString () { 118 | // Attributes 119 | const attribs = getAttrs(this) 120 | 121 | // Children 122 | const childstr = this.childrenToString() 123 | 124 | let node 125 | if (this.name === 'script') { 126 | node = this.children.toString() 127 | } else if (this.name === 'function') { 128 | const name = this.attribs.name || defaultFnName 129 | const argstr = this.attribs.args 130 | ? buildArgs(this.attribs.args) 131 | : defaultFnArgs 132 | 133 | node = ` 134 | ${wrapFn(name, argstr, this.children.toString().trimLeft())} 135 | ` 136 | } else { 137 | const isComponent = /^[A-Z]/.test(this.name) 138 | const name = isComponent ? this.name : `"${this.name}"` 139 | const args = [name] 140 | 141 | if (attribs || childstr) { 142 | args.push(attribs || 'null') 143 | 144 | if (childstr) { 145 | args.push(childstr) 146 | } 147 | } 148 | 149 | node = `h(${args.join(', ')})` 150 | } 151 | 152 | if (this.name === 'if') { 153 | const branches = getBranches(this, node) 154 | let str = '' 155 | branches.forEach((branch, index) => { 156 | if (branch.condition) { 157 | str += `${index === 0 ? 'if' : ' else if '} (${branch.condition}) { 158 | return ${branch.rtn} 159 | }` 160 | } else { 161 | str += ` else { 162 | return ${branch.rtn} 163 | }` 164 | } 165 | }) 166 | 167 | return `(function () { 168 | ${str} 169 | }).call(this)` 170 | } else if ('if' in this.attribs) { 171 | return `${this.attribs['if']} ? ${node} : undefined` 172 | } else if ('each' in this.attribs) { 173 | const eachAttr = this.attribs['each'] 174 | const eachParts = eachAttr.split(' in ') 175 | const key = eachParts[0] 176 | const target = eachParts[1] 177 | 178 | return `${getIterator(target)}.map(function ($value, $index, $target) {\nvar ${key} = $value\nreturn ${node}\n}, this)` 179 | } else { 180 | return node 181 | } 182 | } 183 | } 184 | 185 | class Root extends Node { 186 | toString () { 187 | return this.children.map(c => c.toString()).join('\n').trim() 188 | } 189 | } 190 | 191 | const handler = { 192 | onopentag: function (name, attribs) { 193 | const newCurr = new Node(curr, name, attribs) 194 | curr.children.push(newCurr) 195 | buffer.push(newCurr) 196 | curr = newCurr 197 | }, 198 | ontext: function (text) { 199 | if (!text || !(text = text.trim())) { 200 | return 201 | } 202 | 203 | let value 204 | if (curr.name === 'script') { 205 | value = text 206 | } else if (text.indexOf(startDelim) > -1) { 207 | value = interpolate(text) 208 | } else { 209 | value = strify(text) 210 | } 211 | 212 | curr.children.push(value) 213 | }, 214 | onclosetag: function (name) { 215 | buffer.pop() 216 | curr = buffer[buffer.length - 1] 217 | } 218 | } 219 | 220 | function buildArgs (args) { 221 | return args.split(' ').filter(item => { 222 | return item.trim() 223 | }).join(', ') 224 | } 225 | 226 | function wrapFn (name, args, value) { 227 | return `function ${name} (${args}) { 228 | return ${value} 229 | }` 230 | } 231 | 232 | module.exports = function (tmpl, mode = 'raw', name = 'view', args = 'props state') { 233 | root = new Root() 234 | buffer = [root] 235 | curr = root 236 | 237 | defaultFnName = name 238 | defaultFnArgs = buildArgs(args) 239 | 240 | const parser = new htmlparser.Parser(handler, { 241 | decodeEntities: false, 242 | lowerCaseAttributeNames: false, 243 | lowerCaseTags: false, 244 | recognizeSelfClosing: true 245 | }) 246 | 247 | parser.write(tmpl) 248 | parser.end() 249 | 250 | const js = root.toString() 251 | 252 | let result = '' 253 | try { 254 | if (mode === 'raw') { 255 | result = js 256 | } else { 257 | let wrap = false 258 | let useMode = false 259 | 260 | const children = root.children 261 | if (children.length === 1) { 262 | const onlyChild = children[0] 263 | 264 | // Only wrap the output if there's a single root 265 | // child and that child is not a tag 266 | if (onlyChild.name !== 'function') { 267 | wrap = true 268 | } 269 | 270 | // Only mode the output if there's a single 271 | // root child and that child is not a 29 | 30 | 31 |
32 |

{state.count}

33 | 34 | 35 | 36 |
37 |
38 | 39 | `), 42 | `import { h, Component } from 'preact' 43 | import MyComponent from './component.js' 44 | 45 | export default class MyCounter extends Component { 46 | constructor () { 47 | super() 48 | } 49 | 50 | componentDidMount () { 51 | 52 | } 53 | } 54 | 55 | function view (props, state) { 56 | return h('div', null, [ 57 | h('h1', null, (state.count)), 58 | h('button', { 'onclick': this.onClickDown, 'disabled': (state.count <= 0) }, '-'), 59 | h('button', { 'onclick': this.onClickUp }, '+'), 60 | h(MyComponent) 61 | ]) 62 | } 63 | 64 | console.log('End') 65 | `) 66 | -------------------------------------------------------------------------------- /test/conditionals.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const hv = require('..') 3 | 4 | assert.strictEqual(hv(` 5 |
6 | 7 | Show Me! 8 | 9 |
10 | `), 11 | `h('div', null, (function () { 12 | if (state.bar === 1) { 13 | return h('span', null, 'Show Me!') 14 | } 15 | }.call(this))) 16 | `) 17 | 18 | assert.strictEqual(hv(` 19 |
20 | Show Me! 21 |
22 | `), 23 | `h('div', null, state.bar === 1 ? h('span', null, 'Show Me!') : undefined) 24 | `) 25 | 26 | assert.strictEqual(hv(` 27 |
28 | 29 | odd 30 | 31 | even 32 | 33 |
34 | `), 35 | `h('div', null, (function () { 36 | if (state.bar % 2) { 37 | return h('span', null, 'odd') 38 | } else { 39 | return h('span', null, 'even') 40 | } 41 | }.call(this))) 42 | `) 43 | 44 | assert.strictEqual(hv(` 45 |
46 | 47 | 1 48 | 49 | 2 50 | 51 |
52 | `), 53 | `h('div', null, (function () { 54 | if (state.bar === 1) { 55 | return h('span', null, '1') 56 | } else if (state.bar === 2) { 57 | return h('span', null, '2') 58 | } 59 | }.call(this))) 60 | `) 61 | 62 | assert.strictEqual(hv(` 63 |
64 | 65 | 1 66 | 67 | 2 68 | 69 | bar is neither 1 or 2, it's {state.bar}! 70 | 71 |
72 | `), 73 | `h('div', null, (function () { 74 | if (state.bar === 1) { 75 | return h('span', null, '1') 76 | } else if (state.bar === 2) { 77 | return h('span', null, '2') 78 | } else { 79 | return h('span', null, "bar is neither 1 or 2, it's " + (state.bar) + '!') 80 | } 81 | }.call(this))) 82 | `) 83 | 84 | assert.strictEqual(hv(` 85 |
86 | 87 | foo1 88 | 89 | bar1 90 | 91 | bar2 92 | 93 | 94 | foo2 95 | 96 | foo3 97 | 98 | Default 99 | 100 |
101 | `), 102 | `h('section', null, (function () { 103 | if (state.foo === 1) { 104 | return [ 105 | h('span', null, 'foo1'), 106 | (function () { 107 | if (state.bar === 1) { 108 | return h('span', null, 'bar1') 109 | } else { 110 | return h('span', null, 'bar2') 111 | } 112 | }.call(this)) 113 | ] 114 | } else if (state.foo === 2) { 115 | return h('span', null, 'foo2') 116 | } else if (state.foo === 3) { 117 | return h('span', null, 'foo3') 118 | } else { 119 | return 'Default' 120 | } 121 | }.call(this))) 122 | `) 123 | -------------------------------------------------------------------------------- /test/events.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const hv = require('..') 3 | 4 | assert.strictEqual(hv(` 5 | {state.foo} 6 | `), 7 | `h('a', { 'href': 'http://example.com', 'onclick': this.onClick }, (state.foo)) 8 | `) 9 | 10 | assert.strictEqual(hv(` 11 | {state.foo} 12 | `), 13 | `h('a', { 'href': 'http://example.com', 'onclick': e => e.preventDefault() }, (state.foo)) 14 | `) 15 | 16 | assert.strictEqual(hv(` 17 | {state.foo} 18 | `), 19 | `h('a', { 'href': 'http://example.com', 'onclick': 'alert()' }, (state.foo)) 20 | `) 21 | -------------------------------------------------------------------------------- /test/function.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const hv = require('..') 3 | 4 | assert.strictEqual(hv(` 5 | 6 | A 7 | 8 | `, 'cjs'), 9 | `module.exports = function a (b, c, d) { 10 | return h('span', null, 'A') 11 | } 12 | `) 13 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./basics') 2 | require('./events') 3 | require('./conditionals') 4 | require('./interpolation') 5 | require('./iteration') 6 | require('./cli') 7 | require('./function') 8 | require('./literal') 9 | require('./components') 10 | console.log('All tests passed') 11 | -------------------------------------------------------------------------------- /test/interpolation.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const hv = require('..') 3 | 4 | assert.strictEqual(hv(` 5 |
{state.foo}
6 | `), 7 | `h('div', null, (state.foo)) 8 | `) 9 | 10 | assert.strictEqual(hv(` 11 |
12 | {state.bar} 13 |
14 | `), 15 | `h('div', null, (state.bar)) 16 | `) 17 | 18 | assert.strictEqual(hv(` 19 |
20 | The value of bar is {state.bar}! 21 |
22 | `), 23 | `h('div', null, h('span', null, 'The value of bar is ' + (state.bar) + '!')) 24 | `) 25 | 26 | assert.strictEqual(hv(` 27 |
28 | {state.a}{state.c} 29 |
30 | `), 31 | `h('div', null, h('span', null, (state.a) + (state.c))) 32 | `) 33 | 34 | assert.strictEqual(hv(` 35 |
36 | {state.a} {state.b} {state.c} 37 |
38 | `), 39 | `h('div', null, h('span', null, (state.a) + ' ' + (state.b) + ' ' + (state.c))) 40 | `) 41 | 42 | assert.strictEqual(hv(` 43 |
44 | a is {state.a}, b is {state.b} and c is {state.c} 45 |
46 | `), 47 | `h('div', null, h('span', null, 'a is ' + (state.a) + ', b is ' + (state.b) + ' and c is ' + (state.c))) 48 | `) 49 | 50 | assert.strictEqual(hv(` 51 |
{state.firstName} {state.lastName}
52 | `), 53 | `h('div', { 'id': 'id' }, (state.firstName) + ' ' + (state.lastName)) 54 | `) 55 | 56 | assert.strictEqual(hv(` 57 |
58 | My name is Elizabeth II. 59 | I am your Queen. 60 |
61 | `), 62 | `h('div', null, 'My name is Elizabeth II.\\ 63 | I am your Queen.') 64 | `) 65 | 66 | assert.strictEqual(hv(` 67 |
68 | 69 | My name is {state.name} my age is {state.age} and I live at {state.address} 70 |
71 | `), 72 | `h('div', null, [ 73 | h('a', { 'href': 'http://www.google.co.uk?q=' + (state.query) }), 74 | 'My name is ' + (state.name) + ' my age is ' + (state.age) + ' and I live at ' + (state.address) 75 | ]) 76 | `) 77 | 78 | assert.strictEqual(hv(` 79 |
80 | My name is {state.name} my age is {state.age}. 81 | I live at {state.address} 82 |
83 | `), 84 | `h('div', null, 'My name is ' + (state.name) + ' my age is ' + (state.age) + '.\\ 85 | I live at ' + (state.address)) 86 | `) 87 | -------------------------------------------------------------------------------- /test/iteration.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const hv = require('..') 3 | 4 | assert.strictEqual(hv(` 5 |
    6 |
  • 7 | {item} 8 | OK 9 |
  • 10 |
11 | `), 12 | `h('ul', null, (state.items || []).map(function ($value, $index, $target) { 13 | var item = $value 14 | return h('li', null, [ 15 | h('span', null, (item)), 16 | h('span', null, 'OK') 17 | ]) 18 | }, this)) 19 | `) 20 | 21 | assert.strictEqual(hv(` 22 |
    23 |
  • 24 | {item} 25 | OK 26 |
  • 27 |
28 | `), 29 | `h('ul', null, (state.items || []).map(function ($value, $index, $target) { 30 | var item = $value 31 | return h('li', { 'key': (item) }, [ 32 | h('span', null, (item)), 33 | h('span', null, 'OK') 34 | ]) 35 | }, this)) 36 | `) 37 | 38 | assert.strictEqual(hv(` 39 |
    40 |
  • 41 | {item} 42 | OK 43 |
  • 44 |
45 | `), 46 | `h('ul', null, (state.items.filter(i => i.isPublished) || []).map(function ($value, $index, $target) { 47 | var item = $value 48 | return h('li', null, [ 49 | h('span', null, (item)), 50 | h('span', null, 'OK') 51 | ]) 52 | }, this)) 53 | `) 54 | -------------------------------------------------------------------------------- /test/literal.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const hv = require('..') 3 | 4 | assert.strictEqual(hv(` 5 | 7 | `), 8 | `var s 9 | `) 10 | 11 | assert.strictEqual(hv(` 12 |
    13 |
  • 14 | 15 |
  • 16 |
17 | `), 18 | `h('ul', null, (state.items || []).map(function ($value, $index, $target) { 19 | var item = $value 20 | return h('li', { 'key': (item) }, view(props, state)) 21 | }, this)) 22 | `) 23 | 24 | assert.strictEqual(hv(` 25 |
    26 | 29 |
30 | `), 31 | `h('ul', null, (state.items || []).map(function ($value, $index, $target) { 32 | var item = $value 33 | return view(item, actions) 34 | }, this)) 35 | `) 36 | 37 | assert.strictEqual(hv(` 38 | 41 |
    42 |
  • 43 |
44 | `), 45 | `const a = 'foo' 46 | h('ul', null, h('li')) 47 | `) 48 | --------------------------------------------------------------------------------