├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── babel-plugin.js ├── index.js ├── package.json └── tests ├── _get-runtime.js ├── _mock-doc.js ├── entries ├── attrs.jsx ├── props.jsx ├── styles.jsx ├── tags.jsx └── text.jsx └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'iojs' 5 | - '0.12' 6 | - '0.10' -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jsx-ir/jsx-to-dom.svg?branch=master)](https://travis-ci.org/jsx-ir/jsx-to-dom) 2 | 3 | ## DOM Renderer for JSX-IR 4 | 5 | ### Installation 6 | 7 | ```npm install jsx-to-dom``` 8 | 9 | ### Usage 10 | 11 | #### Transpiling 12 | 13 | ```js 14 | babel.transform(code, { 15 | plugins: ['jsx-to-dom/babel-plugin'], 16 | blacklist: ['react'] 17 | }); 18 | ``` 19 | or any other way described [here](http://babeljs.io/docs/advanced/plugins/#usage), just pass `'jsx-to-dom/babel-plugin'`` as a plugin name. 20 | 21 | ### Runtime 22 | 23 | ```javascript 24 | import { render } from 'jsx-to-dom'; 25 | 26 | var element = render(
Hello World
); 27 | 28 | container.appendChild(element); 29 | ``` 30 | 31 | ## License 32 | 33 | [MIT](LICENSE.md) -------------------------------------------------------------------------------- /babel-plugin.js: -------------------------------------------------------------------------------- 1 | var htmlTags = require('html-tags'); 2 | var svgTags = require('svg-tags'); 3 | 4 | module.exports = require('babel-plugin-jsx/gen')({ 5 | captureScope: true, 6 | builtins: htmlTags.concat(svgTags) 7 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var jsx = require('jsx-runtime'); 4 | var hasOwn = Object.prototype.hasOwnProperty; 5 | 6 | var emptyTags = require('empty-tags').reduce(function(map, tag) { 7 | map[tag] = true; 8 | return map; 9 | }, Object.create(null)); 10 | 11 | var SVG_NS = 'http://www.w3.org/2000/svg'; 12 | var HTML_NS = 'http://www.w3.org/1999/xhtml'; 13 | 14 | var renderer = jsx.register('DOM', { 15 | before: function(element) { 16 | this.scope.namespaces = []; 17 | return element; 18 | }, 19 | tags: { 20 | '*': { 21 | enter: function(tag, props) { 22 | var namespaces = this.scope.namespaces; 23 | 24 | if (tag === 'svg') { 25 | namespaces.unshift(SVG_NS); 26 | } else if (tag === 'foreignObject') { 27 | namespaces.unshift(HTML_NS); 28 | } 29 | 30 | var element; 31 | 32 | if (namespaces.length) { 33 | element = document.createElementNS(namespaces[0], tag); 34 | } else { 35 | element = document.createElement(tag); 36 | } 37 | 38 | applyProps(element, props); 39 | 40 | return element; 41 | }, 42 | leave: function(parent, tag) { 43 | if ( 44 | tag === 'svg' && this.scope.namespaces[0] === SVG_NS || 45 | tag === 'foreignObject' && this.scope.namespaces[0] === HTML_NS 46 | ) { 47 | this.scope.namespaces.shift(); 48 | } 49 | 50 | return parent; 51 | }, 52 | child: function(child, parent) { 53 | if (child instanceof Element) { 54 | // do nothing 55 | } else { 56 | child = document.createTextNode(child + ''); 57 | } 58 | 59 | parent.appendChild(child); 60 | return parent; 61 | }, 62 | children: function(children, parent, tag) { 63 | if (typeof emptyTags[tag.toLowerCase()] !== 'undefined') { 64 | throw new Error('Tag <' + tag + ' /> cannot have children'); 65 | } 66 | 67 | return children; 68 | } 69 | } 70 | } 71 | }); 72 | 73 | module.exports = renderer; 74 | 75 | function applyStyle(element, style) { 76 | if (typeof style === 'string') { 77 | element.setAttribute('style', style); 78 | return; 79 | } 80 | 81 | var elementStyle = element.style; 82 | 83 | for (var key in style) { 84 | if (!hasOwn.call(style, key)) continue; 85 | 86 | elementStyle[key] = style[key]; 87 | } 88 | } 89 | 90 | function applyProps(element, props) { 91 | for (var key in props) { 92 | if (!hasOwn.call(props, key)) continue; 93 | 94 | var val = props[key]; 95 | 96 | switch (key) { 97 | case 'style': applyStyle(element, val); break; 98 | case 'class': element.className = val; break; 99 | case 'for': element.cssFor = val; break; 100 | 101 | case 'innerHTML': 102 | case 'outerHTML': 103 | case 'textContent': 104 | case 'innerText': 105 | case 'text': 106 | console.warn('Direct manipulation of tags content is not allowed'); 107 | break; 108 | default: { 109 | if (key.indexOf('-') !== -1) { 110 | element.setAttribute(key, val); 111 | } else { 112 | element[key] = val; 113 | } 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-to-dom", 3 | "version": "1.1.0", 4 | "description": "Render JSX-IR directly to DOM (e.g. document.createElement(...))", 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-dom", 12 | "dependencies": { 13 | "babel-plugin-jsx": "^1.1.0", 14 | "empty-tags": "^1.0.0", 15 | "html-tags": "^1.1.1", 16 | "jsx-runtime": "^1.1.0", 17 | "svg-tags": "^1.0.0" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^5.6.20", 21 | "mocha": "^2.2.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/_get-runtime.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Module = require('module'); 6 | 7 | var modulePaths = module.paths; 8 | 9 | function requireString(src, filePath, fileName) { 10 | var module = new Module(fileName); 11 | 12 | module.filename = filePath; 13 | module.paths = Module._nodeModulePaths(path.dirname(filePath)); 14 | module._compile(src, filePath); 15 | 16 | return module.exports; 17 | } 18 | 19 | module.exports = function(filePath) { 20 | filePath = path.join(__dirname, filePath || '../index.js'); 21 | 22 | var file = fs.readFileSync(filePath, 'utf-8'); 23 | var mock = fs.readFileSync(path.join(__dirname, '_mock-doc.js'), 'utf-8'); 24 | 25 | var fakeName = '../fake-runtime.js'; 26 | var module = requireString(mock + '\n' + file, path.join(__dirname, fakeName), fakeName); 27 | 28 | return module; 29 | }; -------------------------------------------------------------------------------- /tests/_mock-doc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var document = { 4 | createElement: function(tag) { 5 | return new Element(tag); 6 | }, 7 | createElementNS: function(ns, tag) { 8 | return new Element(tag, ns); 9 | }, 10 | createTextNode: function(str) { 11 | return new Text(str); 12 | } 13 | }; 14 | 15 | var Element = function(tag, ns) { 16 | this.tagName = tag; 17 | this.namespaceURI = ns || ''; 18 | this.attributes = {}; 19 | this.children = []; 20 | this.style = {}; 21 | this.nodeType = 0; 22 | }; 23 | 24 | Element.prototype = { 25 | appendChild: function(child) { 26 | if (!(child instanceof Element) && !(child instanceof Text)) { 27 | throw new Error('Incorrect child'); 28 | } 29 | 30 | this.children.push(child); 31 | }, 32 | setAttribute: function(name, value) { 33 | this.attributes[name + ''] = value + ''; 34 | }, 35 | 36 | __toMock: function(props) { 37 | var mock = { 38 | tag: this.tagName 39 | }; 40 | 41 | if (this.children.length) { 42 | mock.children = this.children.map(function(child) { 43 | return child instanceof Element ? child.__toMock() : child; 44 | }); 45 | } 46 | 47 | if (Object.keys(this.attributes).length) { 48 | mock.attributes = this.attributes; 49 | } 50 | 51 | if (Object.keys(this.style).length) { 52 | mock.style = this.style; 53 | } 54 | 55 | if (this.namespaceURI) { 56 | mock.namespaceURI = this.namespaceURI; 57 | } 58 | 59 | if (Array.isArray(props) && props.length) { 60 | mock.props = {}; 61 | props.forEach(function(prop) { 62 | mock.props[prop] = this[prop]; 63 | }, this); 64 | } 65 | 66 | return mock; 67 | } 68 | }; 69 | 70 | var Text = function(text) { 71 | this.nodeType = '#text'; 72 | this.value = text + ''; 73 | }; -------------------------------------------------------------------------------- /tests/entries/attrs.jsx: -------------------------------------------------------------------------------- 1 | var runtime = require('../_get-runtime.js')(); 2 | var assert = require('assert'); 3 | 4 | export var simple = () => { 5 | let elem = runtime.render( 6 |
7 | ).__toMock(); 8 | 9 | assert.deepEqual( 10 | elem, { 11 | tag: 'div', 12 | attributes: { 13 | 'data-test': 'test' 14 | } 15 | } 16 | ); 17 | }; 18 | 19 | export var js_values = () => { 20 | let elem = runtime.render( 21 |
28 | ).__toMock(); 29 | 30 | assert.deepEqual( 31 | elem, { 32 | tag: 'div', 33 | attributes: { 34 | 'data-boolean': 'true', 35 | 'data-number': '1', 36 | 'data-string': 'str', 37 | 'data-object': '[object Object]', 38 | 'data-array': '1,2,3' 39 | } 40 | } 41 | ); 42 | }; 43 | 44 | export var prop_isnt_attr = () => { 45 | let elem = runtime.render( 46 |
47 | ).__toMock(); 48 | 49 | assert.deepEqual( 50 | elem, { 51 | tag: 'div', 52 | attributes: { 53 | 'data-test': 'test' 54 | } 55 | } 56 | ); 57 | }; -------------------------------------------------------------------------------- /tests/entries/props.jsx: -------------------------------------------------------------------------------- 1 | var runtime = require('../_get-runtime.js')(); 2 | var assert = require('assert'); 3 | 4 | export var simple = () => { 5 | let elem = runtime.render( 6 |
9 | ).__toMock(['className']); 10 | 11 | assert.deepEqual( 12 | elem, { 13 | tag: 'div', 14 | props: { 15 | className: 'test' 16 | } 17 | } 18 | ); 19 | }; 20 | 21 | export var js_values = () => { 22 | let elem = runtime.render( 23 |
30 | ).__toMock([ 31 | '_boolean', 32 | '_number', 33 | '_string', 34 | '_object', 35 | '_array' 36 | ]); 37 | 38 | assert.deepEqual( 39 | elem, { 40 | tag: 'div', 41 | props: { 42 | _boolean: true, 43 | _number: 1, 44 | _string: 'str', 45 | _object: {}, 46 | _array: [1, 2, 3] 47 | } 48 | } 49 | ); 50 | }; 51 | 52 | export var attr_isnt_prop = () => { 53 | let elem = runtime.render( 54 |
55 | ).__toMock(['className', '_prop', 'data-test']); 56 | 57 | assert.deepEqual( 58 | elem, { 59 | tag: 'div', 60 | attributes: { 61 | 'data-test': 'test' 62 | }, 63 | props: { 64 | className: 'test', 65 | _prop: true, 66 | 'data-test': void 0 67 | } 68 | } 69 | ); 70 | } 71 | 72 | export var props_transform = () => { 73 | let elem = runtime.render( 74 |
75 | ).__toMock(['className', 'cssFor', 'class', 'for']); 76 | 77 | assert.deepEqual( 78 | elem, { 79 | tag: 'div', 80 | props: { 81 | className: 'test', 82 | cssFor: 'thing', 83 | 'class': void 0, 84 | 'for': void 0 85 | } 86 | } 87 | ); 88 | } 89 | 90 | export var content_manipulation = () => { 91 | let originalWarn = global.console.warn; 92 | global.console.warn = function() {}; 93 | 94 | let elem = runtime.render( 95 |
102 | ).__toMock([ 103 | 'innerHTML', 104 | 'outerHTML', 105 | 'textContent', 106 | 'innerText', 107 | 'text' 108 | ]); 109 | 110 | global.console.warn = originalWarn; 111 | 112 | assert.deepEqual( 113 | elem, { 114 | tag: 'div', 115 | props: { 116 | 'innerHTML': void 0, 117 | 'outerHTML': void 0, 118 | 'textContent': void 0, 119 | 'innerText': void 0, 120 | 'text': void 0, 121 | } 122 | } 123 | ); 124 | } -------------------------------------------------------------------------------- /tests/entries/styles.jsx: -------------------------------------------------------------------------------- 1 | var runtime = require('../_get-runtime.js')(); 2 | var assert = require('assert'); 3 | 4 | export var object = () => { 5 | let elem = runtime.render( 6 |
11 | ).__toMock(); 12 | 13 | assert.deepEqual( 14 | elem, { 15 | tag: 'div', 16 | style: { 17 | color: 'black' 18 | } 19 | } 20 | ); 21 | }; 22 | 23 | export var string = () => { 24 | let elem = runtime.render( 25 |
28 | ).__toMock(); 29 | 30 | assert.deepEqual( 31 | elem, { 32 | tag: 'div', 33 | attributes: { 34 | style: 'color: black' 35 | } 36 | } 37 | ); 38 | }; -------------------------------------------------------------------------------- /tests/entries/tags.jsx: -------------------------------------------------------------------------------- 1 | var runtime = require('../_get-runtime.js')(); 2 | var assert = require('assert'); 3 | var emptyTags = require('empty-tags'); 4 | 5 | var SVG_NS = 'http://www.w3.org/2000/svg'; 6 | var HTML_NS = 'http://www.w3.org/1999/xhtml'; 7 | 8 | export var div = () => { 9 | let elem = runtime.render(
); 10 | 11 | assert.equal( 12 | elem.tagName, 'div' 13 | ); 14 | }; 15 | 16 | export var svg_namespace = () => { 17 | let elem = runtime.render( 18 |
19 | 20 |
21 | ).__toMock(); 22 | 23 | assert.deepEqual(elem, { 24 | tag: 'div', 25 | children: [{ 26 | tag: 'svg', 27 | namespaceURI: SVG_NS 28 | }] 29 | }); 30 | }; 31 | 32 | export var html_namespace = () => { 33 | let elem = runtime.render( 34 |
35 | 36 | 37 | 38 |
39 | ).__toMock(); 40 | 41 | assert.deepEqual(elem, { 42 | tag: 'div', 43 | children: [{ 44 | tag: 'svg', 45 | namespaceURI: SVG_NS, 46 | children: [{ 47 | tag: 'foreignObject', 48 | namespaceURI: HTML_NS 49 | }] 50 | }] 51 | }); 52 | }; 53 | 54 | export var custom_tags = () => { 55 | let elem = runtime.render( 56 |
57 | 58 |
59 | ).__toMock(); 60 | 61 | assert.deepEqual(elem, { 62 | tag: 'div', 63 | children: [{ 64 | tag: 'custom-tag' 65 | }] 66 | }); 67 | }; 68 | 69 | export var scope_tags = () => { 70 | var Scoped = function() { 71 | return 72 | }; 73 | 74 | let elem = runtime.render( 75 |
76 | 77 |
78 | ).__toMock(); 79 | 80 | assert.deepEqual(elem, { 81 | tag: 'div', 82 | children: [{ 83 | tag: 'span' 84 | }] 85 | }); 86 | }; 87 | 88 | export var empty_tags = () => { 89 | emptyTags.forEach(function(tag, i) { 90 | var Empty = function() { 91 | return { 92 | tag: tag, 93 | children: [
], 94 | props: null 95 | } 96 | }; 97 | 98 | assert.throws(function() { 99 | runtime.render(); 100 | }, function(e) { 101 | if (e.message === 'Tag <' + tag + ' /> cannot have children') { 102 | return true; 103 | } 104 | }, 'Tag <' + tag + '> cannot have children, but it has'); 105 | }); 106 | } -------------------------------------------------------------------------------- /tests/entries/text.jsx: -------------------------------------------------------------------------------- 1 | var runtime = require('../_get-runtime.js')(); 2 | var assert = require('assert'); 3 | 4 | var SVG_NS = 'http://www.w3.org/2000/svg'; 5 | var HTML_NS = 'http://www.w3.org/1999/xhtml'; 6 | 7 | export var simple = () => { 8 | let elem = runtime.render( 9 |
10 | text 11 |
12 | ).__toMock(); 13 | 14 | assert.deepEqual( 15 | elem, { 16 | tag: 'div', 17 | children: [{ 18 | nodeType: '#text', 19 | value: 'text' 20 | }] 21 | } 22 | ); 23 | }; -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------