├── .gitignore ├── babel-plugin.js ├── .travis.yml ├── tests ├── _run.js ├── entries │ ├── text.jsx │ ├── styles.jsx │ ├── tags.jsx │ └── attrs.jsx ├── _get-runtime.js ├── test.js └── _mockdom.js ├── package.json ├── README.md ├── LICENSE.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /babel-plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require('babel-plugin-jsx/gen')({ 2 | captureScope: true 3 | }); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'iojs' 5 | - '0.12' 6 | - '0.10' -------------------------------------------------------------------------------- /tests/_run.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var runtime = require('./_get-runtime.js')(); 4 | var _mockdom = require('./_mockdom'); 5 | 6 | var run = function(view) { 7 | return _mockdom.run(function() { 8 | runtime.render(view); 9 | }); 10 | }; 11 | 12 | module.exports = run; -------------------------------------------------------------------------------- /tests/entries/text.jsx: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var run = require('../_run'); 3 | 4 | export var simple = () => { 5 | let elem = run( 6 |
7 | text-test 8 |
9 | ); 10 | 11 | assert.deepEqual( 12 | elem, { 13 | tag: 'div', 14 | children: [{ 15 | type: '#text', 16 | value: 'text-test' 17 | }] 18 | } 19 | ); 20 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-to-idom", 3 | "version": "1.1.1", 4 | "description": "JSX-IR renderer for Incremental DOM", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha tests/test.js" 8 | }, 9 | "author": "Arthur Stolyar ", 10 | "license": "MIT", 11 | "repository": "jsx-ir/jsx-to-idom", 12 | "peerDependencies": { 13 | "incremental-dom": "^0.x" 14 | }, 15 | "dependencies": { 16 | "babel-plugin-jsx": "^1.1.0", 17 | "jsx-runtime": "^1.1.0" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^5.6.20", 21 | "mocha": "^2.2.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/entries/styles.jsx: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var run = require('../_run'); 3 | 4 | export var object = () => { 5 | let elem = run( 6 |
11 | ); 12 | 13 | assert.deepEqual( 14 | elem, { 15 | tag: 'div', 16 | props: { 17 | style: { 18 | color: 'black' 19 | } 20 | } 21 | } 22 | ); 23 | }; 24 | 25 | export var string = () => { 26 | let elem = run( 27 |
30 | ); 31 | 32 | assert.deepEqual( 33 | elem, { 34 | tag: 'div', 35 | attrs: { 36 | style: 'color: black' 37 | } 38 | } 39 | ); 40 | }; -------------------------------------------------------------------------------- /tests/_get-runtime.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var Module = require('module'); 4 | 5 | var modulePaths = module.paths; 6 | 7 | function requireString(src, filePath, fileName) { 8 | var module = new Module(fileName); 9 | 10 | module.filename = filePath; 11 | module.paths = Module._nodeModulePaths(path.dirname(filePath)); 12 | module._compile(src, filePath); 13 | 14 | return module.exports; 15 | } 16 | 17 | module.exports = function(filePath) { 18 | filePath = path.join(__dirname, filePath || '../index.js'); 19 | 20 | var file = fs.readFileSync(filePath, 'utf-8'); 21 | var mock = '"use strict"; var _mockdom = require("./tests/_mockdom");\n'; 22 | 23 | var fakeName = '../fake-runtime.js'; 24 | var module = requireString(mock + '\n' + file, path.join(__dirname, fakeName), fakeName); 25 | 26 | return module; 27 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jsx-ir/jsx-to-idom.svg?branch=master)](https://travis-ci.org/jsx-ir/jsx-to-idom) 2 | 3 | ## Incremental DOM Renderer for JSX-IR 4 | 5 | ### Installation 6 | 7 | ```npm install jsx-to-idom``` 8 | 9 | _**Note:** do not forget to install "incremental-dom" manually since from NPM 3 peer dependencies won't be installed automatically_ 10 | 11 | ### Usage 12 | 13 | #### Transpiling 14 | 15 | ```js 16 | babel.transform(code, { 17 | plugins: ['jsx-to-idom/babel-plugin'], 18 | blacklist: ['react'] 19 | }); 20 | ``` 21 | or any other way described [here](http://babeljs.io/docs/advanced/plugins/#usage), just pass `'jsx-to-idom/babel-plugin'`` as a plugin name. 22 | 23 | ### Runtime 24 | 25 | ```javascript 26 | import { render } from 'jsx-to-idom'; 27 | import { patch } from 'incremental-dom' 28 | 29 | patch(container, () => { 30 | render(
Hello World
); 31 | }); 32 | 33 | ``` 34 | 35 | ## License 36 | 37 | [MIT](LICENSE.md) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Arthur Stolyar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var babel = require('babel-core'); 6 | var plugin = require('../babel-plugin'); 7 | var Module = require('module'); 8 | 9 | var testsFolder = path.join(__dirname, 'entries'); 10 | var dir = fs.readdirSync(testsFolder); 11 | 12 | var modulePaths = module.paths; 13 | 14 | describe('dom tests', function() { 15 | dir.forEach(function(fileName) { 16 | var filePath = path.join(testsFolder, fileName); 17 | var file = fs.readFileSync(filePath, 'utf-8'); 18 | 19 | testFile(file, filePath, fileName); 20 | 21 | function testFile(file, filePath, fileName) { 22 | var result = babel.transform(file, { 23 | plugins: [plugin], 24 | blacklist: ['react'] 25 | }); 26 | 27 | var mod = requireString(result.code, filePath, fileName); 28 | 29 | describe(fileName, function() { 30 | Object.keys(mod).forEach(function(key) { 31 | it(key, mod[key]); 32 | }); 33 | }); 34 | } 35 | }); 36 | }); 37 | 38 | function requireString(src, filePath, fileName) { 39 | var module = new Module(fileName); 40 | 41 | module.filename = filePath; 42 | module.paths = Module._nodeModulePaths(path.dirname(filePath)); 43 | module._compile(src, filePath); 44 | 45 | return module.exports; 46 | } 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var jsx = require('jsx-runtime'); 4 | var idom = typeof _mockdom !== 'undefined' ? _mockdom : require('incremental-dom'); 5 | var hasOwn = Object.prototype.hasOwnProperty; 6 | 7 | var openStart = idom.elementOpenStart; 8 | var openEnd = idom.elementOpenEnd; 9 | var close = idom.elementClose; 10 | var text = idom.text; 11 | var attr = idom.attr; 12 | 13 | var element = {}; 14 | 15 | var renderer = jsx.register('DOM', { 16 | tags: { 17 | '*': { 18 | enter: function(tag, props) { 19 | openStart(tag); 20 | 21 | if (props) { 22 | handleProps(props); 23 | } 24 | 25 | openEnd(); 26 | 27 | return element; 28 | }, 29 | leave: function(parent, tag) { 30 | close(tag); 31 | return parent; 32 | }, 33 | child: function(child, parent) { 34 | if (child === element) { 35 | // do nothing 36 | } else { 37 | text(child + ''); 38 | } 39 | 40 | return parent; 41 | } 42 | } 43 | } 44 | }); 45 | 46 | module.exports = renderer; 47 | 48 | function handleProps(props) { 49 | for (var key in props) { 50 | if (!hasOwn.call(props, key)) continue; 51 | 52 | var val = props[key]; 53 | 54 | if (key === 'className') key = 'class'; 55 | if (key === 'cssFor') key = 'for'; 56 | 57 | attr(key, val); 58 | } 59 | } -------------------------------------------------------------------------------- /tests/entries/tags.jsx: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var run = require('../_run'); 3 | 4 | export var simple = () => { 5 | var elem = run(
); 6 | 7 | assert.deepEqual( 8 | elem, { 9 | tag: 'div' 10 | } 11 | ); 12 | }; 13 | 14 | export var svg_namespace = () => { 15 | let elem = run( 16 |
17 | 18 |
19 | ); 20 | 21 | // cannot test namespaces for idom 22 | assert.deepEqual(elem, { 23 | tag: 'div', 24 | children: [{ 25 | tag: 'svg' 26 | }] 27 | }); 28 | }; 29 | 30 | export var html_namespace = () => { 31 | let elem = run( 32 |
33 | 34 | 35 | 36 |
37 | ); 38 | 39 | // cannot test namespaces for idom 40 | assert.deepEqual(elem, { 41 | tag: 'div', 42 | children: [{ 43 | tag: 'svg', 44 | children: [{ 45 | tag: 'foreignObject', 46 | }] 47 | }] 48 | }); 49 | }; 50 | 51 | export var custom_tags = () => { 52 | let elem = run( 53 |
54 | 55 |
56 | ); 57 | 58 | assert.deepEqual(elem, { 59 | tag: 'div', 60 | children: [{ 61 | tag: 'custom-tag' 62 | }] 63 | }); 64 | }; 65 | 66 | export var scope_tags = () => { 67 | var Scoped = function() { 68 | return 69 | }; 70 | 71 | let elem = run( 72 |
73 | 74 |
75 | ); 76 | 77 | assert.deepEqual(elem, { 78 | tag: 'div', 79 | children: [{ 80 | tag: 'span' 81 | }] 82 | }); 83 | }; -------------------------------------------------------------------------------- /tests/entries/attrs.jsx: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var run = require('../_run'); 3 | 4 | export var simple = () => { 5 | let elem = run( 6 |
7 | ); 8 | 9 | assert.deepEqual( 10 | elem, { 11 | tag: 'div', 12 | attrs: { 13 | 'data-test': 'test' 14 | } 15 | } 16 | ); 17 | }; 18 | 19 | export var js_values = () => { 20 | let fn = () => { 1; }; 21 | 22 | let elem = run( 23 |
33 | ); 34 | 35 | assert.deepEqual( 36 | elem, { 37 | tag: 'div', 38 | attrs: { 39 | 'data-boolean': true, 40 | 'data-number': 1, 41 | 'data-string': 'str' 42 | }, 43 | props: { 44 | 'data-object': {}, 45 | 'data-array': [1, 2, 3], 46 | 'data-fn': fn, 47 | 'data-null': null 48 | } 49 | } 50 | ); 51 | }; 52 | 53 | export var props_transformation = () => { 54 | let elem = run( 55 |
56 | ); 57 | 58 | assert.deepEqual( 59 | elem, { 60 | tag: 'div', 61 | attrs: { 62 | 'class': 'test', 63 | 'for': 'target' 64 | } 65 | } 66 | ); 67 | }; 68 | 69 | export var identifier_attrs = () => { 70 | let elem = run( 71 |
72 | ); 73 | 74 | assert.deepEqual( 75 | elem, { 76 | tag: 'div', 77 | attrs: { 78 | 'class': 'test', 79 | 'for': 'target' 80 | } 81 | } 82 | ); 83 | }; -------------------------------------------------------------------------------- /tests/_mockdom.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var currentRun; 4 | 5 | var getElement = function() { 6 | var stack = currentRun.stack; 7 | 8 | return stack.length ? stack[stack.length - 1] : null; 9 | }; 10 | 11 | module.exports = { 12 | run: function(fn) { 13 | if (currentRun) throw new Error('Cannot run while run'); 14 | 15 | currentRun = { 16 | stack: [], 17 | result: [] 18 | }; 19 | 20 | fn(); 21 | 22 | var result = currentRun.result; 23 | currentRun = null; 24 | 25 | // jsx does not support top-level siblings 26 | return result[0]; 27 | }, 28 | elementOpenStart: function(tag) { 29 | var parent = getElement(); 30 | var element = { 31 | tag: tag, 32 | children: [], 33 | props: {}, 34 | attrs: {}, 35 | // openEnded: false 36 | }; 37 | 38 | if (parent) { 39 | parent.children.push(element); 40 | } else { 41 | currentRun.result.push(element); 42 | } 43 | 44 | currentRun.stack.push(element); 45 | }, 46 | attr: function(key, val) { 47 | var element = getElement(); 48 | 49 | if (!element) { 50 | throw new Error('Cannot set attr when there is no element'); 51 | } 52 | 53 | if (element.openEnded) { 54 | // throw new Error('Cannot set attr when element opening is ended'); 55 | } 56 | 57 | var type = typeof val; 58 | 59 | if (val === undefined) { 60 | delete element.attrs[key]; 61 | } else if (type === 'object' || type === 'function') { 62 | element.props[key] = val; 63 | } else { 64 | element.attrs[key] = val; 65 | } 66 | }, 67 | elementOpenEnd: function() { 68 | var element = getElement(); 69 | 70 | if (!element) { 71 | throw new Error('Cannot end element opening when there is no element'); 72 | } 73 | 74 | if (!Object.keys(element.props).length) { 75 | delete element.props; 76 | } 77 | 78 | if (!Object.keys(element.attrs).length) { 79 | delete element.attrs; 80 | } 81 | 82 | // element.openEnded = true; 83 | }, 84 | text: function(text) { 85 | var parent = getElement(); 86 | var text = { 87 | type: '#text', 88 | value: text + '' 89 | }; 90 | 91 | if (!parent) { 92 | throw new Error('Text cannot be top-level call'); 93 | } 94 | 95 | parent.children.push(text); 96 | }, 97 | elementClose: function(tag) { 98 | var element = getElement(); 99 | 100 | if (!element) { 101 | throw new Error('Cannot close element when there is no one'); 102 | } 103 | 104 | if (element.tag !== tag) { 105 | throw new Error('Cannot close element with different tag: actual <' + 106 | element.tag + '>, expected <' + tag + '>'); 107 | } 108 | 109 | if (!element.children.length) { 110 | delete element.children; 111 | } 112 | 113 | currentRun.stack.pop(); 114 | } 115 | }; --------------------------------------------------------------------------------