├── .babelrc ├── .codeclimate.yml ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.jsx /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "syntax-jsx", 4 | ["./index", { "pragma": "h", }], 5 | ["jsx-pragmatic", { "module": "snabbdom/h", "import": "h" }] 6 | ], 7 | "presets": ["es2015"], 8 | "ignore": ["node_modules"], 9 | "sourceMaps": true 10 | } 11 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # This is a sample .codeclimate.yml configured for Engine analysis on Code 2 | # Climate Platform. For an overview of the Code Climate Platform, see here: 3 | # http://docs.codeclimate.com/article/300-the-codeclimate-platform 4 | 5 | # Under the engines key, you can configure which engines will analyze your repo. 6 | # Each key is an engine name. For each value, you need to specify enabled: true 7 | # to enable the engine as well as any other engines-specific configuration. 8 | 9 | # For more details, see here: 10 | # http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform 11 | 12 | # For a list of all available engines, see here: 13 | # http://docs.codeclimate.com/article/296-engines-available-engines 14 | 15 | engines: 16 | # to turn on an engine, add it here and set enabled to `true` 17 | # to turn off an engine, set enabled to `false` or remove it 18 | fixme: 19 | enabled: true 20 | eslint: 21 | enabled: true 22 | 23 | # Engines can analyze files and report issues on them, but you can separately 24 | # decide which files will receive ratings based on those issues. This is 25 | # specified by path patterns under the ratings key. 26 | 27 | # For more details see here: 28 | # http://docs.codeclimate.com/article/289-configuring-your-repository-via-codeclimate-yml#platform 29 | 30 | # Note: If the ratings key is not specified, this will result in a 0.0 GPA on your dashboard. 31 | 32 | # ratings: 33 | # paths: 34 | # - app/** 35 | # - lib/** 36 | # - "**.rb" 37 | # - "**.go" 38 | 39 | # You can globally exclude files from being analyzed by any engine using the 40 | # exclude_paths key. 41 | 42 | #exclude_paths: 43 | #- spec/**/* 44 | #- vendor/**/* 45 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2 6 | ], 7 | "quotes": [ 8 | 0, 9 | "double" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [ 16 | 2, 17 | "always" 18 | ] 19 | }, 20 | "env": { 21 | "es6": true, 22 | "node": true 23 | }, 24 | "extends": "eslint:recommended", 25 | "ecmaFeatures": { 26 | "jsx": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/tmp.js 3 | coverage 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | before_script: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Sebastian McKenzie , 2 | 2015 Oscar Finnsson 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-snabbdom-jsx 2 | 3 | [![Build Status](https://travis-ci.org/finnsson/babel-snabbdom-jsx.svg)](https://travis-ci.org/finnsson/babel-snabbdom-jsx) 4 | 5 | A [JSX](https://facebook.github.io/jsx/)-[Babel](https://babeljs.io)-plugin for [snabbdom](https://github.com/paldepind/snabbdom). 6 | 7 | See [tests](test/test.jsx) for examples. 8 | 9 | ## Contribute 10 | 11 | $ npm install 12 | $ npm run test 13 | 14 | Write your code 15 | 16 | $ npm run test 17 | $ npm run istanbul 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function (babel) { 2 | "use strict"; 3 | var t = babel.types; 4 | 5 | var visitor = parser({ 6 | t: t, 7 | pre: function pre(state) { 8 | var tagName = state.tagName; 9 | var args = state.args; 10 | if (t.react.isCompatTag(tagName)) { 11 | args.push(t.stringLiteral(tagName)); 12 | } else { 13 | args.push(state.tagExpr); 14 | } 15 | }, 16 | 17 | post: function post(state, pass) { 18 | state.callee = pass.get("jsxIdentifier"); 19 | } 20 | }); 21 | 22 | visitor.Program = function (path, state) { 23 | var id = state.opts.pragma || "h"; 24 | 25 | state.set("jsxIdentifier", id.split(".").map(function (name) { 26 | return t.identifier(name); 27 | }).reduce(function (object, property) { 28 | return t.memberExpression(object, property); 29 | })); 30 | }; 31 | 32 | return { 33 | inherits: require("babel-plugin-syntax-jsx"), 34 | visitor: visitor 35 | }; 36 | }; 37 | 38 | function parser(opts) { 39 | var t = opts.t; 40 | var visitor = {}; 41 | 42 | /* 43 | visitor.JSXNamespacedName = function (path) { 44 | throw path.buildCodeFrameError("Namespace tags are not supported yet."); 45 | }; 46 | */ 47 | 48 | visitor.JSXElement = { 49 | exit: function exit(path, file) { 50 | var callExpr = buildElementCall(path.get("openingElement"), file); 51 | 52 | var concatExpression = []; 53 | 54 | if (path.node.children.length) { 55 | var emptyArray = t.arrayExpression([]); 56 | var emptyArrayDotConcat = t.memberExpression(emptyArray, t.identifier("concat")); 57 | concatExpression = t.callExpression(emptyArrayDotConcat, path.node.children); 58 | } 59 | 60 | callExpr.arguments = callExpr.arguments.concat(concatExpression); // path.node.children 61 | 62 | if (callExpr.arguments.length >= 3) { 63 | callExpr._prettyCall = true; 64 | } 65 | 66 | path.replaceWith(t.inherits(callExpr, path.node)); 67 | } 68 | }; 69 | 70 | return visitor; 71 | 72 | function convertJSXIdentifier(node, parent) { 73 | if (t.isJSXIdentifier(node)) { 74 | if (node.name === "this" && t.isReferenced(node, parent)) { 75 | return t.thisExpression(); 76 | } else if (/^[A-Z]/.test(node.name)) { 77 | // node is assumed to be a JS identifier (var, function, class, etc) if the first letter is UPPERCASE 78 | node.type = "Identifier"; 79 | } else { 80 | return t.stringLiteral(node.name); 81 | } 82 | } else if (t.isJSXMemberExpression(node)) { 83 | return t.memberExpression(convertJSXIdentifier(node.object, node), convertJSXIdentifier(node.property, node)); 84 | } 85 | 86 | return node; 87 | } 88 | 89 | function convertAttributeValue(node) { 90 | if (t.isJSXExpressionContainer(node)) { 91 | return node.expression; 92 | } else { 93 | return node; 94 | } 95 | } 96 | 97 | function convertAttribute(node) { 98 | var value = convertAttributeValue(node.value || t.booleanLiteral(true)); 99 | 100 | if (t.isStringLiteral(value)) { 101 | value.value = value.value.replace(/\n\s+/g, " "); 102 | } 103 | 104 | if (t.isValidIdentifier(node.name.name)) { 105 | node.name.type = "Identifier"; 106 | } else { 107 | var nodeName = node.name.namespace ? node.name.namespace.name + ":" + node.name.name.name : node.name.name; 108 | node.name = t.stringLiteral(nodeName); 109 | } 110 | 111 | return t.inherits(t.objectProperty(node.name, value), node); 112 | } 113 | 114 | function buildElementCall(path, file) { 115 | path.parent.children = t.react.buildChildren(path.parent); 116 | 117 | var tagExpr = convertJSXIdentifier(path.node.name, path.node); 118 | var args = []; 119 | 120 | var tagName = undefined; 121 | if (t.isIdentifier(tagExpr)) { 122 | tagName = tagExpr.name; 123 | } else if (t.isLiteral(tagExpr)) { 124 | tagName = tagExpr.value; 125 | } 126 | 127 | var state = { 128 | tagExpr: tagExpr, 129 | tagName: tagName, 130 | args: args 131 | }; 132 | 133 | if (opts.pre) { 134 | opts.pre(state, file); 135 | } 136 | 137 | var attribs = path.node.attributes; 138 | if (attribs.length) { 139 | attribs = buildOpeningElementAttributes(attribs, file); 140 | } else { 141 | attribs = t.objectExpression([]); 142 | // attribs = t.nullLiteral(); 143 | } 144 | 145 | args.push(attribs); 146 | 147 | if (opts.post) { 148 | opts.post(state, file); 149 | } 150 | 151 | return state.call || t.callExpression(state.callee, args); 152 | } 153 | 154 | function groupAttributes(props) { 155 | var attributes = []; 156 | 157 | // group props based on prefix (prefix-name) 158 | 159 | props.forEach(prop => { 160 | var propName = prop.key.name || prop.key.value; 161 | var propNameSplitOnDash = propName.split("-"); 162 | if (propNameSplitOnDash.length > 1) { // contains - 163 | var prefix = propNameSplitOnDash[0]; 164 | var suffix = propNameSplitOnDash[1]; 165 | // check if node already contains property with name prefix 166 | var propertyWithPrefix = attributes.find(p => p.key.name === prefix); 167 | var suffixProperty = t.objectProperty(t.identifier(suffix), prop.value); 168 | if (!propertyWithPrefix) { 169 | propertyWithPrefix = t.objectProperty(t.identifier(prefix), t.objectExpression([suffixProperty])); 170 | attributes.push(propertyWithPrefix); 171 | } else { 172 | propertyWithPrefix.value.properties.push( 173 | suffixProperty 174 | ); 175 | } 176 | } else if (propName.endsWith("_")) { 177 | 178 | attributes.push(t.objectProperty(t.identifier(propName.replace(/_$/, "")), prop.value)); 179 | } else { 180 | 181 | // push to attrs property 182 | var attrsProperty = attributes.find(p => p.key.name === "attrs"); 183 | if (!attrsProperty) { 184 | attrsProperty = t.objectProperty(t.identifier("attrs"), t.objectExpression([prop])); 185 | attributes.push(attrsProperty); 186 | } else { 187 | attrsProperty.value.properties.push(prop); 188 | } 189 | // attributes.push(prop); 190 | } 191 | }); 192 | 193 | return attributes; 194 | 195 | //obj.properties = obj.properties.concat(attributes); 196 | //return obj; 197 | } 198 | 199 | /** 200 | * The logic for this is quite terse. It's because we need to 201 | * support spread elements. We loop over all attributes, 202 | * breaking on spreads, we then push a new object containg 203 | * all prior attributes to an array for later processing. 204 | */ 205 | 206 | function buildOpeningElementAttributes(attribs, file) { 207 | var _props = []; 208 | var objs = []; 209 | 210 | function pushProps() { 211 | if (!_props.length) return; 212 | 213 | objs.push(t.objectExpression(_props)); 214 | _props = []; 215 | } 216 | 217 | while (attribs.length) { 218 | var prop = attribs.shift(); 219 | if (t.isJSXSpreadAttribute(prop)) { 220 | pushProps(); 221 | objs.push(prop.argument); 222 | } else { 223 | _props.push(convertAttribute(prop)); 224 | } 225 | } 226 | 227 | pushProps(); 228 | 229 | if (objs.length === 1) { 230 | // only one object 231 | attribs = objs[0]; 232 | attribs = t.objectExpression( groupAttributes(attribs.properties)); 233 | // group attribs based on 234 | } else { 235 | // looks like we have multiple objects 236 | if (!t.isObjectExpression(objs[0])) { 237 | objs.unshift(t.objectExpression([])); 238 | } 239 | 240 | // spread it 241 | attribs = t.callExpression(file.addHelper("extends"), objs); 242 | } 243 | 244 | return attribs; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-snabbdom-jsx", 3 | "version": "0.4.0", 4 | "description": "Babeljs plugin for snabbdom JSX", 5 | "main": "index.js", 6 | "repository": "https://github.com/finnsson/babel-snabbdom-jsx", 7 | "scripts": { 8 | "test": "mocha test/tmp.js", 9 | "mocha": "mocha test/tmp.js", 10 | "pretest": "npm run lint && npm run babel", 11 | "babel": "babel test/test.jsx > test/tmp.js", 12 | "lint": "eslint index.js", 13 | "istanbul": "istanbul cover babel -- test/test.jsx --out-file test/tmp.js" 14 | }, 15 | "keywords": [ 16 | "babel", 17 | "snabbdom", 18 | "vdom", 19 | "jsx" 20 | ], 21 | "author": "Oscar Finnsson", 22 | "license": "ISC", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "babel": "^6.3.13", 26 | "babel-cli": "^6.3.13", 27 | "babel-plugin-jsx-pragmatic": "~1.0.2", 28 | "babel-plugin-syntax-jsx": "^6.3.13", 29 | "babel-preset-es2015": "^6.3.13", 30 | "eslint": "^1.10.3", 31 | "istanbul": "^0.4.1", 32 | "mocha": "^2.3.4", 33 | "snabbdom": "^0.2.6" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/test.jsx: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var h_ = require('snabbdom/h'); 4 | 5 | describe("loader", function() { 6 | 7 | it("should transpile to the equivalent h call", function() { 8 | var dom =
test
; 9 | var dom2 = h_("div", {}, ["test"]); 10 | 11 | assert.equal("div", dom.sel); 12 | assert.equal("test", dom.children[0].text); 13 | 14 | assert.deepEqual(dom, dom2); 15 | }); 16 | 17 | 18 | it("should contain text", function() { 19 | var dom =
test
; 20 | 21 | assert.equal("div", dom.sel); 22 | assert.equal("test", dom.children[0].text); 23 | }); 24 | 25 | 26 | it("should bind text", function() { 27 | var text = "Lorem"; 28 | var dom =
{text}
; 29 | 30 | assert.equal("div", dom.sel); 31 | assert.equal(text, dom.children[0].text); 32 | }); 33 | 34 | it("should bind text expressions", function() { 35 | var text = "Lorem"; 36 | var dom =
{text + " ipsum"}
; 37 | 38 | assert.equal("div", dom.sel); 39 | assert.equal("Lorem ipsum", dom.children[0].text); 40 | }); 41 | 42 | it("should extract attrs", function() { 43 | var dom =
test
; 44 | 45 | assert.equal("div", dom.sel); 46 | assert.equal("ltr", dom.data.attrs.dir); 47 | }); 48 | 49 | it("should extract events", function() { 50 | var doSomething = function() {}; 51 | var dom =
test
; 52 | 53 | assert.equal("div", dom.sel); 54 | assert.equal(doSomething, dom.data.on.click); 55 | }); 56 | 57 | it("should extract multiple events", function() { 58 | var onClick = function() {}; 59 | var onMouseOver = function() {}; 60 | var dom =
test
; 61 | 62 | assert.equal("div", dom.sel); 63 | assert.equal(onClick, dom.data.on.click); 64 | assert.equal(onMouseOver, dom.data.on.mouseover); 65 | }); 66 | 67 | it("should extract class hash when using multiple -", function() { 68 | var truthy = true; 69 | var dom =
Lorem ipsum
; 70 | 71 | assert.equal(true, dom.data.class.foo); 72 | assert.equal(false, dom.data.class.bar); 73 | assert.equal(true, dom.data.class.goo); 74 | }); 75 | 76 | it("should extract class hash when using _", function() { 77 | var truthy = true; 78 | var dom =
Lorem ipsum
; 83 | 84 | assert.equal(true, dom.data.class.foo); 85 | assert.equal(false, dom.data.class.bar); 86 | assert.equal(true, dom.data.class.goo); 87 | }); 88 | 89 | it("should extract style hash when using _", function() { 90 | var fontWeight = "bold"; 91 | var dom = Lorem ipsum; 96 | 97 | assert.equal("span", dom.sel); 98 | assert.equal("1px solid #bada55", dom.data.style.border); 99 | assert.equal("bold", dom.data.style.fontWeight); 100 | assert.equal("Lorem ipsum", dom.children[0].text); 101 | }); 102 | 103 | it("should put style string as attribute", function() { 104 | var dom = Lorem ipsum; 105 | 106 | assert.equal("color: red", dom.data.attrs.style); 107 | }); 108 | 109 | it("should handle key_", function() { 110 | var num = 11; 111 | var dom = Foo; 112 | 113 | assert.equal(11, dom.data.key); 114 | }); 115 | 116 | it("should manage children in children", function() { 117 | var dom =
text 1 118 |
first
119 | 123 | text2
; 124 | 125 | assert.equal("div", dom.sel); 126 | var children = dom.children; 127 | assert.equal("text 1", children[0].text); 128 | assert.equal("div", children[1].sel); 129 | assert.equal("ul", children[2].sel); 130 | 131 | var grandChildren = children[2].children; 132 | assert.equal("li", grandChildren[0].sel); 133 | assert.equal("list item 1", grandChildren[0].children[0].text); 134 | assert.equal("list item 2", grandChildren[1].children[0].text); 135 | 136 | assert.equal("text2", children[3].text); 137 | }); 138 | 139 | 140 | 141 | it("should manage svg", function() { 142 | var dom = 143 | 144 | ; 145 | 146 | assert.equal("svg", dom.sel); 147 | assert.equal("http://www.w3.org/1999/xlink", dom.data.attrs["xmlns:xlink"]); 148 | assert.equal("http://www.w3.org/2000/svg", dom.data.attrs.xmlns); 149 | assert.equal("rect", dom.children[0].sel); 150 | assert.equal("100", dom.children[0].data.attrs.height); 151 | assert.equal("10", dom.children[0].data.attrs.x); 152 | assert.equal("stroke:#ff0000; fill: #0000ff", dom.children[0].data.attrs.style); 153 | }); 154 | 155 | 156 | /* 157 | it("should handle member tag name", function() { 158 | //var tagName = "span"; 159 | var dom = Lorem 160 | 161 | ; 162 | 163 | assert.equal("button.large", dom.sel); 164 | assert.equal("span", dom.children[1].sel); 165 | 166 | }); 167 | */ 168 | 169 | it("should handle self closing tags", function() { 170 | var dom = ; 171 | 172 | assert.equal("span", dom.sel); 173 | assert.equal("button", dom.data.attrs.class); 174 | }); 175 | 176 | it("should handle function tag name", function() { 177 | var X = function() {}; 178 | 179 | var dom = ; 180 | 181 | assert.equal(X, dom.sel); 182 | assert.equal("foo", dom.data.attrs.class); 183 | }); 184 | 185 | it("should handle this", function() { 186 | this.FOO = function() {}; 187 | 188 | var dom =
test
; 189 | 190 | assert.equal(this.FOO, dom.data.on.click); 191 | }); 192 | 193 | it("should handle boolean attributes", function() { 194 | var dom = ; 195 | 196 | assert.equal("input", dom.sel); 197 | assert.equal(true, dom.data.attrs.checked); 198 | }); 199 | 200 | it("should handle empty expressions", function() { 201 | var dom = {}; 202 | 203 | assert.equal("input", dom.sel); 204 | assert.equal(null, dom.children); 205 | 206 | }); 207 | 208 | it("should handle this in tag name", function() { 209 | this.FOO = function() {}; 210 | 211 | var dom = test; 212 | 213 | assert.equal(this.FOO, dom.sel); 214 | }); 215 | 216 | it("should handle a child in code", function() { 217 | var dom =
{ }
; 218 | 219 | assert.equal("span", dom.children[0].sel); 220 | }); 221 | 222 | it("should handle children in code", function() { 223 | var dom =
{ [,
    ] }
    ; 224 | 225 | assert.equal(2, dom.children.length); 226 | assert.equal("ul", dom.children[1].sel); 227 | }); 228 | 229 | it("should insert insert-hook", function() { 230 | var onInsert = function(vnode) { 231 | 232 | }; 233 | var dom =
    test
    ; 234 | assert.equal(onInsert, dom.data.hook.insert); 235 | 236 | }); 237 | 238 | // spread operator ... {...props} => props_={{props}} 239 | 240 | 241 | }); 242 | --------------------------------------------------------------------------------