├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── bel.js ├── bel └── elements.js ├── fixtures ├── arrowFunctions.expected.js ├── arrowFunctions.js ├── booleanAttr.expected.js ├── booleanAttr.js ├── combinedAttr.expected.js ├── combinedAttr.js ├── comment.expected.js ├── comment.js ├── dynamicAttr.expected.js ├── dynamicAttr.js ├── elementsChildren.expected.js ├── elementsChildren.js ├── empty.expected.js ├── empty.js ├── events.expected.js ├── events.js ├── hyperx.expected.js ├── hyperx.js ├── nesting.expected.js ├── nesting.js ├── orderOfOperations.expected.js ├── orderOfOperations.js ├── require.expected.js ├── require.js ├── simple.expected.js ├── simple.js ├── svg.expected.js ├── svg.js ├── this.expected.js ├── this.js ├── useImport.expected.js ├── useImport.js ├── variableNames.expected.js ├── variableNames.js ├── yoyoBindings.expected.js └── yoyoBindings.js ├── run.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.actual.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | 4 | node_js: 5 | - 9 6 | - 8 7 | - 6 8 | - 4 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Renée Kooi 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This plugin was merged with [nanohtml](https://github.com/choojs/nanohtml). 4 | Please use that instead! 5 | 6 | ```js 7 | // .babelrc 8 | { 9 | "plugins": ["nanohtml"] 10 | } 11 | ``` 12 | 13 | # babel-plugin-yo-yoify 14 | 15 | Like [yo-yoify][], but as a Babel plugin. Transform [yo-yo][] or [bel][] 16 | template strings into raw document calls. 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install --save-dev babel-plugin-yo-yoify 22 | # And: 23 | npm install --save yo-yoify 24 | ``` 25 | 26 | `yo-yoify` is used in the compiled output of `babel-plugin-yo-yoify`, so it must 27 | be installed next to it. 28 | 29 | ## Example 30 | 31 | Using `babel --plugins yo-yoify | prettier --stdin`: 32 | 33 | **In** 34 | 35 | ```js 36 | import html from 'bel' 37 | 38 | const title = 'yo-yoify' 39 | const header = html` 40 | 44 | ` 45 | ``` 46 | 47 | **Out** 48 | 49 | ```js 50 | var _h, _button, _pageHeader; 51 | 52 | var _appendChild = require('yo-yoify/lib/appendChild'); 53 | 54 | const title = 'yo-yoify'; 55 | const header = (_pageHeader = document.createElement( 56 | 'header' 57 | ), _pageHeader.setAttribute('id', 'page-header'), _appendChild(_pageHeader, [ 58 | '\n ', 59 | (_h = document.createElement('h1'), _appendChild(_h, [title]), _h), 60 | '\n ', 61 | (_button = document.createElement('button'), _button.onclick = event => 62 | alert('Hello world!'), _button.textContent = 'Click here', _button), 63 | '\n ' 64 | ]), _pageHeader); 65 | ``` 66 | 67 | ## Usage 68 | 69 | Without options: 70 | 71 | ```js 72 | // .babelrc 73 | { 74 | "plugins": [ 75 | "yo-yoify" 76 | ] 77 | } 78 | ``` 79 | 80 | With options: 81 | 82 | ```js 83 | // .babelrc 84 | { 85 | "plugins": [ 86 | ["yo-yoify", { 87 | "useImport": true 88 | }] 89 | ] 90 | } 91 | ``` 92 | 93 | ### bel v5 94 | 95 | When used with `bel` v5.1.3 or up, it's recommended to tell 96 | babel-plugin-yo-yoify to use bel's exported `appendChild` function. This way, 97 | the transformed output will always use the same appending and white space 98 | handling logic as the original source. 99 | 100 | ```js 101 | { 102 | "plugins": [ 103 | ["yo-yoify", { 104 | "appendChildModule": "bel/appendChild" 105 | }] 106 | ] 107 | } 108 | ``` 109 | 110 | bel versions v5.1.2 and below do not export the `appendChild` function--for 111 | those, the default `"yo-yoify/lib/appendChild"` function is used instead. This 112 | function may have different behaviour from the bel version being used, though. 113 | 114 | ## Options 115 | 116 | - `useImport` - Set to true to use `import` statements for injected modules. 117 | By default, `require` is used. Enable this if you're using Rollup. 118 | - `appendChildModule` - Import path to a module that contains an `appendChild` 119 | function. Set this to `"bel/appendChild"` when using bel v5.1.3 or up! 120 | Defaults to `"yo-yoify/lib/appendChild"`. 121 | 122 | ## License 123 | 124 | [ISC][] 125 | 126 | [yo-yoify]: https://github.com/shama/yo-yoify 127 | [yo-yo]: https://github.com/maxogden/yo-yo 128 | [bel]: https://github.com/shama/bel 129 | [ISC]: ./LICENSE 130 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const camelCase = require('camel-case') 4 | const hyperx = require('hyperx') 5 | const issvg = require('@f/is-svg') 6 | const svgNamespace = require('@f/svg-namespace') 7 | const normalizeWhitespace = require('normalize-html-whitespace') 8 | const isBooleanAttr = require('is-boolean-attribute') 9 | 10 | /** 11 | * Try to return a nice variable name for an element based on its HTML id, 12 | * classname, or tagname. 13 | */ 14 | function getElementName (props, tag) { 15 | if (typeof props.id === 'string' && !placeholderRe.test(props.id)) { 16 | return camelCase(props.id) 17 | } 18 | if (typeof props.className === 'string' && !placeholderRe.test(props.className)) { 19 | return camelCase(props.className.split(' ')[0]) 20 | } 21 | return tag || 'bel' 22 | } 23 | 24 | /** 25 | * Regex for detecting placeholders. 26 | */ 27 | const placeholderRe = /\0(\d+)\0/g 28 | 29 | /** 30 | * Get a placeholder string for a numeric ID. 31 | */ 32 | const getPlaceholder = (i) => `\0${i}\0` 33 | 34 | /** 35 | * Remove a binding and its import or require() call from the file. 36 | */ 37 | function removeBindingImport (binding) { 38 | const path = binding.path 39 | if (path.parentPath.isImportDeclaration() && 40 | // Remove the entire Import if this is the only imported binding. 41 | path.parentPath.node.specifiers.length === 1) { 42 | path.parentPath.remove() 43 | } else { 44 | path.remove() 45 | } 46 | } 47 | 48 | module.exports = (babel) => { 49 | const t = babel.types 50 | const belModuleNames = ['bel', 'yo-yo', 'choo', 'choo/html', 'nanohtml'] 51 | 52 | /** 53 | * Returns a node that creates a namespaced HTML element. 54 | */ 55 | const createNsElement = (ns, tag) => 56 | t.callExpression( 57 | t.memberExpression(t.identifier('document'), t.identifier('createElementNS')), 58 | [ns, t.stringLiteral(tag)] 59 | ) 60 | 61 | /** 62 | * Returns a node that creates an element. 63 | */ 64 | const createElement = (tag) => 65 | t.callExpression( 66 | t.memberExpression(t.identifier('document'), t.identifier('createElement')), 67 | [t.stringLiteral(tag)] 68 | ) 69 | 70 | /** 71 | * Returns a node that creates a comment. 72 | */ 73 | const createComment = (text) => 74 | t.callExpression( 75 | t.memberExpression(t.identifier('document'), t.identifier('createComment')), 76 | [t.stringLiteral(text)] 77 | ) 78 | 79 | /** 80 | * Returns a node that sets a DOM property. 81 | */ 82 | const setDomProperty = (id, prop, value) => 83 | t.assignmentExpression('=', 84 | t.memberExpression(id, t.identifier(prop)), 85 | value) 86 | 87 | /** 88 | * Returns a node that sets a DOM attribute. 89 | */ 90 | const setDomAttribute = (id, attr, value) => 91 | t.callExpression( 92 | t.memberExpression(id, t.identifier('setAttribute')), 93 | [t.stringLiteral(attr), value]) 94 | 95 | /** 96 | * Returns a node that sets a boolean DOM attribute. 97 | */ 98 | const setBooleanAttribute = (id, attr, value) => 99 | t.logicalExpression('&&', value, 100 | setDomAttribute(id, attr, t.stringLiteral(attr))) 101 | 102 | /** 103 | * Returns a node that appends children to an element. 104 | */ 105 | const appendChild = (appendChildId, id, children) => 106 | t.callExpression( 107 | appendChildId, 108 | [id, t.arrayExpression(children)] 109 | ) 110 | 111 | const appendTextNode = (id, text) => 112 | t.callExpression( 113 | t.memberExpression(id, t.identifier('appendChild')), 114 | [t.callExpression( 115 | t.memberExpression(t.identifier('document'), t.identifier('createTextNode')), 116 | [text] 117 | )] 118 | ) 119 | 120 | const addDynamicAttribute = (helperId, id, attr, value) => 121 | t.callExpression(helperId, [id, attr, value]) 122 | 123 | /** 124 | * Wrap a node in a String() call if it may not be a string. 125 | */ 126 | const ensureString = (node) => { 127 | if (t.isStringLiteral(node)) { 128 | return node 129 | } 130 | return t.callExpression(t.identifier('String'), [node]) 131 | } 132 | 133 | /** 134 | * Concatenate multiple parts of an HTML attribute. 135 | */ 136 | const concatAttribute = (left, right) => 137 | t.binaryExpression('+', left, right) 138 | 139 | /** 140 | * Check if a node is *not* the empty string. 141 | * (Inverted so it can be used with `[].map` easily) 142 | */ 143 | const isNotEmptyString = (node) => 144 | !t.isStringLiteral(node, { value: '' }) 145 | 146 | const isEmptyTemplateLiteral = (node) => { 147 | return t.isTemplateLiteral(node) && 148 | node.expressions.length === 0 && 149 | node.quasis.length === 1 && 150 | t.isTemplateElement(node.quasis[0]) && 151 | node.quasis[0].value.raw === '' 152 | } 153 | 154 | /** 155 | * Transform a template literal into raw DOM calls. 156 | */ 157 | const yoyoify = (path, state) => { 158 | if (isEmptyTemplateLiteral(path.node)) { 159 | return t.unaryExpression('void', t.numericLiteral(0)) 160 | } 161 | 162 | const quasis = path.node.quasis.map((quasi) => quasi.value.cooked) 163 | const expressions = path.node.expressions 164 | const expressionPlaceholders = expressions.map((expr, i) => getPlaceholder(i)) 165 | 166 | const root = hyperx(transform, { comments: true }).apply(null, 167 | [quasis].concat(expressionPlaceholders)) 168 | 169 | /** 170 | * Convert placeholders used in the template string back to the AST nodes 171 | * they reference. 172 | */ 173 | function convertPlaceholders (value) { 174 | // Probably AST nodes. 175 | if (typeof value !== 'string') { 176 | return [value] 177 | } 178 | 179 | const items = value.split(placeholderRe) 180 | let placeholder = true 181 | return items.map((item) => { 182 | placeholder = !placeholder 183 | return placeholder ? expressions[item] : t.stringLiteral(item) 184 | }) 185 | } 186 | 187 | /** 188 | * Transform a hyperx vdom element to an AST node that creates the element. 189 | */ 190 | function transform (tag, props, children) { 191 | if (tag === '!--') { 192 | return createComment(props.comment) 193 | } 194 | 195 | const id = path.scope.generateUidIdentifier(getElementName(props, tag)) 196 | path.scope.push({ id }) 197 | 198 | const result = [] 199 | 200 | // Use the SVG namespace for svg elements. 201 | if (issvg(tag)) { 202 | state.svgNamespaceId.used = true 203 | result.push(t.assignmentExpression('=', id, createNsElement(state.svgNamespaceId, tag))) 204 | } else { 205 | result.push(t.assignmentExpression('=', id, createElement(tag))) 206 | } 207 | 208 | Object.keys(props).forEach((propName) => { 209 | const dynamicPropName = convertPlaceholders(propName).filter(isNotEmptyString) 210 | // Just use the normal propName if there are no placeholders 211 | if (dynamicPropName.length === 1 && t.isStringLiteral(dynamicPropName[0])) { 212 | propName = dynamicPropName[0].value 213 | } else { 214 | state.setAttributeId.used = true 215 | result.push(addDynamicAttribute(state.setAttributeId, id, dynamicPropName.reduce(concatAttribute), 216 | convertPlaceholders(props[propName]).filter(isNotEmptyString).reduce(concatAttribute))) 217 | return 218 | } 219 | 220 | // don’t convert to lowercase, since some attributes are case-sensetive 221 | let attrName = propName 222 | 223 | if (attrName === 'className') { 224 | attrName = 'class' 225 | } 226 | 227 | if (attrName === 'htmlFor') { 228 | attrName = 'for' 229 | } 230 | 231 | // abc.onclick = xyz 232 | if (attrName.slice(0, 2) === 'on') { 233 | const value = convertPlaceholders(props[propName]).filter(isNotEmptyString) 234 | result.push(setDomProperty(id, attrName, 235 | value.length === 1 236 | ? value[0] 237 | : value.map(ensureString).reduce(concatAttribute) 238 | )) 239 | 240 | return 241 | } 242 | 243 | // Dynamic boolean attributes 244 | if (isBooleanAttr(attrName) && props[propName] !== attrName) { 245 | // if (xyz) abc.setAttribute('disabled', 'disabled') 246 | result.push(setBooleanAttribute(id, attrName, 247 | convertPlaceholders(props[propName]) 248 | .filter(isNotEmptyString)[0])) 249 | return 250 | } 251 | 252 | // abc.setAttribute('class', xyz) 253 | result.push(setDomAttribute(id, attrName, 254 | convertPlaceholders(props[propName]) 255 | .map(ensureString) 256 | .reduce(concatAttribute) 257 | )) 258 | }) 259 | 260 | if (Array.isArray(children)) { 261 | const realChildren = children.map(convertPlaceholders) 262 | // Flatten 263 | .reduce((flat, arr) => flat.concat(arr), []) 264 | // Remove empty strings since they don't affect output 265 | .filter(isNotEmptyString) 266 | // Remove unnecessary whitespace from other strings 267 | .map((child) => { 268 | if (t.isStringLiteral(child)) { 269 | child.value = normalizeWhitespace(child.value) 270 | } 271 | return child 272 | }) 273 | 274 | if (realChildren.length === 1 && t.isStringLiteral(realChildren[0])) { 275 | // A single string child doesn't need to call the `appendChild` helper 276 | // but can just append a new TextNode. 277 | result.push(appendTextNode(id, realChildren[0])) 278 | } else if (realChildren.length > 0) { 279 | state.appendChildId.used = true 280 | result.push(appendChild(state.appendChildId, id, realChildren)) 281 | } 282 | } 283 | 284 | result.push(id) 285 | return t.sequenceExpression(result) 286 | } 287 | 288 | return root 289 | } 290 | 291 | function isYoyoRequireCall (node) { 292 | if (!t.isIdentifier(node.callee, { name: 'require' })) { 293 | return false 294 | } 295 | const firstArg = node.arguments[0] 296 | // Not a `require('module')` call 297 | if (!firstArg || !t.isStringLiteral(firstArg)) { 298 | return false 299 | } 300 | 301 | const importFrom = firstArg.value 302 | return belModuleNames.indexOf(importFrom) !== -1 303 | } 304 | 305 | return { 306 | pre () { 307 | this.yoyoBindings = new Set() 308 | }, 309 | post () { 310 | this.yoyoBindings.clear() 311 | }, 312 | 313 | visitor: { 314 | Program: { 315 | enter (path) { 316 | this.appendChildId = path.scope.generateUidIdentifier('appendChild') 317 | this.setAttributeId = path.scope.generateUidIdentifier('setAttribute') 318 | this.svgNamespaceId = path.scope.generateUidIdentifier('svgNamespace') 319 | }, 320 | exit (path, state) { 321 | const appendChildModule = this.opts.appendChildModule || 'yo-yoify/lib/appendChild' 322 | const setAttributeModule = this.opts.setAttributeModule || 'yo-yoify/lib/setAttribute' 323 | const useImport = this.opts.useImport 324 | 325 | if (this.appendChildId.used) { 326 | addImport(this.appendChildId, appendChildModule) 327 | } 328 | if (this.setAttributeId.used) { 329 | addImport(this.setAttributeId, setAttributeModule) 330 | } 331 | if (this.svgNamespaceId.used) { 332 | path.scope.push({ 333 | id: this.svgNamespaceId, 334 | init: t.stringLiteral(svgNamespace) 335 | }) 336 | } 337 | 338 | function addImport (id, source) { 339 | if (useImport) { 340 | path.unshiftContainer('body', t.importDeclaration([ 341 | t.importDefaultSpecifier(id) 342 | ], t.stringLiteral(source))) 343 | } else { 344 | path.scope.push({ 345 | id: id, 346 | init: t.callExpression(t.identifier('require'), [t.stringLiteral(source)]) 347 | }) 348 | } 349 | } 350 | } 351 | }, 352 | 353 | /** 354 | * Collect bel variable names and remove their imports if necessary. 355 | */ 356 | ImportDeclaration (path, state) { 357 | const importFrom = path.get('source').node.value 358 | if (belModuleNames.indexOf(importFrom) !== -1) { 359 | const specifier = path.get('specifiers')[0] 360 | if (specifier.isImportDefaultSpecifier()) { 361 | this.yoyoBindings.add(path.scope.getBinding(specifier.node.local.name)) 362 | } 363 | } 364 | }, 365 | 366 | CallExpression (path, state) { 367 | if (isYoyoRequireCall(path.node)) { 368 | // Not a `thing = require(...)` declaration 369 | if (!path.parentPath.isVariableDeclarator()) return 370 | 371 | this.yoyoBindings.add(path.parentPath.scope.getBinding(path.parentPath.node.id.name)) 372 | } 373 | }, 374 | 375 | TaggedTemplateExpression (path, state) { 376 | const tag = path.get('tag') 377 | const binding = tag.isIdentifier() 378 | ? path.scope.getBinding(tag.node.name) 379 | : null 380 | 381 | const isYoyoBinding = binding ? this.yoyoBindings.has(binding) : false 382 | if (isYoyoBinding || isYoyoRequireCall(tag.node)) { 383 | let newPath = yoyoify(path.get('quasi'), state) 384 | // If this template string is the only expression inside an arrow 385 | // function, the `yoyoify` call may have introduced new variables 386 | // inside its scope and forced it to become an arrow function with 387 | // a block body. In that case if we replace the old `path`, it 388 | // doesn't do anything. Instead we need to find the newly introduced 389 | // `return` statement. 390 | if (path.parentPath.isArrowFunctionExpression()) { 391 | const statements = path.parentPath.get('body.body') 392 | if (statements) { 393 | path = statements.find((st) => st.isReturnStatement()) 394 | } 395 | } 396 | path.replaceWith(newPath) 397 | 398 | // Remove the import or require() for the tag if it's no longer used 399 | // anywhere. 400 | if (binding) { 401 | binding.dereference() 402 | if (!binding.referenced) { 403 | removeBindingImport(binding) 404 | } 405 | } 406 | } 407 | } 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-yo-yoify", 3 | "version": "2.0.0", 4 | "description": "Babel plugin to transform yo-yo or bel template strings into pure and fast document calls.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/goto-bus-stop/babel-plugin-yo-yoify.git" 12 | }, 13 | "keywords": [ 14 | "yo-yo", 15 | "bel", 16 | "yo-yoify", 17 | "babel", 18 | "babel-plugin" 19 | ], 20 | "author": "Renée Kooi ", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@f/is-svg": "^1.0.0", 24 | "@f/svg-namespace": "^1.0.1", 25 | "camel-case": "^3.0.0", 26 | "hyperx": "^2.3.0", 27 | "is-boolean-attribute": "0.0.1", 28 | "normalize-html-whitespace": "^0.2.0", 29 | "yo-yoify": "^4.0.0" 30 | }, 31 | "peerDependencies": { 32 | "yo-yoify": "^4.0.0" 33 | }, 34 | "devDependencies": { 35 | "babel-core": "^6.26.0", 36 | "babel-register": "^6.26.0", 37 | "jsdom": "^9.12.0", 38 | "jsdom-global": "^2.1.1", 39 | "pify": "^3.0.0", 40 | "proxyquire": "^1.8.0", 41 | "tape": "^4.8.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/bel.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const tape = require('tape') 3 | const proxyquire = require('proxyquire') 4 | const yoyoify = require('../') 5 | 6 | require('jsdom-global')() 7 | require('babel-register')({ 8 | only: /test\/bel/, 9 | plugins: [yoyoify] 10 | }) 11 | 12 | // Prefix test names from the `bel` test suite 13 | function wrappedTape (name, fn) { 14 | return tape(`bel: ${name}`, fn) 15 | } 16 | Object.assign(wrappedTape, tape) 17 | 18 | proxyquire('./bel/elements', { 19 | tape: wrappedTape 20 | }) 21 | -------------------------------------------------------------------------------- /test/bel/elements.js: -------------------------------------------------------------------------------- 1 | // Tests from shama/bel: 2 | // https://github.com/shama/bel/blob/3a05345fa880aed4ed91eb0dbf9293fff870d691/test/elements.js 3 | var test = require('tape') 4 | var bel = require('bel') 5 | 6 | test('create inputs', function (t) { 7 | t.plan(5) 8 | 9 | var expected = 'testing' 10 | var result = bel`` 11 | t.equal(result.tagName, 'INPUT', 'created an input') 12 | t.equal(result.value, expected, 'set the value of an input') 13 | 14 | result = bel`` 15 | t.equal(result.getAttribute('type'), 'checkbox', 'created a checkbox') 16 | t.equal(result.getAttribute('checked'), 'checked', 'set the checked attribute') 17 | t.equal(result.getAttribute('disabled'), null, 'should not have set the disabled attribute') 18 | 19 | t.end() 20 | }) 21 | 22 | test('can update and submit inputs', function (t) { 23 | t.plan(2) 24 | document.body.innerHTML = '' 25 | var expected = 'testing' 26 | function render (data, onsubmit) { 27 | var input = bel`` 28 | return bel`
29 | ${input} 30 | 33 |
` 34 | } 35 | var result = render(expected, function onsubmit (newvalue) { 36 | t.equal(newvalue, 'changed') 37 | document.body.innerHTML = '' 38 | t.end() 39 | }) 40 | document.body.appendChild(result) 41 | t.equal(document.querySelector('input').value, expected, 'set the input correctly') 42 | document.querySelector('input').value = 'changed' 43 | document.querySelector('button').click() 44 | }) 45 | 46 | test('svg', function (t) { 47 | t.plan(3) 48 | var result = bel` 49 | 50 | 51 | ` 52 | t.equal(result.tagName, 'svg', 'create svg tag') 53 | t.equal(result.childNodes[0].tagName, 'rect', 'created child rect tag') 54 | t.equal(result.childNodes[1].getAttribute('xlink:href'), '#test', 'created child use tag with xlink:href') 55 | t.end() 56 | }) 57 | 58 | test('svg with namespace', function (t) { 59 | t.plan(3) 60 | var result 61 | function create () { 62 | result = bel` 63 | 64 | ` 65 | } 66 | t.doesNotThrow(create) 67 | t.equal(result.tagName, 'svg', 'create svg tag') 68 | t.equal(result.childNodes[0].tagName, 'rect', 'created child rect tag') 69 | }) 70 | 71 | test('svg with xmlns:svg', function (t) { 72 | t.plan(3) 73 | var result 74 | function create () { 75 | result = bel` 76 | 77 | ` 78 | } 79 | t.doesNotThrow(create) 80 | t.equal(result.tagName, 'svg', 'create svg tag') 81 | t.equal(result.childNodes[0].tagName, 'rect', 'created child rect tag') 82 | }) 83 | 84 | test('comments', function (t) { 85 | var result = bel`
` 86 | t.equal(result.outerHTML, '
', 'created comment') 87 | t.end() 88 | }) 89 | 90 | test('style', function (t) { 91 | t.plan(2) 92 | var name = 'test' 93 | var result = bel`

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

` 94 | t.equal(result.style.color, 'red', 'set style color on parent') 95 | t.equal(result.querySelector('span').style.color, 'blue', 'set style color on child') 96 | t.end() 97 | }) 98 | 99 | test('adjacent text nodes', function (t) { 100 | t.plan(2) 101 | var who = 'world' 102 | var exclamation = ['!', ' :)'] 103 | var result = bel`
hello ${who}${exclamation}
` 104 | t.equal(result.childNodes.length, 1, 'should be merged') 105 | t.equal(result.outerHTML, '
hello world! :)
', 'should have correct output') 106 | t.end() 107 | }) 108 | 109 | test('for attribute is set correctly', function (t) { 110 | t.plan(1) 111 | var result = bel`
112 | 113 | 114 |
` 115 | t.ok(result.outerHTML.indexOf('') !== -1, 'contains for="heyo"') 116 | t.end() 117 | }) 118 | 119 | test('allow objects to be passed', function (t) { 120 | t.plan(1) 121 | var result = bel`
122 |
hey
123 |
` 124 | t.ok(result.outerHTML.indexOf('
hey
') !== -1, 'contains foo="bar"') 125 | t.end() 126 | }) 127 | -------------------------------------------------------------------------------- /test/fixtures/arrowFunctions.expected.js: -------------------------------------------------------------------------------- 1 | var _appendChild = require('yo-yoify/lib/appendChild'); 2 | 3 | const component = () => { 4 | var _h, _div; 5 | 6 | _div = document.createElement('div'), _appendChild(_div, [' ', (_h = document.createElement('h1'), _h.appendChild(document.createTextNode(' hello world ')), _h), ' ', list.map(x => { 7 | var _span; 8 | 9 | _span = document.createElement('span'), _span.setAttribute('style', 'background-color: red; margin: 10px;'), _appendChild(_span, [' ', x, ' ']), _span; 10 | }), ' ']), _div; 11 | }; // https://github.com/goto-bus-stop/babel-plugin-yo-yoify/issues/14 -------------------------------------------------------------------------------- /test/fixtures/arrowFunctions.js: -------------------------------------------------------------------------------- 1 | // https://github.com/goto-bus-stop/babel-plugin-yo-yoify/issues/14 2 | 3 | var bel = require('bel') 4 | 5 | const component = () => bel` 6 |
7 |

hello world

8 | ${list.map(x => bel` ${x} `)} 9 |
10 | ` 11 | -------------------------------------------------------------------------------- /test/fixtures/booleanAttr.expected.js: -------------------------------------------------------------------------------- 1 | var _input, _input2, _button; 2 | 3 | _input = document.createElement('input'), _input.setAttribute('autofocus', 'autofocus'), _input; 4 | _input2 = document.createElement('input'), true && _input2.setAttribute('checked', 'checked'), _input2; 5 | _button = document.createElement('button'), someVariable && _button.setAttribute('disabled', 'disabled'), _button; 6 | -------------------------------------------------------------------------------- /test/fixtures/booleanAttr.js: -------------------------------------------------------------------------------- 1 | const bel = require('bel') 2 | 3 | bel`` 4 | bel`` 5 | bel` 19 | \` 20 | `).code 21 | 22 | const props = { 23 | onclick: () => { /* an event */ }, 24 | disabled: true, // a boolean attribute 25 | className: 'abc', // an attribute with a different property name 26 | id: 'def', // a normal attribute 27 | } 28 | const context = { 29 | document, 30 | props, 31 | require, 32 | output: '' 33 | } 34 | 35 | vm.runInNewContext(src, context) 36 | const str = context.output.outerHTML 37 | 38 | // check attributes 39 | t.equal(str, '') 40 | 41 | // events should be assigned as properties 42 | t.equal(context.output.onclick, props.onclick) 43 | 44 | t.end() 45 | }) 46 | 47 | -------------------------------------------------------------------------------- /test/test.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 yoyoify = 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 | [yoyoify, 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('empty') 41 | testFixture('this') 42 | testFixture('variableNames') 43 | testFixture('nesting') 44 | testFixture('elementsChildren') 45 | testFixture('combinedAttr') 46 | testFixture('booleanAttr') 47 | testFixture('dynamicAttr') 48 | testFixture('events') 49 | testFixture('orderOfOperations') 50 | testFixture('svg') 51 | testFixture('require') 52 | testFixture('yoyoBindings') 53 | testFixture('arrowFunctions') 54 | testFixture('hyperx') 55 | testFixture('comment') 56 | testFixture('useImport', { useImport: true }) 57 | --------------------------------------------------------------------------------