├── .npmrc ├── dom.js ├── lib ├── browser.js ├── direct-props.js ├── supported-views.js ├── raw-server.js ├── bool-props.js ├── raw-browser.js ├── set-attribute.js ├── svg-tags.js ├── server.js ├── dom.js ├── append-child.js ├── browserify-transform.js └── babel.js ├── .gitignore ├── tests ├── babel │ ├── fixtures │ │ ├── useImport.js │ │ ├── combinedAttr.js │ │ ├── simple.js │ │ ├── events.js │ │ ├── nesting.js │ │ ├── custom-build-in.js │ │ ├── comment.js │ │ ├── booleanAttr.js │ │ ├── elementsChildren.js │ │ ├── simple.expected.js │ │ ├── empty.js │ │ ├── orderOfOperations.js │ │ ├── variableNames.js │ │ ├── require.js │ │ ├── custom-build-in.expected.js │ │ ├── comment.expected.js │ │ ├── this.js │ │ ├── combinedAttr.expected.js │ │ ├── events.expected.js │ │ ├── useImport.expected.js │ │ ├── empty.expected.js │ │ ├── nesting.expected.js │ │ ├── elementsChildren.expected.js │ │ ├── svg.js │ │ ├── this.expected.js │ │ ├── orderOfOperations.expected.js │ │ ├── arrowFunctions.js │ │ ├── require.expected.js │ │ ├── booleanAttr.expected.js │ │ ├── yoyoBindings.js │ │ ├── yoyoBindings.expected.js │ │ ├── dynamicAttr.js │ │ ├── hyperx.js │ │ ├── variableNames.expected.js │ │ ├── arrowFunctions.expected.js │ │ ├── hyperx.expected.js │ │ ├── dynamicAttr.expected.js │ │ └── svg.expected.js │ ├── build.js │ └── index.js ├── browser │ ├── index.js │ ├── html.js │ ├── raw.js │ ├── api.js │ ├── multiple.js │ ├── elements.js │ └── events.js ├── index.js ├── transform │ ├── build.js │ ├── browser.js │ └── index.js └── server │ └── index.js ├── index.js ├── raw.js ├── types └── index.d.ts ├── .travis.yml ├── bench ├── client.js ├── server.js └── fixtures │ └── app.js ├── LICENSE ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /dom.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/dom') 2 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dom')(document) 2 | -------------------------------------------------------------------------------- /lib/direct-props.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = [ 4 | 'indeterminate' 5 | ] 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | tests/babel/fixtures/*.actual.js 5 | -------------------------------------------------------------------------------- /tests/babel/fixtures/useImport.js: -------------------------------------------------------------------------------- 1 | import html from 'nanohtml' 2 | 3 | html` 4 |
5 | ` 6 | -------------------------------------------------------------------------------- /tests/babel/fixtures/combinedAttr.js: -------------------------------------------------------------------------------- 1 | import html from 'nanohtml' 2 | 3 | html` 4 |
5 | ` 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = typeof window !== 'undefined' 2 | ? require('./lib/browser') 3 | : require('./lib/server') 4 | -------------------------------------------------------------------------------- /tests/babel/fixtures/simple.js: -------------------------------------------------------------------------------- 1 | import html from 'nanohtml' 2 | 3 | html` 4 |
5 | Hello world 6 |
7 | ` 8 | -------------------------------------------------------------------------------- /raw.js: -------------------------------------------------------------------------------- 1 | module.exports = typeof window !== 'undefined' 2 | ? require('./lib/raw-browser') 3 | : require('./lib/raw-server') 4 | -------------------------------------------------------------------------------- /tests/babel/fixtures/events.js: -------------------------------------------------------------------------------- 1 | import html from 'nanohtml' 2 | 3 | html` 4 | 14 | ` 15 | 16 | var result = html` 17 |
    18 |
  • ${button}
  • 19 |
20 | ` 21 | 22 | function onselected (result) { 23 | t.equal(result, 'success') 24 | t.end() 25 | } 26 | 27 | t.equal(result.tagName, 'UL') 28 | t.equal(result.querySelector('button').textContent, 'click me') 29 | 30 | button.click() 31 | }) 32 | 33 | test('using class and className', function (t) { 34 | t.plan(2) 35 | var result = html`
` 36 | t.equal(result.className, 'test1') 37 | result = html`
` 38 | t.equal(result.className, 'test2 another') 39 | t.end() 40 | }) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Choo Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /lib/svg-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = [ 4 | 'svg', 'altGlyph', 'altGlyphDef', 'altGlyphItem', 'animate', 'animateColor', 5 | 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'color-profile', 6 | 'cursor', 'defs', 'desc', 'ellipse', 'feBlend', 'feColorMatrix', 7 | 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 8 | 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 9 | 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 10 | 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 11 | 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence', 'filter', 12 | 'font', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 13 | 'font-face-uri', 'foreignObject', 'g', 'glyph', 'glyphRef', 'hkern', 'image', 14 | 'line', 'linearGradient', 'marker', 'mask', 'metadata', 'missing-glyph', 15 | 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 16 | 'set', 'stop', 'switch', 'symbol', 'text', 'textPath', 'title', 'tref', 17 | 'tspan', 'use', 'view', 'vkern' 18 | ] 19 | -------------------------------------------------------------------------------- /bench/fixtures/app.js: -------------------------------------------------------------------------------- 1 | module.exports = function (html) { 2 | var greeting = 'Hello' 3 | var name = 'special characters, <, >, &' 4 | var drinks = [ 5 | { name: 'Cafe Latte', price: 3.0 }, 6 | { name: 'Cappucino', price: 2.9 }, 7 | { name: 'Club Mate', price: 2.2 }, 8 | { name: 'Berliner Weiße', price: 3.5 } 9 | ] 10 | 11 | var listeners = [] 12 | function onChange (listener) { 13 | listeners.push(listener) 14 | } 15 | function notifyChange () { 16 | listeners.forEach((listener) => listener()) 17 | } 18 | 19 | function devareDrink (drink) { 20 | var index = drinks.indexOf(drink) 21 | if (index >= 0) { 22 | drinks.splice(index, 1) 23 | } 24 | notifyChange() 25 | } 26 | 27 | function drinkView (drink, devareDrink) { 28 | return html` 29 |
  • 30 | ${drink.name} is € ${drink.price} 31 | devareDrink(drink)}>Give me! 32 |
  • 33 | ` 34 | } 35 | 36 | function mainView (greeting, name, drinks, devareDrink) { 37 | return html` 38 |
    39 |

    ${greeting}, ${name}!

    40 | ${drinks.length > 0 ? html` 41 |
      42 | ${drinks.map(drink => drinkView(drink, devareDrink))} 43 |
    44 | ` : html` 45 |

    All drinks are gone!

    46 | `} 47 |
    48 | ` 49 | } 50 | 51 | function render () { 52 | return mainView(greeting, name, drinks, devareDrink) 53 | } 54 | 55 | return { 56 | render: render, 57 | onChange: onChange 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/babel/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const babel = require('babel-core') 5 | const pify = require('pify') 6 | const nanohtml = require('../../') 7 | 8 | const transformFixture = pify(babel.transformFile) 9 | const readExpected = pify(fs.readFile) 10 | const writeActual = pify(fs.writeFile) 11 | 12 | function testFixture (name, opts) { 13 | test(name, (t) => { 14 | t.plan(1) 15 | 16 | const actualPromise = transformFixture(path.join(__dirname, 'fixtures', `${name}.js`), { 17 | plugins: [ 18 | [nanohtml, opts || {}] 19 | ] 20 | }) 21 | const expectedPromise = readExpected(path.join(__dirname, 'fixtures', `${name}.expected.js`), 'utf8') 22 | 23 | Promise.all([ actualPromise, expectedPromise ]) 24 | .then((results) => { 25 | const actual = results[0].code.trim() 26 | const expected = results[1].trim() 27 | 28 | t.equal(actual, expected) 29 | 30 | return writeActual(path.join(__dirname, 'fixtures', `${name}.actual.js`), results[0].code) 31 | }) 32 | .catch((err) => { 33 | t.fail(err.message) 34 | }) 35 | .then(() => t.end()) 36 | }) 37 | } 38 | 39 | testFixture('simple') 40 | testFixture('custom-build-in') 41 | testFixture('empty') 42 | testFixture('this') 43 | testFixture('variableNames') 44 | testFixture('nesting') 45 | testFixture('elementsChildren') 46 | testFixture('combinedAttr') 47 | testFixture('booleanAttr') 48 | testFixture('dynamicAttr') 49 | testFixture('events') 50 | testFixture('orderOfOperations') 51 | testFixture('svg') 52 | testFixture('require') 53 | testFixture('yoyoBindings') 54 | testFixture('arrowFunctions') 55 | testFixture('hyperx') 56 | testFixture('comment') 57 | testFixture('useImport', { useImport: true }) 58 | -------------------------------------------------------------------------------- /tests/browser/multiple.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | if (typeof window !== 'undefined') { 3 | var document = window.document 4 | var html = require('../../') 5 | } else { 6 | var nano = require('./html') 7 | document = nano.document 8 | html = nano.html 9 | } 10 | 11 | test('multiple elements', function (t) { 12 | var multiple = html`
  • Hamburg
  • Helsinki
  • haha
  • Berlin
    test
  • ` 13 | 14 | var list = document.createElement('ul') 15 | list.appendChild(multiple) 16 | t.equal(list.children.length, 3, '3 children') 17 | t.equal(list.childNodes.length, 4, '4 childNodes') 18 | t.equal(list.children[0].tagName, 'LI', 'list tag name') 19 | t.equal(list.children[0].textContent, 'Hamburg') 20 | t.equal(list.children[1].textContent, 'Helsinki') 21 | t.equal(list.children[2].textContent, 'Berlintest') 22 | t.equal(list.querySelector('div').textContent, 'test', 'created sub-element') 23 | t.equal(list.childNodes[2].nodeValue, 'haha') 24 | t.end() 25 | }) 26 | 27 | test('nested fragments', function (t) { 28 | var fragments = html`
    1
    ab${html`cd
    2
    between
    3
    `}
    4
    ` 29 | t.equals(fragments.textContent, '1abcd2between34') 30 | t.equals(fragments.children.length, 4) 31 | t.equals(fragments.childNodes[4].textContent, 'between') 32 | t.equals(fragments.childNodes.length, 7) 33 | t.end() 34 | }) 35 | 36 | test('multiple elements mixed with array', function (t) { 37 | var arr = [html`
  • Helsinki
  • `, null, html`
  • Stockholm
  • `] 38 | var multiple = html`
  • Hamburg
  • ${arr}
  • Berlin
  • ` 39 | 40 | var list = document.createElement('ul') 41 | list.appendChild(multiple) 42 | t.equal(list.children.length, 4, '4 children') 43 | t.equal(list.children[0].tagName, 'LI', 'list tag name') 44 | t.equal(list.children[0].textContent, 'Hamburg') 45 | t.equal(list.children[1].textContent, 'Helsinki') 46 | t.equal(list.children[2].textContent, 'Stockholm') 47 | t.equal(list.children[3].textContent, 'Berlin') 48 | t.end() 49 | }) 50 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var BOOL_PROPS = require('./bool-props') 4 | 5 | var boolPropRx = new RegExp('([^-a-z](' + BOOL_PROPS.join('|') + '))=["\']?$', 'i') 6 | var query = /(?:="|&)[^"]*=$/ 7 | 8 | module.exports = nanothtmlServer 9 | module.exports.default = module.exports 10 | 11 | function nanothtmlServer (src, filename, options, done) { 12 | if (typeof src === 'string' && !/\n/.test(src) && filename && filename._flags) { 13 | var args = Array.prototype.slice.apply(arguments) 14 | return require('./browserify-transform.js').apply(this, args) 15 | } 16 | if (typeof src === 'object' && src && src.types && src.template) { 17 | return require('./babel').apply(this, arguments) 18 | } 19 | 20 | var boolMatch 21 | var pieces = arguments[0] 22 | var output = '' 23 | for (var i = 0; i < pieces.length; i++) { 24 | var piece = pieces[i] 25 | if (i < pieces.length - 1) { 26 | if ((boolMatch = boolPropRx.exec(piece))) { 27 | output += piece.slice(0, boolMatch.index) 28 | if (arguments[i + 1]) { 29 | output += boolMatch[1] + '="' + boolMatch[2] + '"' 30 | } 31 | continue 32 | } 33 | 34 | var value = handleValue(arguments[i + 1]) 35 | if (piece[piece.length - 1] === '=' && !query.test(piece)) { 36 | output += piece + '"' + value + '"' 37 | } else { 38 | output += piece + value 39 | } 40 | } else { 41 | output += piece 42 | } 43 | } 44 | 45 | // HACK: Avoid double encoding by marking encoded string 46 | // You cannot add properties to string literals 47 | // eslint-disable-next-line no-new-wrappers 48 | var wrapper = new String(output) 49 | wrapper.__encoded = true 50 | return wrapper 51 | } 52 | 53 | function handleValue (value) { 54 | // Handle each item in array as potential unescaped value 55 | if (Array.isArray(value)) return value.map(handleValue).join('') 56 | 57 | // Ignore event handlers. `onclick=${(e) => doSomething(e)}` 58 | // will become. `onclick=""` 59 | if (typeof value === 'function') return '' 60 | if (value === null || value === undefined) return '' 61 | if (value.__encoded) return value 62 | 63 | if (typeof value === 'object') { 64 | if (typeof value.outerHTML === 'string') return value.outerHTML 65 | return Object.keys(value).reduce(function (str, key, i) { 66 | if (str.length > 0) str += ' ' 67 | 68 | if (BOOL_PROPS.indexOf(key) !== -1) { 69 | if (value[key]) { 70 | return str + key + '="' + key + '"' 71 | } 72 | return str 73 | } 74 | 75 | var handled = handleValue(value[key]) 76 | return str + key + '="' + handled + '"' 77 | }, '') 78 | } 79 | 80 | var str = value.toString() 81 | return str 82 | .replace(/&/g, '&') 83 | .replace(//g, '>') 85 | .replace(/"/g, '"') 86 | .replace(/'/g, ''') 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanohtml", 3 | "description": "HTML template strings for the Browser with support for Server Side Rendering in Node.", 4 | "repository": "choojs/nanohtml", 5 | "version": "1.10.0", 6 | "main": "index.js", 7 | "files": [ 8 | "index.js", 9 | "raw.js", 10 | "dom.js", 11 | "lib", 12 | "types", 13 | "dist" 14 | ], 15 | "types": "./types/index.d.ts", 16 | "scripts": { 17 | "bench": "node bench/server.js && browserify bench/client.js | tape-run", 18 | "build": "mkdir -p dist/ && browserify index -s html -p bundle-collapser/plugin > dist/bundle.js && browserify index -s html -p tinyify > dist/bundle.min.js && cat dist/bundle.min.js | gzip --best --stdout | wc -c", 19 | "prepublishOnly": "npm run build", 20 | "test": "npm run test:standard && npm run test:node && npm run test:browser && npm run test:transform-browser && npm run test:babel-browser", 21 | "test:standard": "standard", 22 | "test:node": "node tests", 23 | "test:browser": "browserify tests/browser | tape-run", 24 | "test:transform-browser": "node tests/transform/build | tape-run", 25 | "test:babel-browser": "node tests/babel/build | tape-run" 26 | }, 27 | "dependencies": { 28 | "acorn-node": "^1.8.2", 29 | "camel-case": "^3.0.0", 30 | "convert-source-map": "^1.5.1", 31 | "estree-is-member-expression": "^1.0.0", 32 | "hyperx": "^2.5.0", 33 | "is-boolean-attribute": "0.0.1", 34 | "nanoassert": "^1.1.0", 35 | "nanobench": "^2.1.0", 36 | "normalize-html-whitespace": "^0.2.0", 37 | "through2": "^2.0.3", 38 | "transform-ast": "^2.4.0" 39 | }, 40 | "devDependencies": { 41 | "aliasify": "^2.1.0", 42 | "babel-core": "^6.26.0", 43 | "babel-plugin-transform-es2015-template-literals": "^6.22.0", 44 | "babel-register": "^6.26.0", 45 | "babelify": "^8.0.0", 46 | "browserify": "^16.1.1", 47 | "bubleify": "^1.2.0", 48 | "bundle-collapser": "^1.3.0", 49 | "choo": "^6.9.0", 50 | "jsdom": "^15.2.0", 51 | "pify": "^3.0.0", 52 | "standard": "^10.0.3", 53 | "tape": "^4.8.0", 54 | "tape-run": "^6.0.0", 55 | "tinyify": "^2.4.0" 56 | }, 57 | "keywords": [ 58 | "choo", 59 | "node", 60 | "html", 61 | "template-string", 62 | "strings", 63 | "template", 64 | "string", 65 | "lit-html", 66 | "yo-yo", 67 | "choo.js", 68 | "es6", 69 | "HTML", 70 | "DOM", 71 | "diff", 72 | "render", 73 | "multi", 74 | "line", 75 | "tagged", 76 | "native", 77 | "hyperhtml", 78 | "hyperdom", 79 | "fast", 80 | "small", 81 | "lite", 82 | "tiny", 83 | "nano" 84 | ], 85 | "license": "MIT", 86 | "browser": { 87 | "assert": "nanoassert", 88 | "./index.js": "./lib/browser.js", 89 | "./raw.js": "./lib/raw-browser.js" 90 | }, 91 | "standard": { 92 | "ignore": [ 93 | "tests/babel/fixtures/**/*.js" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/dom.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var hyperx = require('hyperx') 4 | var appendChild = require('./append-child') 5 | var SVG_TAGS = require('./svg-tags') 6 | var BOOL_PROPS = require('./bool-props') 7 | // Props that need to be set directly rather than with el.setAttribute() 8 | var DIRECT_PROPS = require('./direct-props') 9 | 10 | var SVGNS = 'http://www.w3.org/2000/svg' 11 | var XLINKNS = 'http://www.w3.org/1999/xlink' 12 | 13 | var COMMENT_TAG = '!--' 14 | 15 | module.exports = function (document) { 16 | function nanoHtmlCreateElement (tag, props, children) { 17 | var el 18 | 19 | // If an svg tag, it needs a namespace 20 | if (SVG_TAGS.indexOf(tag) !== -1) { 21 | props.namespace = SVGNS 22 | } 23 | 24 | // If we are using a namespace 25 | var ns = false 26 | if (props.namespace) { 27 | ns = props.namespace 28 | delete props.namespace 29 | } 30 | 31 | // If we are extending a builtin element 32 | var isCustomElement = false 33 | if (props.is) { 34 | isCustomElement = props.is 35 | delete props.is 36 | } 37 | 38 | // Create the element 39 | if (ns) { 40 | if (isCustomElement) { 41 | el = document.createElementNS(ns, tag, { is: isCustomElement }) 42 | } else { 43 | el = document.createElementNS(ns, tag) 44 | } 45 | } else if (tag === COMMENT_TAG) { 46 | return document.createComment(props.comment) 47 | } else if (isCustomElement) { 48 | el = document.createElement(tag, { is: isCustomElement }) 49 | } else { 50 | el = document.createElement(tag) 51 | } 52 | 53 | // Create the properties 54 | for (var p in props) { 55 | if (props.hasOwnProperty(p)) { 56 | var key = p.toLowerCase() 57 | var val = props[p] 58 | // Normalize className 59 | if (key === 'classname') { 60 | key = 'class' 61 | p = 'class' 62 | } 63 | // The for attribute gets transformed to htmlFor, but we just set as for 64 | if (p === 'htmlFor') { 65 | p = 'for' 66 | } 67 | // If a property is boolean, set itself to the key 68 | if (BOOL_PROPS.indexOf(key) !== -1) { 69 | if (String(val) === 'true') val = key 70 | else if (String(val) === 'false') continue 71 | } 72 | // If a property prefers being set directly vs setAttribute 73 | if (key.slice(0, 2) === 'on' || DIRECT_PROPS.indexOf(key) !== -1) { 74 | el[p] = val 75 | } else { 76 | if (ns) { 77 | if (p === 'xlink:href') { 78 | el.setAttributeNS(XLINKNS, p, val) 79 | } else if (/^xmlns($|:)/i.test(p)) { 80 | // skip xmlns definitions 81 | } else { 82 | el.setAttributeNS(null, p, val) 83 | } 84 | } else { 85 | el.setAttribute(p, val) 86 | } 87 | } 88 | } 89 | } 90 | 91 | appendChild(el, children) 92 | return el 93 | } 94 | 95 | function createFragment (nodes) { 96 | var fragment = document.createDocumentFragment() 97 | for (var i = 0; i < nodes.length; i++) { 98 | if (nodes[i] == null) continue 99 | if (Array.isArray(nodes[i])) { 100 | fragment.appendChild(createFragment(nodes[i])) 101 | } else { 102 | if (typeof nodes[i] === 'string') nodes[i] = document.createTextNode(nodes[i]) 103 | fragment.appendChild(nodes[i]) 104 | } 105 | } 106 | return fragment 107 | } 108 | 109 | var exports = hyperx(nanoHtmlCreateElement, { 110 | comments: true, 111 | createFragment: createFragment 112 | }) 113 | exports.default = exports 114 | exports.createComment = nanoHtmlCreateElement 115 | return exports 116 | } 117 | -------------------------------------------------------------------------------- /tests/server/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var html = require('../../') 3 | var raw = require('../../raw') 4 | 5 | test('server side render', function (t) { 6 | t.plan(2) 7 | var element = html`
    8 |

    hello!

    9 |
    ` 10 | var result = element.toString() 11 | t.ok(result.indexOf('

    hello!

    ') !== -1, 'contains a child element') 12 | t.ok(result.indexOf('
    ') !== -1, 'attribute gets set') 13 | t.end() 14 | }) 15 | 16 | test('passing another element to html on server side render', function (t) { 17 | t.plan(1) 18 | var button = html`` 19 | var element = html`
    20 | ${button} 21 |
    ` 22 | var result = element.toString() 23 | t.ok(result.indexOf('') !== -1, 'button rendered correctly') 24 | t.end() 25 | }) 26 | 27 | test('style attribute', function (t) { 28 | t.plan(1) 29 | var name = 'test' 30 | var result = html`

    Hey ${name.toUpperCase()}, This is a card!!!

    ` 31 | var expected = '

    Hey TEST, This is a card!!!

    ' 32 | t.equal(result.toString(), expected) 33 | t.end() 34 | }) 35 | 36 | test('escape text inside html', function (t) { 37 | t.plan(1) 38 | 39 | var expected = 'Hello <3' 40 | var result = html`${'Hello <3'}`.toString() 41 | 42 | t.equal(result, expected) 43 | t.end() 44 | }) 45 | 46 | test('escape array of text inside html', function (t) { 47 | t.plan(1) 48 | 49 | var expected = 'Hello <3' 50 | var result = html`${['Hello', ' ', '<3']}`.toString() 51 | 52 | t.equal(result, expected) 53 | t.end() 54 | }) 55 | 56 | test('unescape html', function (t) { 57 | t.plan(1) 58 | 59 | var expected = 'Hello there' 60 | var result = raw('Hello there').toString() 61 | 62 | t.equal(result, expected) 63 | t.end() 64 | }) 65 | 66 | test('unescape html inside html', function (t) { 67 | t.plan(1) 68 | 69 | var expected = 'Hello there' 70 | var result = html`${raw('Hello there')}`.toString() 71 | 72 | t.equal(result, expected) 73 | t.end() 74 | }) 75 | 76 | test('quote attributes', function (t) { 77 | t.plan(1) 78 | 79 | var title = 'greeting' 80 | var expected = 'Hello there' 81 | var result = html`Hello there`.toString() 82 | 83 | t.equal(result, expected) 84 | t.end() 85 | }) 86 | 87 | test('respect query parameters', function (t) { 88 | t.plan(1) 89 | 90 | var param = 'planet' 91 | var expected = 'Hello planet' 92 | var result = html`Hello planet`.toString() 93 | 94 | t.equal(result, expected) 95 | t.end() 96 | }) 97 | 98 | test('event attribute', function (t) { 99 | t.plan(1) 100 | 101 | var expected = '
    Hello
    ' 102 | var result = html`
    Hello
    `.toString() 103 | 104 | t.equal(result, expected) 105 | t.end() 106 | 107 | function onmouseover () {} 108 | function onmouseout () {} 109 | }) 110 | 111 | test('boolean attribute', function (t) { 112 | t.plan(1) 113 | 114 | var expected = '' 115 | var result = html``.toString() 116 | 117 | t.equal(result, expected) 118 | t.end() 119 | }) 120 | 121 | test('spread attributes', function (t) { 122 | t.plan(1) 123 | 124 | var expected = '
    Hello
    ' 125 | var props = { class: 'abc', id: 'def' } 126 | var result = html`
    Hello
    `.toString() 127 | 128 | t.equal(result, expected) 129 | t.end() 130 | }) 131 | 132 | test('multiple root elements', function (t) { 133 | t.plan(1) 134 | 135 | var expected = '
    1
    2
    3
    5
    ' 136 | var result = html`
    1
    2
    3
    5
    `.toString() 137 | 138 | t.equal(expected, result) 139 | t.end() 140 | }) 141 | 142 | test('nested multiple root elements', function (t) { 143 | t.plan(1) 144 | 145 | var expected = '
    1
    2
    3
    4
    ' 146 | var result = html`
    1
    ${html`
    2
    3
    `}
    4
    `.toString() 147 | 148 | t.equal(expected, result) 149 | t.end() 150 | }) 151 | -------------------------------------------------------------------------------- /lib/append-child.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var trailingNewlineRegex = /\n[\s]+$/ 4 | var leadingNewlineRegex = /^\n[\s]+/ 5 | var trailingSpaceRegex = /[\s]+$/ 6 | var leadingSpaceRegex = /^[\s]+/ 7 | var multiSpaceRegex = /[\n\s]+/g 8 | 9 | var TEXT_TAGS = [ 10 | 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'data', 'dfn', 'em', 'i', 11 | 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'amp', 'small', 'span', 12 | 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr' 13 | ] 14 | 15 | var VERBATIM_TAGS = [ 16 | 'code', 'pre', 'textarea' 17 | ] 18 | 19 | module.exports = function appendChild (el, childs) { 20 | if (!Array.isArray(childs)) return 21 | 22 | var nodeName = el.nodeName.toLowerCase() 23 | 24 | var hadText = false 25 | var value, leader 26 | 27 | for (var i = 0, len = childs.length; i < len; i++) { 28 | var node = childs[i] 29 | if (Array.isArray(node)) { 30 | appendChild(el, node) 31 | continue 32 | } 33 | 34 | if (typeof node === 'number' || 35 | typeof node === 'boolean' || 36 | typeof node === 'function' || 37 | node instanceof Date || 38 | node instanceof RegExp) { 39 | node = node.toString() 40 | } 41 | 42 | var lastChild = el.childNodes[el.childNodes.length - 1] 43 | 44 | // Iterate over text nodes 45 | if (typeof node === 'string') { 46 | hadText = true 47 | 48 | // If we already had text, append to the existing text 49 | if (lastChild && lastChild.nodeName === '#text') { 50 | lastChild.nodeValue += node 51 | 52 | // We didn't have a text node yet, create one 53 | } else { 54 | node = el.ownerDocument.createTextNode(node) 55 | el.appendChild(node) 56 | lastChild = node 57 | } 58 | 59 | // If this is the last of the child nodes, make sure we close it out 60 | // right 61 | if (i === len - 1) { 62 | hadText = false 63 | // Trim the child text nodes if the current node isn't a 64 | // node where whitespace matters. 65 | if (TEXT_TAGS.indexOf(nodeName) === -1 && 66 | VERBATIM_TAGS.indexOf(nodeName) === -1) { 67 | value = lastChild.nodeValue 68 | .replace(leadingNewlineRegex, '') 69 | .replace(trailingSpaceRegex, '') 70 | .replace(trailingNewlineRegex, '') 71 | .replace(multiSpaceRegex, ' ') 72 | if (value === '') { 73 | el.removeChild(lastChild) 74 | } else { 75 | lastChild.nodeValue = value 76 | } 77 | } else if (VERBATIM_TAGS.indexOf(nodeName) === -1) { 78 | // The very first node in the list should not have leading 79 | // whitespace. Sibling text nodes should have whitespace if there 80 | // was any. 81 | leader = i === 0 ? '' : ' ' 82 | value = lastChild.nodeValue 83 | .replace(leadingNewlineRegex, leader) 84 | .replace(leadingSpaceRegex, ' ') 85 | .replace(trailingSpaceRegex, '') 86 | .replace(trailingNewlineRegex, '') 87 | .replace(multiSpaceRegex, ' ') 88 | lastChild.nodeValue = value 89 | } 90 | } 91 | 92 | // Iterate over DOM nodes 93 | } else if (node && node.nodeType) { 94 | // If the last node was a text node, make sure it is properly closed out 95 | if (hadText) { 96 | hadText = false 97 | 98 | // Trim the child text nodes if the current node isn't a 99 | // text node or a code node 100 | if (TEXT_TAGS.indexOf(nodeName) === -1 && 101 | VERBATIM_TAGS.indexOf(nodeName) === -1) { 102 | value = lastChild.nodeValue 103 | .replace(leadingNewlineRegex, '') 104 | .replace(trailingNewlineRegex, ' ') 105 | .replace(multiSpaceRegex, ' ') 106 | 107 | // Remove empty text nodes, append otherwise 108 | if (value === '') { 109 | el.removeChild(lastChild) 110 | } else { 111 | lastChild.nodeValue = value 112 | } 113 | // Trim the child nodes but preserve the appropriate whitespace 114 | } else if (VERBATIM_TAGS.indexOf(nodeName) === -1) { 115 | value = lastChild.nodeValue 116 | .replace(leadingSpaceRegex, ' ') 117 | .replace(leadingNewlineRegex, '') 118 | .replace(trailingNewlineRegex, ' ') 119 | .replace(multiSpaceRegex, ' ') 120 | lastChild.nodeValue = value 121 | } 122 | } 123 | 124 | // Store the last nodename 125 | var _nodeName = node.nodeName 126 | if (_nodeName) nodeName = _nodeName.toLowerCase() 127 | 128 | // Append the node to the DOM 129 | el.appendChild(node) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanohtml 2 | [![npm version][2]][3] [![build status][4]][5] 3 | [![downloads][8]][9] [![js-standard-style][10]][11] 4 | 5 | HTML template strings for the Browser with support for Server Side 6 | Rendering in Node. 7 | 8 | ## Installation 9 | ```sh 10 | $ npm install nanohtml 11 | ``` 12 | 13 | ## Usage 14 | ### Browser 15 | ```js 16 | var html = require('nanohtml') 17 | 18 | var el = html` 19 | 20 |

    Hello planet

    21 | 22 | ` 23 | 24 | document.body.appendChild(el) 25 | ``` 26 | 27 | ### Node 28 | Node doesn't have a DOM available. So in order to render HTML we use string 29 | concatenation instead. This has the fun benefit of being quite efficient, which 30 | in turn means it's great for server rendering! 31 | 32 | ```js 33 | var html = require('nanohtml') 34 | 35 | var el = html` 36 | 37 |

    Hello planet

    38 | 39 | ` 40 | 41 | console.log(el.toString()) 42 | ``` 43 | 44 | ### Node with custom DOM 45 | Modules like [`jsdom`](https://github.com/jsdom/jsdom) implement (parts of) 46 | the DOM in pure JavaScript. If you don't really need the performance of 47 | string concatenation, or use nanohtml components that modify the raw DOM, use 48 | `nanohtml/dom` to give nanohtml a custom Document. 49 | 50 | ```js 51 | var JSDOM = require('jsdom').JSDOM 52 | var nanohtml = require('nanohtml/dom') 53 | var jsdom = new JSDOM() 54 | 55 | var html = nanohtml(jsdom.window.document) 56 | var el = html` 57 | 58 |

    Hello planet

    59 | 60 | ` 61 | el.appendChild(html`

    A paragraph

    `) 62 | 63 | el.outerHTML === '

    Hello planet

    A paragraph

    ' 64 | ``` 65 | 66 | ### Interpolating unescaped HTML 67 | By default all content inside template strings is escaped. This is great for 68 | strings, but not ideal if you want to insert HTML that's been returned from 69 | another function (for example: a markdown renderer). Use `nanohtml/raw` for 70 | to interpolate HTML directly. 71 | 72 | ```js 73 | var raw = require('nanohtml/raw') 74 | var html = require('nanohtml') 75 | 76 | var string = '

    This a regular string.

    ' 77 | var el = html` 78 | 79 | ${raw(string)} 80 | 81 | ` 82 | 83 | document.body.appendChild(el) 84 | ``` 85 | 86 | ### Attaching event listeners 87 | ```js 88 | var html = require('nanohtml') 89 | 90 | var el = html` 91 | 92 | 95 | 96 | ` 97 | 98 | document.body.appendChild(el) 99 | 100 | function onclick (e) { 101 | console.log(`${e.target} was clicked`) 102 | } 103 | ``` 104 | 105 | ### Multiple root elements 106 | 107 | If you have more than one root element they will be combined with a [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment). 108 | 109 | ```js 110 | var html = require('nanohtml') 111 | 112 | var el = html` 113 |
  • Chashu
  • 114 |
  • Nori
  • 115 | ` 116 | 117 | document.querySelector('ul').appendChild(el) 118 | ``` 119 | 120 | ### Conditional attributes 121 | 122 | Use a javascript object to conditionally add HTML attributes. 123 | 124 | ```js 125 | var html = require('nanohtml') 126 | 127 | var customAttr = isFuzzy ? { 'data-hand-feel': 'super-fuzzy' } : {} 128 | var el = html` 129 |
    130 | ` 131 | ``` 132 | 133 | ## Static optimizations 134 | Parsing HTML has significant overhead. Being able to parse HTML statically, 135 | ahead of time can speed up rendering to be about twice as fast. 136 | 137 | ### Browserify 138 | 139 | #### From the command line 140 | ```sh 141 | $ browserify -t nanohtml index.js > bundle.js 142 | ``` 143 | 144 | #### Programmatically 145 | ```js 146 | var browserify = require('browserify') 147 | var nanohtml = require('nanohtml') 148 | var path = require('path') 149 | 150 | var b = browserify(path.join(__dirname, 'index.js')) 151 | .transform(nanohtml) 152 | 153 | b.bundle().pipe(process.stdout) 154 | ``` 155 | 156 | #### In package.json 157 | ```json 158 | { 159 | "name": "my-app", 160 | "private": true, 161 | "browserify": { 162 | "transform": [ 163 | "nanohtml" 164 | ] 165 | }, 166 | "dependencies": { 167 | "nanohtml": "^1.0.0" 168 | } 169 | } 170 | ``` 171 | 172 | ## Bundling 173 | 174 | ### Webpack 175 | At the time of writing there's no Webpack loader yet. We'd love a contribution! 176 | 177 | ### Babel / Parcel 178 | 179 | Add nanohtml to your `.babelrc` config. 180 | 181 | Without options: 182 | 183 | ```js 184 | { 185 | "plugins": [ 186 | "nanohtml" 187 | ] 188 | } 189 | ``` 190 | 191 | With options: 192 | 193 | ```js 194 | { 195 | "plugins": [ 196 | ["nanohtml", { 197 | "useImport": true 198 | }] 199 | ] 200 | } 201 | ``` 202 | 203 | #### Options 204 | 205 | - `useImport` - Set to true to use `import` statements for injected modules. 206 | By default, `require` is used. 207 | - `appendChildModule` - Import path to a module that contains an `appendChild` 208 | function. Defaults to `"nanohtml/lib/append-child"`. 209 | 210 | ### Rollup 211 | 212 | Use the [@rollup/plugin-commonjs](https://github.com/rollup/plugins/tree/master/packages/commonjs#using-with-rollupplugin-node-resolve) plugin with [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/master/packages/node-resolve). Explicitly import the browser or server entrypoint in your application. E.g.: 213 | 214 | ``` 215 | import html from 'nanohtml/lib/browser'; 216 | ``` 217 | 218 | ## Attributions 219 | Shout out to [Shama](https://github.com/shama) and 220 | [Shuhei](https://github.com/shuhei) for their contributions to 221 | [Bel](https://github.com/shama/bel), 222 | [yo-yoify](https://github.com/shama/yo-yoify) and 223 | [pelo](https://github.com/shuhei/pelo). This module is based on their work, and 224 | wouldn't have been possible otherwise! 225 | 226 | ## See Also 227 | - [choojs/nanomorph](https://github.com/choojs/nanomorph) 228 | 229 | ## License 230 | [MIT](./LICENSE) 231 | 232 | [0]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square 233 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index 234 | [2]: https://img.shields.io/npm/v/nanohtml.svg?style=flat-square 235 | [3]: https://npmjs.org/package/nanohtml 236 | [4]: https://img.shields.io/travis/choojs/nanohtml/master.svg?style=flat-square 237 | [5]: https://travis-ci.org/choojs/nanohtml 238 | [6]: https://img.shields.io/codecov/c/github/choojs/nanohtml/master.svg?style=flat-square 239 | [7]: https://codecov.io/github/choojs/nanohtml 240 | [8]: http://img.shields.io/npm/dt/nanohtml.svg?style=flat-square 241 | [9]: https://npmjs.org/package/nanohtml 242 | [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 243 | [11]: https://github.com/feross/standard 244 | -------------------------------------------------------------------------------- /tests/browser/elements.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | if (typeof window !== 'undefined') { 3 | var document = window.document 4 | var html = require('../../') 5 | } else { 6 | var nano = require('./html') 7 | document = nano.document 8 | html = nano.html 9 | } 10 | 11 | test('create inputs', function (t) { 12 | t.plan(7) 13 | 14 | var expected = 'testing' 15 | var result = html`` 16 | t.equal(result.tagName, 'INPUT', 'created an input') 17 | t.equal(result.value, expected, 'set the value of an input') 18 | 19 | result = html`` 20 | t.equal(result.getAttribute('type'), 'checkbox', 'created a checkbox') 21 | t.equal(result.getAttribute('checked'), 'checked', 'set the checked attribute') 22 | t.equal(result.getAttribute('disabled'), null, 'should not have set the disabled attribute') 23 | t.equal(result.indeterminate, true, 'should have set indeterminate property') 24 | 25 | result = html`` 26 | t.equal(result.indeterminate, true, 'should have set indeterminate property') 27 | 28 | t.end() 29 | }) 30 | 31 | test('create inputs with object spread', function (t) { 32 | t.plan(7) 33 | 34 | var expected = 'testing' 35 | var props = { type: 'text', value: expected } 36 | var result = html`` 37 | t.equal(result.tagName, 'INPUT', 'created an input') 38 | t.equal(result.value, expected, 'set the value of an input') 39 | 40 | props = { type: 'checkbox', checked: true, disabled: false, indeterminate: true } 41 | result = html`` 42 | t.equal(result.getAttribute('type'), 'checkbox', 'created a checkbox') 43 | t.equal(result.getAttribute('checked'), 'checked', 'set the checked attribute') 44 | t.equal(result.getAttribute('disabled'), null, 'should not have set the disabled attribute') 45 | t.equal(result.indeterminate, true, 'should have set indeterminate property') 46 | 47 | props = { indeterminate: true } 48 | result = html`` 49 | t.equal(result.indeterminate, true, 'should have set indeterminate property') 50 | 51 | t.end() 52 | }) 53 | 54 | test('can update and submit inputs', function (t) { 55 | t.plan(2) 56 | document.body.innerHTML = '' 57 | var expected = 'testing' 58 | function render (data, onsubmit) { 59 | var input = html`` 60 | return html`
    61 | ${input} 62 | 65 |
    ` 66 | } 67 | var result = render(expected, function onsubmit (newvalue) { 68 | t.equal(newvalue, 'changed') 69 | document.body.innerHTML = '' 70 | t.end() 71 | }) 72 | document.body.appendChild(result) 73 | t.equal(document.querySelector('input').value, expected, 'set the input correctly') 74 | document.querySelector('input').value = 'changed' 75 | document.querySelector('button').click() 76 | }) 77 | 78 | test('svg', function (t) { 79 | t.plan(4) 80 | var result = html` 81 | 82 | 83 | ` 84 | t.equal(result.tagName, 'svg', 'create svg tag') 85 | t.equal(result.childNodes[0].tagName, 'rect', 'created child rect tag') 86 | t.equal(result.childNodes[1].getAttribute('xlink:href'), '#test', 'created child use tag with xlink:href') 87 | t.equal(result.childNodes[1].attributes.getNamedItem('xlink:href').namespaceURI, 'http://www.w3.org/1999/xlink', 'created child use tag with xlink:href') 88 | t.end() 89 | }) 90 | 91 | test('svg with namespace', function (t) { 92 | t.plan(3) 93 | var result 94 | function create () { 95 | result = html` 96 | 97 | ` 98 | } 99 | t.doesNotThrow(create) 100 | t.equal(result.tagName, 'svg', 'create svg tag') 101 | t.equal(result.childNodes[0].tagName, 'rect', 'created child rect tag') 102 | }) 103 | 104 | test('svg with xmlns:svg', function (t) { 105 | t.plan(3) 106 | var result 107 | function create () { 108 | result = html` 109 | 110 | ` 111 | } 112 | t.doesNotThrow(create) 113 | t.equal(result.tagName, 'svg', 'create svg tag') 114 | t.equal(result.childNodes[0].tagName, 'rect', 'created child rect tag') 115 | }) 116 | 117 | test('comments', function (t) { 118 | var result = html`
    ` 119 | t.equal(result.outerHTML, '
    ', 'created comment') 120 | t.end() 121 | }) 122 | 123 | test('style', function (t) { 124 | t.plan(2) 125 | var name = 'test' 126 | var result = html`

    Hey ${name.toUpperCase()}, This is a card!!!

    ` 127 | t.equal(result.style.color, 'red', 'set style color on parent') 128 | t.equal(result.querySelector('span').style.color, 'blue', 'set style color on child') 129 | t.end() 130 | }) 131 | 132 | test('adjacent text nodes', function (t) { 133 | t.plan(2) 134 | var who = 'world' 135 | var exclamation = ['!', ' :)'] 136 | var result = html`
    hello ${who}${exclamation}
    ` 137 | t.equal(result.childNodes.length, 1, 'should be merged') 138 | t.equal(result.outerHTML, '
    hello world! :)
    ', 'should have correct output') 139 | t.end() 140 | }) 141 | 142 | test('space in only-child text nodes', function (t) { 143 | t.plan(1) 144 | var result = html` 145 | 146 | surrounding 147 | newlines 148 | 149 | ` 150 | t.equal(result.outerHTML, 'surrounding newlines', 'should remove extra space') 151 | t.end() 152 | }) 153 | 154 | test('space between text and non-text nodes', function (t) { 155 | t.plan(1) 156 | var result = html` 157 |

    158 | whitespace 159 | is empty 160 |

    161 | ` 162 | t.equal(result.outerHTML, '

    whitespace is empty

    ', 'should have correct output') 163 | t.end() 164 | }) 165 | 166 | test('space between text followed by non-text nodes', function (t) { 167 | t.plan(1) 168 | var result = html` 169 |

    170 | whitespace 171 | is strong 172 |

    173 | ` 174 | t.equal(result.outerHTML, '

    whitespace is strong

    ', 'should have correct output') 175 | t.end() 176 | }) 177 | 178 | test('space around text surrounded by non-text nodes', function (t) { 179 | t.plan(1) 180 | var result = html` 181 |

    182 | I agree 183 | whitespace 184 | is strong 185 |

    186 | ` 187 | t.equal(result.outerHTML, '

    I agree whitespace is strong

    ', 'should have correct output') 188 | t.end() 189 | }) 190 | 191 | test('space between non-text nodes', function (t) { 192 | t.plan(1) 193 | var result = html` 194 |

    195 | whitespace 196 | is beautiful 197 |

    198 | ` 199 | t.equal(result.outerHTML, '

    whitespace is beautiful

    ', 'should have correct output') 200 | t.end() 201 | }) 202 | 203 | test('space in
    ', function (t) {
    204 |   t.plan(1)
    205 |   var result = html`
    206 |     
    207 |       whitespace is empty
    208 |     
    209 | ` 210 | t.equal(result.outerHTML, '
    \n      whitespace is empty\n    
    ', 'should preserve space') 211 | t.end() 212 | }) 213 | 214 | test('space in 220 | ` 221 | t.equal(result.outerHTML, '', 'should preserve space') 222 | t.end() 223 | }) 224 | 225 | test('for attribute is set correctly', function (t) { 226 | t.plan(1) 227 | var result = html`
    228 | 229 | 230 |
    ` 231 | t.ok(result.outerHTML.indexOf('') !== -1, 'contains for="heyo"') 232 | t.end() 233 | }) 234 | 235 | test('allow objects to be passed', function (t) { 236 | t.plan(1) 237 | var result = html`
    238 |
    hey
    239 |
    ` 240 | t.ok(result.outerHTML.indexOf('
    hey
    ') !== -1, 'contains foo="bar"') 241 | t.end() 242 | }) 243 | 244 | test('supports extended build-in elements', function (t) { 245 | t.plan(1) 246 | 247 | var originalCreateElement = document.createElement 248 | var optionsArg 249 | 250 | // this iife is a must to avoid illegal invocation type errors, caused by transformed nanohtml tests 251 | (function () { 252 | document.createElement = function () { 253 | optionsArg = arguments[1] 254 | return originalCreateElement.apply(this, arguments) 255 | } 256 | })() 257 | 258 | ;html`
    ` 259 | 260 | t.ok(typeof optionsArg === 'object' && optionsArg.is === 'my-div', 'properly passes optional extends object') 261 | 262 | // revert to original prototype method 263 | delete document.createElement 264 | 265 | t.end() 266 | }) 267 | -------------------------------------------------------------------------------- /tests/transform/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var browserify = require('browserify') 3 | var fs = require('fs') 4 | var path = require('path') 5 | 6 | var FIXTURE = path.join(__dirname, 'fixture.js') 7 | 8 | test('works', function (t) { 9 | t.plan(5) 10 | var src = 'var html = require(\'nanohtml\')\n module.exports = function (data) {\n var className = \'test\'\n return html`
    \n

    ${data}

    \n
    `\n }' // eslint-disable-line 11 | fs.writeFileSync(FIXTURE, src) 12 | var b = browserify(FIXTURE, { 13 | browserField: false, 14 | transform: path.join(__dirname, '../../') 15 | }) 16 | b.bundle(function (err, src) { 17 | fs.unlinkSync(FIXTURE) 18 | t.ifError(err, 'no error') 19 | var result = src.toString() 20 | t.ok(result.indexOf('var html = {}') !== -1, 'replaced html dependency with {}') 21 | t.ok(result.indexOf('document.createElement("h1")') !== -1, 'created an h1 tag') 22 | t.ok(result.indexOf('document.createElement("div", { is: "my-div" })') !== -1, 'created an extended build-in element') 23 | t.ok(result.indexOf('setAttribute("class", arguments[1])') !== -1, 'set a class attribute') 24 | t.end() 25 | }) 26 | }) 27 | 28 | test('works with code transpiled from Typescript with esModuleInterop', function (t) { 29 | t.plan(5) 30 | var src = '"use strict";\n var __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { "default": mod };\n };\n Object.defineProperty(exports, "__esModule", { value: true });\n const nanohtml_1 = __importDefault(require("nanohtml"));\n module.exports = function (data) {\n const className = \'test\';\n return nanohtml_1.default `
    \n

    ${data}

    \n
    \n
    `;\n };' // eslint-disable-line 31 | fs.writeFileSync(FIXTURE, src) 32 | var b = browserify(FIXTURE, { 33 | browserField: false, 34 | transform: path.join(__dirname, '../../') 35 | }) 36 | b.bundle(function (err, src) { 37 | fs.unlinkSync(FIXTURE) 38 | t.ifError(err, 'no error') 39 | var result = src.toString() 40 | t.ok(result.indexOf('const nanohtml_1 = __importDefault({})') !== -1, 'replaced html dependency with {}') 41 | t.ok(result.indexOf('document.createElement("h1")') !== -1, 'created an h1 tag') 42 | t.ok(result.indexOf('document.createElement("div", { is: "my-div" })') !== -1, 'created an extended build-in element') 43 | t.ok(result.indexOf('setAttribute("class", arguments[1])') !== -1, 'set a class attribute') 44 | t.end() 45 | }) 46 | }) 47 | 48 | test('strings + template expressions', function (t) { 49 | t.plan(2) 50 | var src = 'var html = require(\'nanohtml\')\n var className = \'test\'\n var el = html`
    `' // eslint-disable-line 51 | fs.writeFileSync(FIXTURE, src) 52 | var b = browserify(FIXTURE, { 53 | browserField: false, 54 | transform: path.join(__dirname, '../../') 55 | }) 56 | b.bundle(function (err, src) { 57 | fs.unlinkSync(FIXTURE) 58 | t.ifError(err, 'no error') 59 | var result = src.toString() 60 | t.ok(result.indexOf('nanohtml0.setAttribute("class", "before " + arguments[0] + " after")') !== -1, 'concats strings + template expressions') 61 | t.end() 62 | }) 63 | }) 64 | 65 | test('append children in the correct order', function (t) { 66 | t.plan(2) 67 | var src = 'var html = require(\'nanohtml\')\n var el = html`
    This is a test to ensure strings get appended in the correct order.
    `' // eslint-disable-line 68 | fs.writeFileSync(FIXTURE, src) 69 | var b = browserify(FIXTURE, { 70 | browserField: false, 71 | transform: path.join(__dirname, '../../') 72 | }) 73 | b.bundle(function (err, src) { 74 | fs.unlinkSync(FIXTURE) 75 | t.ifError(err, 'no error') 76 | var result = src.toString() 77 | var expected = '(nanohtml2, ["This is a ",nanohtml0," to ensure ",nanohtml1," get appended in the correct order."])' 78 | t.ok(result.indexOf(expected) !== -1, 'append children in the correct order') 79 | t.end() 80 | }) 81 | }) 82 | 83 | test('multiple values on single attribute', function (t) { 84 | t.plan(4) 85 | var src = 'var html = require(\'nanohtml\')\n var a = \'testa\'\n var b = \'testb\'\n html`
    `' // eslint-disable-line 86 | fs.writeFileSync(FIXTURE, src) 87 | var b = browserify(FIXTURE, { 88 | transform: path.join(__dirname, '../../') 89 | }) 90 | b.bundle(function (err, src) { 91 | fs.unlinkSync(FIXTURE) 92 | t.ifError(err, 'no error') 93 | var result = src.toString() 94 | t.ok(result.indexOf('arguments[0]') !== -1, 'first argument') 95 | t.ok(result.indexOf('arguments[1]') !== -1, 'second argument') 96 | t.ok(result.indexOf('(a,b)') !== -1, 'calling with both variables') 97 | t.end() 98 | }) 99 | }) 100 | 101 | test('svg', function (t) { 102 | t.plan(2) 103 | var src = 'var html = require(\'nanohtml\')\n var el = html``' // eslint-disable-line 104 | fs.writeFileSync(FIXTURE, src) 105 | var b = browserify(FIXTURE, { 106 | browserField: false, 107 | transform: path.join(__dirname, '../../') 108 | }) 109 | b.bundle(function (err, src) { 110 | fs.unlinkSync(FIXTURE) 111 | t.ifError(err, 'no error') 112 | var result = src.toString() 113 | t.ok(result.indexOf('document.createElementNS("http://www.w3.org/2000/svg", "svg")') !== -1, 'created namespaced svg element') 114 | t.end() 115 | }) 116 | }) 117 | 118 | test('xlink:href', function (t) { 119 | t.plan(2) 120 | var src = 'var html = require(\'nanohtml\')\n var el = html``' // eslint-disable-line 121 | fs.writeFileSync(FIXTURE, src) 122 | var b = browserify(FIXTURE, { 123 | browserField: false, 124 | transform: path.join(__dirname, '../../') 125 | }) 126 | b.bundle(function (err, src) { 127 | fs.unlinkSync(FIXTURE) 128 | t.iferror(err, 'no error') 129 | var result = src.toString() 130 | var match = result.indexOf('setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#cat")') !== -1 131 | t.ok(match, 'created namespaced xlink:href attribute') 132 | t.end() 133 | }) 134 | }) 135 | 136 | test('choo and friends', function (t) { 137 | t.plan(3) 138 | var src = 'const choo = require(\'choo\')\n const html = require(\'nanohtml\')\n const el1 = choo.view``\n const el2 = html``' // eslint-disable-line 139 | fs.writeFileSync(FIXTURE, src) 140 | var b = browserify(FIXTURE, { 141 | transform: path.join(__dirname, '../../') 142 | }) 143 | b.bundle(function (err, src) { 144 | fs.unlinkSync(FIXTURE) 145 | t.ifError(err, 'no error') 146 | var result = src.toString() 147 | t.ok(result.indexOf('const el1 = (function () {') !== -1, 'converted el1 to a iife') 148 | t.ok(result.indexOf('const el2 = (function () {') !== -1, 'converted el1 to a iife') 149 | t.end() 150 | }) 151 | }) 152 | 153 | test('emits error for syntax error', function (t) { 154 | var src = 'var html = require(\'nanohtml\')\n module.exports = function (data) {\n var className = (\'test\' + ) // <--- HERE\'S A SYNTAX ERROR\n return html`
    \n

    ${data}

    \n
    `\n }' // eslint-disable-line 155 | fs.writeFileSync(FIXTURE, src) 156 | var b = browserify(FIXTURE, { 157 | browserField: false, 158 | transform: path.join(__dirname, '../../') 159 | }) 160 | b.bundle(function (err, src) { 161 | t.ok(err) 162 | t.end() 163 | }) 164 | }) 165 | 166 | test('works with newer js', function (t) { 167 | t.plan(1) 168 | var src = 'const html = require(\'nanohtml\')\n async function whatever() {\n return html`
    yep
    `\n }' // eslint-disable-line 169 | fs.writeFileSync(FIXTURE, src) 170 | var b = browserify(FIXTURE, { 171 | transform: path.join(__dirname, '../../') 172 | }) 173 | b.bundle(function (err, src) { 174 | fs.unlinkSync(FIXTURE) 175 | t.ifError(err, 'no error') 176 | t.end() 177 | }) 178 | }) 179 | 180 | test('boolean attribute expression', function (t) { 181 | t.plan(1) 182 | var src = 'const html = require(\'nanohtml\')\n async function whatever() {\nvar b = "disabled"\nreturn html``\n }' // eslint-disable-line 183 | fs.writeFileSync(FIXTURE, src) 184 | var b = browserify(FIXTURE, { 185 | transform: path.join(__dirname, '../../') 186 | }) 187 | b.bundle(function (err, src) { 188 | fs.unlinkSync(FIXTURE) 189 | t.ifError(err, 'no error') 190 | t.end() 191 | }) 192 | }) 193 | 194 | test('babel-compiled template literals', function (t) { 195 | t.plan(3) 196 | fs.writeFileSync(FIXTURE, ` 197 | var html = require('nanohtml') 198 | 199 | html\`
    \${xyz}
    \` 200 | `) 201 | var b = browserify(FIXTURE, { 202 | transform: [ 203 | ['babelify', { 204 | plugins: ['transform-es2015-template-literals'] 205 | }], 206 | path.join(__dirname, '../../') 207 | ] 208 | }) 209 | b.bundle(function (err, src) { 210 | fs.unlinkSync(FIXTURE) 211 | t.ifError(err) 212 | t.ok(src.indexOf('document.createElement("div")') !== -1, 'created a tag') 213 | t.ok(src.indexOf('\${xyz}
    \` 225 | `) 226 | 227 | var b = browserify(FIXTURE, { 228 | transform: [ 229 | ['bubleify', { 230 | transforms: { 231 | dangerousTaggedTemplateString: true 232 | } 233 | }], 234 | path.join(__dirname, '../../') 235 | ] 236 | }) 237 | b.bundle(function (err, src) { 238 | fs.unlinkSync(FIXTURE) 239 | t.ifError(err) 240 | t.ok(src.indexOf('document.createElement("div")') !== -1, 'created a tag') 241 | t.end() 242 | }) 243 | }) 244 | 245 | test('generates source maps in debug mode', function (t) { 246 | t.plan(2) 247 | fs.writeFileSync(FIXTURE, ` 248 | var html = require('nanohtml') 249 | var el = html\`title\` 250 | html\` 251 |
    252 |

    \${el}

    253 |
    254 | \` 255 | `) 256 | 257 | var b = browserify(FIXTURE, { 258 | debug: true, 259 | transform: path.join(__dirname, '../../') 260 | }) 261 | b.bundle(function (err, src) { 262 | fs.unlinkSync(FIXTURE) 263 | t.ifError(err) 264 | t.ok(src.indexOf('//# sourceMappingURL=') !== -1, 'has source map') 265 | t.end() 266 | }) 267 | }) 268 | 269 | test('accepts input source maps in debug mode', function (t) { 270 | t.plan(2) 271 | fs.writeFileSync(FIXTURE, ` 272 | var html = require('nanohtml') 273 | var el = html\`title\` 274 | html\` 275 |
    276 |

    \${el}

    277 |
    278 | \` 279 | `) 280 | 281 | var b = browserify(FIXTURE, { 282 | debug: true, 283 | transform: [ 284 | ['babelify', { 285 | plugins: ['transform-es2015-template-literals'] 286 | }], 287 | path.join(__dirname, '../../') 288 | ] 289 | }) 290 | b.bundle(function (err, src) { 291 | fs.unlinkSync(FIXTURE) 292 | t.ifError(err) 293 | t.ok(src.indexOf('//# sourceMappingURL=') !== -1, 'has source map') 294 | t.end() 295 | }) 296 | }) 297 | -------------------------------------------------------------------------------- /lib/browserify-transform.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var isMemberExpression = require('estree-is-member-expression') 4 | var convertSourceMap = require('convert-source-map') 5 | var transformAst = require('transform-ast') 6 | var through = require('through2') 7 | var hyperx = require('hyperx') 8 | var acorn = require('acorn-node') 9 | var path = require('path') 10 | var SVG_TAGS = require('./svg-tags') 11 | var SUPPORTED_VIEWS = require('./supported-views') 12 | 13 | var DELIM = '~!@|@|@!~' 14 | var VARNAME = 'nanohtml' 15 | var SVGNS = 'http://www.w3.org/2000/svg' 16 | var XLINKNS = '"http://www.w3.org/1999/xlink"' 17 | var BOOL_PROPS = require('./bool-props').reduce(function (o, key) { 18 | o[key] = 1 19 | return o 20 | }, {}) 21 | // Props that need to be set directly rather than with el.setAttribute() 22 | var DIRECT_PROPS = require('./direct-props').reduce(function (o, key) { 23 | o[key] = 1 24 | return o 25 | }, {}) 26 | 27 | module.exports = function yoYoify (file, opts) { 28 | if (/\.json$/.test(file)) return through() 29 | var bufs = [] 30 | var viewVariables = [] 31 | var babelTemplateObjects = Object.create(null) 32 | var bubleTemplateObjects = Object.create(null) 33 | return through(write, end) 34 | function write (buf, enc, next) { 35 | bufs.push(buf) 36 | next() 37 | } 38 | function end (cb) { 39 | var src = Buffer.concat(bufs).toString('utf8') 40 | var flags = (opts && opts._flags) || {} 41 | var basedir = flags.basedir || process.cwd() 42 | var filename = path.relative(basedir, file) 43 | var res 44 | try { 45 | res = transformAst(src, { parser: acorn, inputFilename: filename }, walk) 46 | if (flags.debug) { 47 | var sm = convertSourceMap.fromObject(res.map).toComment() 48 | res = res.toString() + '\n' + sm + '\n' 49 | } else { 50 | res = res.toString() 51 | } 52 | } catch (err) { 53 | return cb(err) 54 | } 55 | this.push(res) 56 | this.push(null) 57 | } 58 | function walk (node) { 59 | if (isSupportedView(node)) { 60 | if (node.arguments[0].value === 'bel' || 61 | node.arguments[0].value === 'choo/html' || 62 | node.arguments[0].value === 'nanohtml') { 63 | // html and choo/html have no other exports that may be used 64 | node.edit.update('{}') 65 | } 66 | if (node.parent.type === 'VariableDeclarator') { 67 | viewVariables.push(node.parent.id.name) 68 | // Typescript Synthetic Default Imports 69 | } else if (node.parent.type === 'CallExpression' && node.parent.callee.name === '__importDefault' && node.parent.parent.type === 'VariableDeclarator') { 70 | viewVariables.push(node.parent.parent.id.name) 71 | } 72 | } 73 | 74 | if (node.type === 'VariableDeclarator' && node.init && isBabelTemplateDefinition(node.init)) { 75 | // Babel generates helper calls like 76 | // _taggedTemplateLiteral([""], [""]) 77 | // The first parameter is the `cooked` template literal parts, and the second parameter is the `raw` 78 | // template literal parts. 79 | // We just pick the cooked parts. 80 | babelTemplateObjects[node.id.name] = node.init.arguments[0] 81 | } 82 | 83 | if (node.type === 'VariableDeclarator' && node.init && isBubleTemplateDefinition(node.init)) { 84 | // Buble generates helper calls like 85 | // Object.freeze([":host .class { color: hotpink; }"]) 86 | bubleTemplateObjects[node.id.name] = node.init.arguments[0] 87 | } 88 | 89 | if (node.type === 'TemplateLiteral' && node.parent.tag) { 90 | var name = node.parent.tag.name || (node.parent.tag.object && node.parent.tag.object.name) 91 | if (viewVariables.indexOf(name) !== -1) { 92 | processNode(node.parent, [ node.quasis.map(cooked) ].concat(node.expressions.map(expr))) 93 | } 94 | } 95 | if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && viewVariables.indexOf(node.callee.name) !== -1) { 96 | var template, expressions 97 | if (node.arguments[0] && node.arguments[0].type === 'ArrayExpression') { 98 | // Detect transpiled template strings like: 99 | // html([""], {id: "test"}) 100 | // Emitted by Buble < 0.19. 101 | template = node.arguments[0].elements.map(function (part) { return part.value }) 102 | expressions = node.arguments.slice(1).map(expr) 103 | processNode(node, [ template ].concat(expressions)) 104 | } else if (node.arguments[0] && node.arguments[0].type === 'Identifier') { 105 | // Detect transpiled template strings like: 106 | // html(_templateObject, {id: "test"}) 107 | // Emitted by Babel and buble. 108 | var arg = node.arguments[0].name 109 | var templateObject = babelTemplateObjects[arg] || bubleTemplateObjects[arg] 110 | if (!templateObject) { 111 | return // Does something else I guess. probably a manual call and not a tagged template string 112 | } 113 | template = templateObject.elements.map(function (part) { return part.value }) 114 | expressions = node.arguments.slice(1).map(expr) 115 | processNode(node, [ template ].concat(expressions)) 116 | 117 | // Remove the _taggedTemplateLiteral helper call 118 | templateObject.parent.edit.update('0') 119 | } 120 | } 121 | } 122 | } 123 | 124 | function processNode (node, args) { 125 | var resultArgs = [] 126 | var argCount = 0 127 | var tagCount = 0 128 | 129 | var needsAc = false 130 | var needsSa = false 131 | 132 | function createElement (tag, props, children) { 133 | var res = [] 134 | 135 | var elname = VARNAME + tagCount 136 | tagCount++ 137 | 138 | if (tag === '!--') { 139 | return DELIM + [elname, 'var ' + elname + ' = document.createComment(' + JSON.stringify(props.comment) + ')', null].join(DELIM) + DELIM 140 | } 141 | 142 | if (tag === 'nanohtml-fragment') { 143 | res.push('var ' + elname + ' = document.createDocumentFragment()') 144 | } else { 145 | // Whether this element needs a namespace 146 | var namespace = props.namespace 147 | if (!namespace && SVG_TAGS.indexOf(tag) !== -1) { 148 | namespace = SVGNS 149 | } 150 | 151 | // Whether this element is extended 152 | var isCustomElement = props.is 153 | delete props.is 154 | 155 | // Create the element 156 | if (namespace) { 157 | if (isCustomElement) { 158 | res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })') 159 | } else { 160 | res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')') 161 | } 162 | } else if (isCustomElement) { 163 | res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ', { is: ' + JSON.stringify(isCustomElement) + ' })') 164 | } else { 165 | res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ')') 166 | } 167 | } 168 | 169 | function addAttr (to, key, val) { 170 | // Normalize className 171 | if (key.toLowerCase() === '"classname"') { 172 | key = '"class"' 173 | } 174 | // The for attribute gets transformed to htmlFor, but we just set as for 175 | if (key === '"htmlFor"') { 176 | key = '"for"' 177 | } 178 | // If a property is boolean, set itself to the key 179 | if (BOOL_PROPS[key.slice(1, -1)]) { 180 | if (val.slice(0, 9) === 'arguments') { 181 | if (namespace) { 182 | res.push('if (' + val + ' && ' + key + ') ' + to + '.setAttributeNS(null, ' + key + ', ' + key + ')') 183 | } else if (DIRECT_PROPS[key.slice(1, -1)]) { 184 | res.push('if (' + val + ' && ' + key + ') ' + to + '[' + key + '] = true') 185 | } else { 186 | res.push('if (' + val + ' && ' + key + ') ' + to + '.setAttribute(' + key + ', ' + key + ')') 187 | } 188 | return 189 | } else { 190 | if (val === 'true') val = key 191 | else if (val === 'false') return 192 | } 193 | } 194 | 195 | if (key.slice(1, 3) === 'on' || DIRECT_PROPS[key.slice(1, -1)]) { 196 | res.push(to + '[' + key + '] = ' + val) 197 | } else { 198 | if (key === '"xlink:href"') { 199 | res.push(to + '.setAttributeNS(' + XLINKNS + ', ' + key + ', ' + val + ')') 200 | } else if (namespace && key.slice(0, 1) === '"') { 201 | if (!/^xmlns($|:)/i.test(key.slice(1, -1))) { 202 | // skip xmlns definitions 203 | res.push(to + '.setAttributeNS(null, ' + key + ', ' + val + ')') 204 | } 205 | } else if (namespace) { 206 | res.push('if (' + key + ') ' + to + '.setAttributeNS(null, ' + key + ', ' + val + ')') 207 | } else if (key.slice(0, 1) === '"') { 208 | res.push(to + '.setAttribute(' + key + ', ' + val + ')') 209 | } else { 210 | needsSa = true 211 | res.push('sa(' + to + ', ' + key + ', ' + val + ')') 212 | } 213 | } 214 | } 215 | 216 | // Add properties to element 217 | Object.keys(props).forEach(function (key) { 218 | var prop = props[key] 219 | var ksrcs = getSourceParts(key) 220 | var srcs = getSourceParts(prop) 221 | var k, val 222 | if (srcs) { 223 | val = '' 224 | srcs.forEach(function (src, index) { 225 | if (src.arg) { 226 | if (index > 0) val += ' + ' 227 | if (src.before) val += JSON.stringify(src.before) + ' + ' 228 | val += 'arguments[' + argCount + ']' 229 | if (src.after) val += ' + ' + JSON.stringify(src.after) 230 | resultArgs.push(src.arg) 231 | argCount++ 232 | } 233 | }) 234 | } else { 235 | val = JSON.stringify(prop) 236 | } 237 | if (ksrcs) { 238 | k = '' 239 | ksrcs.forEach(function (src, index) { 240 | if (src.arg) { 241 | if (index > 0) val += ' + ' 242 | if (src.before) val += JSON.stringify(src.before) + ' + ' 243 | k += 'arguments[' + argCount + ']' 244 | if (src.after) k += ' + ' + JSON.stringify(src.after) 245 | resultArgs.push(src.arg) 246 | argCount++ 247 | } 248 | }) 249 | } else { 250 | k = JSON.stringify(key) 251 | } 252 | addAttr(elname, k, val) 253 | }) 254 | 255 | if (Array.isArray(children)) { 256 | var childs = [] 257 | children.forEach(function (child) { 258 | var srcs = getSourceParts(child) 259 | if (srcs) { 260 | var src = srcs[0] 261 | if (src.src) { 262 | res.push(src.src) 263 | } 264 | if (src.name) { 265 | childs.push(src.name) 266 | } 267 | if (src.arg) { 268 | var argname = 'arguments[' + argCount + ']' 269 | resultArgs.push(src.arg) 270 | argCount++ 271 | childs.push(argname) 272 | } 273 | } else { 274 | childs.push(JSON.stringify(child)) 275 | } 276 | }) 277 | if (childs.length > 0) { 278 | needsAc = true 279 | res.push('ac(' + elname + ', [' + childs.join(',') + '])') 280 | } 281 | } 282 | 283 | // Return delim'd parts as a child 284 | // return [elname, res] 285 | return DELIM + [elname, res.join('\n'), null].join(DELIM) + DELIM 286 | } 287 | 288 | var hx = hyperx(createElement, { 289 | comments: true, 290 | createFragment: function (nodes) { 291 | return createElement('nanohtml-fragment', {}, nodes) 292 | } 293 | }) 294 | 295 | // Run through hyperx 296 | var res = hx.apply(null, args) 297 | 298 | // Pull out the final parts and wrap in a closure with arguments 299 | var src = getSourceParts(res) 300 | if (src && src[0].src) { 301 | var params = resultArgs.join(',') 302 | 303 | node.edit.update('(function () {' + 304 | (needsAc ? '\n var ac = require(\'' + path.resolve(__dirname, 'append-child.js').replace(/\\/g, '\\\\') + '\')' : '') + 305 | (needsSa ? '\n var sa = require(\'' + path.resolve(__dirname, 'set-attribute.js').replace(/\\/g, '\\\\') + '\')' : '') + 306 | '\n ' + src[0].src + '\n return ' + src[0].name + '\n }(' + params + '))') 307 | } 308 | } 309 | 310 | function isSupportedView (node) { 311 | return (node.type === 'CallExpression' && 312 | node.callee && node.callee.name === 'require' && 313 | node.arguments.length === 1 && 314 | SUPPORTED_VIEWS.indexOf(node.arguments[0].value) !== -1) 315 | } 316 | 317 | function isBabelTemplateDefinition (node) { 318 | return node.type === 'CallExpression' && 319 | node.callee.type === 'Identifier' && node.callee.name === '_taggedTemplateLiteral' 320 | } 321 | 322 | function isBubleTemplateDefinition (node) { 323 | return node.type === 'CallExpression' && 324 | isMemberExpression(node.callee, 'Object.freeze') && 325 | node.arguments[0] && node.arguments[0].type === 'ArrayExpression' && 326 | node.arguments[0].elements.every(function (el) { return el.type === 'Literal' }) 327 | } 328 | 329 | function cooked (node) { return node.value.cooked } 330 | function expr (ex, idx) { 331 | return DELIM + [null, null, ex.source()].join(DELIM) + DELIM 332 | } 333 | function getSourceParts (str) { 334 | if (typeof str !== 'string') return false 335 | if (str.indexOf(DELIM) === -1) return false 336 | var parts = str.split(DELIM) 337 | 338 | var chunk = parts.splice(0, 5) 339 | var arr = [{ 340 | before: chunk[0], 341 | name: chunk[1], 342 | src: chunk[2], 343 | arg: chunk[3], 344 | after: chunk[4] 345 | }] 346 | while (parts.length > 0) { 347 | chunk = parts.splice(0, 4) 348 | arr.push({ 349 | before: '', 350 | name: chunk[0], 351 | src: chunk[1], 352 | arg: chunk[2], 353 | after: chunk[3] 354 | }) 355 | } 356 | 357 | return arr 358 | } 359 | -------------------------------------------------------------------------------- /tests/browser/events.js: -------------------------------------------------------------------------------- 1 | let test = require('tape') 2 | if (typeof window !== 'undefined') { 3 | var document = window.document 4 | var html = require('../../') 5 | } else { 6 | var nano = require('./html') 7 | document = nano.document 8 | html = nano.html 9 | } 10 | 11 | /* Note: 12 | Failing tests have been commented. They include the following: 13 | onfocusin 14 | onfocusout 15 | ontouchcancel 16 | ontouchend 17 | ontouchmove 18 | ontouchstart 19 | onunload 20 | */ 21 | 22 | function raiseEvent (element, eventName) { 23 | let event = document.createEvent('Event') 24 | event.initEvent(eventName, true, true) 25 | element.dispatchEvent(event) 26 | } 27 | 28 | test('should have onabort events(html attribute) ', function (t) { 29 | t.plan(1) 30 | let expectationMet = false 31 | let res = html`` 32 | 33 | raiseEvent(res, 'abort') 34 | 35 | function pass (e) { 36 | e.preventDefault() 37 | expectationMet = true 38 | } 39 | 40 | t.equal(expectationMet, true, 'result was expected') 41 | }) 42 | test('should have onblur events(html attribute) ', function (t) { 43 | t.plan(1) 44 | let expectationMet = false 45 | let res = html`` 46 | 47 | raiseEvent(res, 'blur') 48 | 49 | function pass (e) { 50 | e.preventDefault() 51 | expectationMet = true 52 | } 53 | 54 | t.equal(expectationMet, true, 'result was expected') 55 | }) 56 | test('should have onchange events(html attribute) ', function (t) { 57 | t.plan(1) 58 | let expectationMet = false 59 | let res = html`` 60 | 61 | raiseEvent(res, 'change') 62 | 63 | function pass (e) { 64 | e.preventDefault() 65 | expectationMet = true 66 | } 67 | 68 | t.equal(expectationMet, true, 'result was expected') 69 | }) 70 | test('should have onclick events(html attribute) ', function (t) { 71 | t.plan(1) 72 | let expectationMet = false 73 | let res = html`` 74 | 75 | raiseEvent(res, 'click') 76 | 77 | function pass (e) { 78 | e.preventDefault() 79 | expectationMet = true 80 | } 81 | 82 | t.equal(expectationMet, true, 'result was expected') 83 | }) 84 | test('should have oncontextmenu events(html attribute) ', function (t) { 85 | t.plan(1) 86 | let expectationMet = false 87 | let res = html`` 88 | 89 | raiseEvent(res, 'contextmenu') 90 | 91 | function pass (e) { 92 | e.preventDefault() 93 | expectationMet = true 94 | } 95 | 96 | t.equal(expectationMet, true, 'result was expected') 97 | }) 98 | test('should have ondblclick events(html attribute) ', function (t) { 99 | t.plan(1) 100 | let expectationMet = false 101 | let res = html`` 102 | 103 | raiseEvent(res, 'dblclick') 104 | 105 | function pass (e) { 106 | e.preventDefault() 107 | expectationMet = true 108 | } 109 | 110 | t.equal(expectationMet, true, 'result was expected') 111 | }) 112 | test('should have ondrag events(html attribute) ', function (t) { 113 | t.plan(1) 114 | let expectationMet = false 115 | let res = html`` 116 | 117 | raiseEvent(res, 'drag') 118 | 119 | function pass (e) { 120 | e.preventDefault() 121 | expectationMet = true 122 | } 123 | 124 | t.equal(expectationMet, true, 'result was expected') 125 | }) 126 | test('should have ondragend events(html attribute) ', function (t) { 127 | t.plan(1) 128 | let expectationMet = false 129 | let res = html`` 130 | 131 | raiseEvent(res, 'dragend') 132 | 133 | function pass (e) { 134 | e.preventDefault() 135 | expectationMet = true 136 | } 137 | 138 | t.equal(expectationMet, true, 'result was expected') 139 | }) 140 | test('should have ondragenter events(html attribute) ', function (t) { 141 | t.plan(1) 142 | let expectationMet = false 143 | let res = html`` 144 | 145 | raiseEvent(res, 'dragenter') 146 | 147 | function pass (e) { 148 | e.preventDefault() 149 | expectationMet = true 150 | } 151 | 152 | t.equal(expectationMet, true, 'result was expected') 153 | }) 154 | test('should have ondragleave events(html attribute) ', function (t) { 155 | t.plan(1) 156 | let expectationMet = false 157 | let res = html`` 158 | 159 | raiseEvent(res, 'dragleave') 160 | 161 | function pass (e) { 162 | e.preventDefault() 163 | expectationMet = true 164 | } 165 | 166 | t.equal(expectationMet, true, 'result was expected') 167 | }) 168 | test('should have ondragover events(html attribute) ', function (t) { 169 | t.plan(1) 170 | let expectationMet = false 171 | let res = html`` 172 | 173 | raiseEvent(res, 'dragover') 174 | 175 | function pass (e) { 176 | e.preventDefault() 177 | expectationMet = true 178 | } 179 | 180 | t.equal(expectationMet, true, 'result was expected') 181 | }) 182 | test('should have ondragstart events(html attribute) ', function (t) { 183 | t.plan(1) 184 | let expectationMet = false 185 | let res = html`` 186 | 187 | raiseEvent(res, 'dragstart') 188 | 189 | function pass (e) { 190 | e.preventDefault() 191 | expectationMet = true 192 | } 193 | 194 | t.equal(expectationMet, true, 'result was expected') 195 | }) 196 | test('should have ondrop events(html attribute) ', function (t) { 197 | t.plan(1) 198 | let expectationMet = false 199 | let res = html`` 200 | 201 | raiseEvent(res, 'drop') 202 | 203 | function pass (e) { 204 | e.preventDefault() 205 | expectationMet = true 206 | } 207 | 208 | t.equal(expectationMet, true, 'result was expected') 209 | }) 210 | test('should have onerror events(html attribute) ', function (t) { 211 | t.plan(1) 212 | let expectationMet = false 213 | let res = html`` 214 | 215 | raiseEvent(res, 'error') 216 | 217 | function pass (e) { 218 | e.preventDefault() 219 | expectationMet = true 220 | } 221 | 222 | t.equal(expectationMet, true, 'result was expected') 223 | }) 224 | test('should have onfocus events(html attribute) ', function (t) { 225 | t.plan(1) 226 | let expectationMet = false 227 | let res = html`` 228 | 229 | raiseEvent(res, 'focus') 230 | 231 | function pass (e) { 232 | e.preventDefault() 233 | expectationMet = true 234 | } 235 | 236 | t.equal(expectationMet, true, 'result was expected') 237 | }) 238 | /* test('should have onfocusin events(html attribute) ', function (t) { 239 | t.plan(1) 240 | let expectationMet = false 241 | let res = html`` 242 | 243 | raiseEvent(res, 'focusin') 244 | 245 | function pass (e) { 246 | e.preventDefault() 247 | expectationMet = true 248 | } 249 | 250 | t.equal(expectationMet, true, 'result was expected') 251 | }) */ 252 | /* test('should have onfocusout events(html attribute) ', function (t) { 253 | t.plan(1) 254 | let expectationMet = false 255 | let res = html`` 256 | 257 | raiseEvent(res, 'focusout') 258 | 259 | function pass (e) { 260 | e.preventDefault() 261 | expectationMet = true 262 | } 263 | 264 | t.equal(expectationMet, true, 'result was expected') 265 | }) */ 266 | test('should have oninput events(html attribute) ', function (t) { 267 | t.plan(1) 268 | let expectationMet = false 269 | let res = html`` 270 | 271 | raiseEvent(res, 'input') 272 | 273 | function pass (e) { 274 | e.preventDefault() 275 | expectationMet = true 276 | } 277 | 278 | t.equal(expectationMet, true, 'result was expected') 279 | }) 280 | test('should have onkeydown events(html attribute) ', function (t) { 281 | t.plan(1) 282 | let expectationMet = false 283 | let res = html`` 284 | 285 | raiseEvent(res, 'keydown') 286 | 287 | function pass (e) { 288 | e.preventDefault() 289 | expectationMet = true 290 | } 291 | 292 | t.equal(expectationMet, true, 'result was expected') 293 | }) 294 | test('should have onkeypress events(html attribute) ', function (t) { 295 | t.plan(1) 296 | let expectationMet = false 297 | let res = html`` 298 | 299 | raiseEvent(res, 'keypress') 300 | 301 | function pass (e) { 302 | e.preventDefault() 303 | expectationMet = true 304 | } 305 | 306 | t.equal(expectationMet, true, 'result was expected') 307 | }) 308 | test('should have onkeyup events(html attribute) ', function (t) { 309 | t.plan(1) 310 | let expectationMet = false 311 | let res = html`` 312 | 313 | raiseEvent(res, 'keyup') 314 | 315 | function pass (e) { 316 | e.preventDefault() 317 | expectationMet = true 318 | } 319 | 320 | t.equal(expectationMet, true, 'result was expected') 321 | }) 322 | test('should have onmousedown events(html attribute) ', function (t) { 323 | t.plan(1) 324 | let expectationMet = false 325 | let res = html`` 326 | 327 | raiseEvent(res, 'mousedown') 328 | 329 | function pass (e) { 330 | e.preventDefault() 331 | expectationMet = true 332 | } 333 | 334 | t.equal(expectationMet, true, 'result was expected') 335 | }) 336 | test('should have onmouseenter events(html attribute) ', function (t) { 337 | t.plan(1) 338 | let expectationMet = false 339 | let res = html`` 340 | 341 | raiseEvent(res, 'mouseenter') 342 | 343 | function pass (e) { 344 | e.preventDefault() 345 | expectationMet = true 346 | } 347 | 348 | t.equal(expectationMet, true, 'result was expected') 349 | }) 350 | test('should have onmouseleave events(html attribute) ', function (t) { 351 | t.plan(1) 352 | let expectationMet = false 353 | let res = html`` 354 | 355 | raiseEvent(res, 'mouseleave') 356 | 357 | function pass (e) { 358 | e.preventDefault() 359 | expectationMet = true 360 | } 361 | 362 | t.equal(expectationMet, true, 'result was expected') 363 | }) 364 | test('should have onmousemove events(html attribute) ', function (t) { 365 | t.plan(1) 366 | let expectationMet = false 367 | let res = html`` 368 | 369 | raiseEvent(res, 'mousemove') 370 | 371 | function pass (e) { 372 | e.preventDefault() 373 | expectationMet = true 374 | } 375 | 376 | t.equal(expectationMet, true, 'result was expected') 377 | }) 378 | test('should have onmouseout events(html attribute) ', function (t) { 379 | t.plan(1) 380 | let expectationMet = false 381 | let res = html`` 382 | 383 | raiseEvent(res, 'mouseout') 384 | 385 | function pass (e) { 386 | e.preventDefault() 387 | expectationMet = true 388 | } 389 | 390 | t.equal(expectationMet, true, 'result was expected') 391 | }) 392 | test('should have onmouseover events(html attribute) ', function (t) { 393 | t.plan(1) 394 | let expectationMet = false 395 | let res = html`` 396 | 397 | raiseEvent(res, 'mouseover') 398 | 399 | function pass (e) { 400 | e.preventDefault() 401 | expectationMet = true 402 | } 403 | 404 | t.equal(expectationMet, true, 'result was expected') 405 | }) 406 | test('should have onmouseup events(html attribute) ', function (t) { 407 | t.plan(1) 408 | let expectationMet = false 409 | let res = html`` 410 | 411 | raiseEvent(res, 'mouseup') 412 | 413 | function pass (e) { 414 | e.preventDefault() 415 | expectationMet = true 416 | } 417 | 418 | t.equal(expectationMet, true, 'result was expected') 419 | }) 420 | test('should have onreset events(html attribute) ', function (t) { 421 | t.plan(1) 422 | let expectationMet = false 423 | let res = html`` 424 | 425 | raiseEvent(res, 'reset') 426 | 427 | function pass (e) { 428 | e.preventDefault() 429 | expectationMet = true 430 | } 431 | 432 | t.equal(expectationMet, true, 'result was expected') 433 | }) 434 | test('should have onresize events(html attribute) ', function (t) { 435 | t.plan(1) 436 | let expectationMet = false 437 | let res = html`` 438 | 439 | raiseEvent(res, 'resize') 440 | 441 | function pass (e) { 442 | e.preventDefault() 443 | expectationMet = true 444 | } 445 | 446 | t.equal(expectationMet, true, 'result was expected') 447 | }) 448 | test('should have onscroll events(html attribute) ', function (t) { 449 | t.plan(1) 450 | let expectationMet = false 451 | let res = html`` 452 | 453 | raiseEvent(res, 'scroll') 454 | 455 | function pass (e) { 456 | e.preventDefault() 457 | expectationMet = true 458 | } 459 | 460 | t.equal(expectationMet, true, 'result was expected') 461 | }) 462 | test('should have onselect events(html attribute) ', function (t) { 463 | t.plan(1) 464 | let expectationMet = false 465 | let res = html`` 466 | 467 | raiseEvent(res, 'select') 468 | 469 | function pass (e) { 470 | e.preventDefault() 471 | expectationMet = true 472 | } 473 | 474 | t.equal(expectationMet, true, 'result was expected') 475 | }) 476 | test('should have onsubmit events(html attribute) ', function (t) { 477 | t.plan(1) 478 | let expectationMet = false 479 | let res = html`` 480 | 481 | raiseEvent(res, 'submit') 482 | 483 | function pass (e) { 484 | e.preventDefault() 485 | expectationMet = true 486 | } 487 | 488 | t.equal(expectationMet, true, 'result was expected') 489 | }) 490 | test('should have ontouchcancel events(html attribute) ', { skip: true }, function (t) { 491 | t.plan(1) 492 | let expectationMet = false 493 | let res = html`` 494 | 495 | raiseEvent(res, 'touchcancel') 496 | 497 | function pass (e) { 498 | e.preventDefault() 499 | expectationMet = true 500 | } 501 | 502 | t.equal(expectationMet, true, 'result was expected') 503 | }) 504 | test('should have ontouchend events(html attribute) ', { skip: true }, function (t) { 505 | t.plan(1) 506 | let expectationMet = false 507 | let res = html`` 508 | 509 | raiseEvent(res, 'touchend') 510 | 511 | function pass (e) { 512 | e.preventDefault() 513 | expectationMet = true 514 | } 515 | 516 | t.equal(expectationMet, true, 'result was expected') 517 | }) 518 | test('should have ontouchmove events(html attribute) ', { skip: true }, function (t) { 519 | t.plan(1) 520 | let expectationMet = false 521 | let res = html`` 522 | 523 | raiseEvent(res, 'touchmove') 524 | 525 | function pass (e) { 526 | e.preventDefault() 527 | expectationMet = true 528 | } 529 | 530 | t.equal(expectationMet, true, 'result was expected') 531 | }) 532 | test('should have ontouchstart events(html attribute) ', { skip: true }, function (t) { 533 | t.plan(1) 534 | let expectationMet = false 535 | let res = html`` 536 | 537 | raiseEvent(res, 'touchstart') 538 | 539 | function pass (e) { 540 | e.preventDefault() 541 | expectationMet = true 542 | } 543 | 544 | t.equal(expectationMet, true, 'result was expected') 545 | }) 546 | test('should have onunload events(html attribute) ', { skip: true }, function (t) { 547 | t.plan(1) 548 | let expectationMet = false 549 | let res = html`` 550 | 551 | raiseEvent(res, 'unload') 552 | 553 | function pass (e) { 554 | e.preventDefault() 555 | expectationMet = true 556 | } 557 | 558 | t.equal(expectationMet, true, 'result was expected') 559 | }) 560 | -------------------------------------------------------------------------------- /lib/babel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const camelCase = require('camel-case') 4 | const hyperx = require('hyperx') 5 | const SVG_TAGS = require('./svg-tags') 6 | const BOOL_PROPS = require('./bool-props') 7 | const DIRECT_PROPS = require('./direct-props') 8 | const SUPPORTED_VIEWS = require('./supported-views') 9 | 10 | const SVGNS = 'http://www.w3.org/2000/svg' 11 | const XLINKNS = 'http://www.w3.org/1999/xlink' 12 | 13 | /** 14 | * Try to return a nice variable name for an element based on its HTML id, 15 | * classname, or tagname. 16 | */ 17 | function getElementName (props, tag) { 18 | if (typeof props.id === 'string' && !placeholderRe.test(props.id)) { 19 | return camelCase(props.id) 20 | } 21 | if (typeof props.className === 'string' && !placeholderRe.test(props.className)) { 22 | return camelCase(props.className.split(' ')[0]) 23 | } 24 | return tag || 'nanohtml' 25 | } 26 | 27 | /** 28 | * Regex for detecting placeholders. 29 | */ 30 | const placeholderRe = /\0(\d+)\0/g 31 | 32 | /** 33 | * Get a placeholder string for a numeric ID. 34 | */ 35 | const getPlaceholder = (i) => `\u0000${i}\u0000` 36 | 37 | /** 38 | * Remove a binding and its import or require() call from the file. 39 | */ 40 | function removeBindingImport (binding) { 41 | const path = binding.path 42 | if (path.parentPath.isImportDeclaration() && 43 | // Remove the entire Import if this is the only imported binding. 44 | path.parentPath.node.specifiers.length === 1) { 45 | path.parentPath.remove() 46 | } else { 47 | path.remove() 48 | } 49 | } 50 | 51 | module.exports = (babel) => { 52 | const t = babel.types 53 | 54 | /** 55 | * Returns an object which specifies the custom elements by which a built-in is extended. 56 | */ 57 | const createExtendsObjectExpression = (is) => 58 | t.objectExpression([t.objectProperty(t.identifier('is'), t.stringLiteral(is))]) 59 | 60 | /** 61 | * Returns a node that creates a namespaced HTML element. 62 | */ 63 | const createNsElement = (ns, tag) => 64 | t.callExpression( 65 | t.memberExpression(t.identifier('document'), t.identifier('createElementNS')), 66 | [ns, t.stringLiteral(tag)] 67 | ) 68 | 69 | /** 70 | * Returns a node that creates a extended namespaced HTML element. 71 | */ 72 | const createNsCustomBuiltIn = (ns, tag, is) => 73 | t.callExpression( 74 | t.memberExpression(t.identifier('document'), t.identifier('createElementNS')), 75 | [ns, t.stringLiteral(tag), createExtendsObjectExpression(is)] 76 | ) 77 | 78 | /** 79 | * Returns a node that creates an element. 80 | */ 81 | const createElement = (tag) => 82 | t.callExpression( 83 | t.memberExpression(t.identifier('document'), t.identifier('createElement')), 84 | [t.stringLiteral(tag)] 85 | ) 86 | 87 | /** 88 | * Returns a node that creates an extended element. 89 | */ 90 | const createCustomBuiltIn = (tag, is) => 91 | t.callExpression( 92 | t.memberExpression(t.identifier('document'), t.identifier('createElement')), 93 | [t.stringLiteral(tag), createExtendsObjectExpression(is)] 94 | ) 95 | 96 | /** 97 | * Returns a node that creates a comment. 98 | */ 99 | const createComment = (text) => 100 | t.callExpression( 101 | t.memberExpression(t.identifier('document'), t.identifier('createComment')), 102 | [t.stringLiteral(text)] 103 | ) 104 | 105 | /** 106 | * Returns a node that creates a fragment. 107 | */ 108 | const createFragment = (text) => 109 | t.callExpression( 110 | t.memberExpression(t.identifier('document'), t.identifier('createDocumentFragment')), 111 | [] 112 | ) 113 | 114 | /** 115 | * Returns a node that sets a DOM property. 116 | */ 117 | const setDomProperty = (id, prop, value) => 118 | t.assignmentExpression('=', 119 | t.memberExpression(id, t.identifier(prop)), 120 | value) 121 | 122 | /** 123 | * Returns a node that sets a DOM attribute. 124 | */ 125 | const setDomAttribute = (id, attr, value) => 126 | t.callExpression( 127 | t.memberExpression(id, t.identifier('setAttribute')), 128 | [t.stringLiteral(attr), value]) 129 | 130 | const setDomAttributeNS = (id, attr, value, ns = t.nullLiteral()) => 131 | t.callExpression( 132 | t.memberExpression(id, t.identifier('setAttributeNS')), 133 | [ns, t.stringLiteral(attr), value]) 134 | 135 | /** 136 | * Returns a node that sets a boolean DOM attribute. 137 | */ 138 | const setBooleanAttribute = (id, attr, value) => 139 | t.logicalExpression('&&', value, 140 | setDomAttribute(id, attr, t.stringLiteral(attr))) 141 | 142 | /** 143 | * Returns a node that appends children to an element. 144 | */ 145 | const appendChild = (appendChildId, id, children) => 146 | t.callExpression( 147 | appendChildId, 148 | [id, t.arrayExpression(children)] 149 | ) 150 | 151 | const addDynamicAttribute = (helperId, id, attr, value) => 152 | t.callExpression(helperId, [id, attr, value]) 153 | 154 | /** 155 | * Wrap a node in a String() call if it may not be a string. 156 | */ 157 | const ensureString = (node) => { 158 | if (t.isStringLiteral(node)) { 159 | return node 160 | } 161 | return t.callExpression(t.identifier('String'), [node]) 162 | } 163 | 164 | /** 165 | * Concatenate multiple parts of an HTML attribute. 166 | */ 167 | const concatAttribute = (left, right) => 168 | t.binaryExpression('+', left, right) 169 | 170 | /** 171 | * Check if a node is *not* the empty string. 172 | * (Inverted so it can be used with `[].map` easily) 173 | */ 174 | const isNotEmptyString = (node) => 175 | !t.isStringLiteral(node, { value: '' }) 176 | 177 | const isEmptyTemplateLiteral = (node) => { 178 | return t.isTemplateLiteral(node) && 179 | node.expressions.length === 0 && 180 | node.quasis.length === 1 && 181 | t.isTemplateElement(node.quasis[0]) && 182 | node.quasis[0].value.raw === '' 183 | } 184 | 185 | /** 186 | * Transform a template literal into raw DOM calls. 187 | */ 188 | const nanohtmlify = (path, state) => { 189 | if (isEmptyTemplateLiteral(path.node)) { 190 | return t.unaryExpression('void', t.numericLiteral(0)) 191 | } 192 | 193 | const quasis = path.node.quasis.map((quasi) => quasi.value.cooked) 194 | const expressions = path.node.expressions 195 | const expressionPlaceholders = expressions.map((expr, i) => getPlaceholder(i)) 196 | 197 | const root = hyperx(transform, { 198 | comments: true, 199 | createFragment: children => transform('nanohtml-fragment', {}, children) 200 | }).apply(null, [quasis].concat(expressionPlaceholders)) 201 | 202 | /** 203 | * Convert placeholders used in the template string back to the AST nodes 204 | * they reference. 205 | */ 206 | function convertPlaceholders (value) { 207 | // Probably AST nodes. 208 | if (typeof value !== 'string') { 209 | return [value] 210 | } 211 | 212 | const items = value.split(placeholderRe) 213 | let placeholder = true 214 | return items.map((item) => { 215 | placeholder = !placeholder 216 | return placeholder ? expressions[item] : t.stringLiteral(item) 217 | }) 218 | } 219 | 220 | /** 221 | * Transform a hyperx vdom element to an AST node that creates the element. 222 | */ 223 | function transform (tag, props, children) { 224 | if (tag === '!--') { 225 | return createComment(props.comment) 226 | } 227 | 228 | const id = path.scope.generateUidIdentifier(getElementName(props, tag)) 229 | path.scope.push({ id }) 230 | 231 | const result = [] 232 | 233 | if (tag === 'nanohtml-fragment') { 234 | result.push(t.assignmentExpression('=', id, createFragment())) 235 | } else { 236 | var isCustomElement = props.is 237 | delete props.is 238 | 239 | // Use the SVG namespace for svg elements. 240 | if (SVG_TAGS.includes(tag)) { 241 | state.svgNamespaceId.used = true 242 | 243 | if (isCustomElement) { 244 | result.push(t.assignmentExpression('=', id, createNsCustomBuiltIn(state.svgNamespaceId, tag, isCustomElement))) 245 | } else { 246 | result.push(t.assignmentExpression('=', id, createNsElement(state.svgNamespaceId, tag))) 247 | } 248 | } else if (isCustomElement) { 249 | result.push(t.assignmentExpression('=', id, createCustomBuiltIn(tag, isCustomElement))) 250 | } else { 251 | result.push(t.assignmentExpression('=', id, createElement(tag))) 252 | } 253 | } 254 | 255 | Object.keys(props).forEach((propName) => { 256 | const dynamicPropName = convertPlaceholders(propName).filter(isNotEmptyString) 257 | // Just use the normal propName if there are no placeholders 258 | if (dynamicPropName.length === 1 && t.isStringLiteral(dynamicPropName[0])) { 259 | propName = dynamicPropName[0].value 260 | } else { 261 | state.setAttributeId.used = true 262 | result.push(addDynamicAttribute(state.setAttributeId, id, dynamicPropName.reduce(concatAttribute), 263 | convertPlaceholders(props[propName]).filter(isNotEmptyString).reduce(concatAttribute))) 264 | return 265 | } 266 | 267 | // don’t convert to lowercase, since some attributes are case-sensetive 268 | let attrName = propName 269 | 270 | if (attrName === 'className') { 271 | attrName = 'class' 272 | } 273 | 274 | if (attrName === 'htmlFor') { 275 | attrName = 'for' 276 | } 277 | 278 | // abc.onclick = xyz 279 | if (attrName.slice(0, 2) === 'on' || DIRECT_PROPS.indexOf(attrName) > -1) { 280 | const value = convertPlaceholders(props[propName]).filter(isNotEmptyString) 281 | result.push(setDomProperty(id, attrName, 282 | value.length === 1 283 | ? value[0] 284 | : value.map(ensureString).reduce(concatAttribute) 285 | )) 286 | 287 | return 288 | } 289 | 290 | // Dynamic boolean attributes 291 | if (BOOL_PROPS.indexOf(attrName) !== -1 && props[propName] !== attrName) { 292 | // if (xyz) abc.setAttribute('disabled', 'disabled') 293 | result.push(setBooleanAttribute(id, attrName, 294 | convertPlaceholders(props[propName]) 295 | .filter(isNotEmptyString)[0])) 296 | return 297 | } 298 | 299 | // use proper xml namespace for svg use links 300 | if (attrName === 'xlink:href') { 301 | const value = convertPlaceholders(props[propName]) 302 | .map(ensureString) 303 | .reduce(concatAttribute) 304 | 305 | state.xlinkNamespaceId.used = true 306 | result.push(setDomAttributeNS(id, attrName, value, state.xlinkNamespaceId)) 307 | 308 | return 309 | } 310 | 311 | // abc.setAttribute('class', xyz) 312 | result.push(setDomAttribute(id, attrName, 313 | convertPlaceholders(props[propName]) 314 | .map(ensureString) 315 | .reduce(concatAttribute) 316 | )) 317 | }) 318 | 319 | if (Array.isArray(children)) { 320 | const realChildren = children.map(convertPlaceholders) 321 | // Flatten 322 | .reduce((flat, arr) => flat.concat(arr), []) 323 | // Remove empty strings since they don't affect output 324 | .filter(isNotEmptyString) 325 | 326 | if (realChildren.length > 0) { 327 | state.appendChildId.used = true 328 | result.push(appendChild(state.appendChildId, id, realChildren)) 329 | } 330 | } 331 | 332 | result.push(id) 333 | return t.sequenceExpression(result) 334 | } 335 | 336 | return root 337 | } 338 | 339 | function isNanohtmlRequireCall (node) { 340 | if (!t.isIdentifier(node.callee, { name: 'require' })) { 341 | return false 342 | } 343 | const firstArg = node.arguments[0] 344 | // Not a `require('module')` call 345 | if (!firstArg || !t.isStringLiteral(firstArg)) { 346 | return false 347 | } 348 | 349 | const importFrom = firstArg.value 350 | return SUPPORTED_VIEWS.indexOf(importFrom) !== -1 351 | } 352 | 353 | return { 354 | pre () { 355 | this.nanohtmlBindings = new Set() 356 | }, 357 | post () { 358 | this.nanohtmlBindings.clear() 359 | }, 360 | 361 | visitor: { 362 | Program: { 363 | enter (path) { 364 | this.appendChildId = path.scope.generateUidIdentifier('appendChild') 365 | this.setAttributeId = path.scope.generateUidIdentifier('setAttribute') 366 | this.svgNamespaceId = path.scope.generateUidIdentifier('svgNamespace') 367 | this.xlinkNamespaceId = path.scope.generateUidIdentifier('xlinkNamespace') 368 | }, 369 | exit (path, state) { 370 | const appendChildModule = this.opts.appendChildModule || 'nanohtml/lib/append-child' 371 | const setAttributeModule = this.opts.setAttributeModule || 'nanohtml/lib/set-attribute' 372 | const useImport = this.opts.useImport 373 | 374 | if (this.appendChildId.used) { 375 | addImport(this.appendChildId, appendChildModule) 376 | } 377 | if (this.setAttributeId.used) { 378 | addImport(this.setAttributeId, setAttributeModule) 379 | } 380 | if (this.svgNamespaceId.used) { 381 | path.scope.push({ 382 | id: this.svgNamespaceId, 383 | init: t.stringLiteral(SVGNS) 384 | }) 385 | } 386 | if (this.xlinkNamespaceId.used) { 387 | path.scope.push({ 388 | id: this.xlinkNamespaceId, 389 | init: t.stringLiteral(XLINKNS) 390 | }) 391 | } 392 | 393 | function addImport (id, source) { 394 | if (useImport) { 395 | path.unshiftContainer('body', t.importDeclaration([ 396 | t.importDefaultSpecifier(id) 397 | ], t.stringLiteral(source))) 398 | } else { 399 | path.scope.push({ 400 | id: id, 401 | init: t.callExpression(t.identifier('require'), [t.stringLiteral(source)]) 402 | }) 403 | } 404 | } 405 | } 406 | }, 407 | 408 | /** 409 | * Collect nanohtml variable names and remove their imports if necessary. 410 | */ 411 | ImportDeclaration (path, state) { 412 | const importFrom = path.get('source').node.value 413 | if (SUPPORTED_VIEWS.indexOf(importFrom) !== -1) { 414 | const specifier = path.get('specifiers')[0] 415 | if (specifier.isImportDefaultSpecifier()) { 416 | this.nanohtmlBindings.add(path.scope.getBinding(specifier.node.local.name)) 417 | } 418 | } 419 | }, 420 | 421 | CallExpression (path, state) { 422 | if (isNanohtmlRequireCall(path.node)) { 423 | // Not a `thing = require(...)` declaration 424 | if (!path.parentPath.isVariableDeclarator()) return 425 | 426 | this.nanohtmlBindings.add(path.parentPath.scope.getBinding(path.parentPath.node.id.name)) 427 | } 428 | }, 429 | 430 | TaggedTemplateExpression (path, state) { 431 | const tag = path.get('tag') 432 | const binding = tag.isIdentifier() 433 | ? path.scope.getBinding(tag.node.name) 434 | : null 435 | 436 | const isNanohtmlBinding = binding ? this.nanohtmlBindings.has(binding) : false 437 | if (isNanohtmlBinding || isNanohtmlRequireCall(tag.node)) { 438 | let newPath = nanohtmlify(path.get('quasi'), state) 439 | // If this template string is the only expression inside an arrow 440 | // function, the `nanohtmlify` call may have introduced new variables 441 | // inside its scope and forced it to become an arrow function with 442 | // a block body. In that case if we replace the old `path`, it 443 | // doesn't do anything. Instead we need to find the newly introduced 444 | // `return` statement. 445 | if (path.parentPath.isArrowFunctionExpression()) { 446 | const statements = path.parentPath.get('body.body') 447 | if (statements) { 448 | path = statements.find((st) => st.isReturnStatement()) 449 | } 450 | } 451 | path.replaceWith(newPath) 452 | 453 | // Remove the import or require() for the tag if it's no longer used 454 | // anywhere. 455 | if (binding) { 456 | binding.dereference() 457 | if (!binding.referenced) { 458 | removeBindingImport(binding) 459 | } 460 | } 461 | } 462 | } 463 | } 464 | } 465 | } 466 | --------------------------------------------------------------------------------