diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9c378bd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,.*rc,*.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..dd44972 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +*.md diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..3a94731 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,58 @@ +{ + "parser": "babel-eslint", + "extends": "eslint:recommended", + "env": { + "browser": true, + "mocha": true, + "es6": true + }, + "parserOptions": { + "ecmaFeatures": { + "modules": true, + "jsx": true + } + }, + "globals": { + "require": true + }, + "rules": { + "no-empty": 0, + "no-console": 0, + "no-unused-vars": [0, { "varsIgnorePattern": "^h$" }], + "no-cond-assign": 1, + "semi": 2, + "camelcase": 0, + "comma-style": 2, + "comma-dangle": [2, "never"], + "indent": [2, "tab", {"SwitchCase": 1}], + "no-mixed-spaces-and-tabs": [2, "smart-tabs"], + "no-trailing-spaces": [2, { "skipBlankLines": true }], + "max-nested-callbacks": [2, 3], + "no-eval": 2, + "no-implied-eval": 2, + "no-new-func": 2, + "guard-for-in": 2, + "eqeqeq": 1, + "no-else-return": 2, + "no-redeclare": 2, + "no-dupe-keys": 2, + "radix": 2, + "strict": [2, "never"], + "no-shadow": 0, + "no-delete-var": 2, + "no-undef-init": 2, + "no-shadow-restricted-names": 2, + "handle-callback-err": 0, + "no-lonely-if": 2, + "keyword-spacing": 2, + "constructor-super": 2, + "no-this-before-super": 2, + "no-dupe-class-members": 2, + "no-const-assign": 2, + "prefer-spread": 2, + "no-useless-concat": 2, + "no-var": 2, + "object-shorthand": 2, + "prefer-arrow-callback": 2 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72e9b45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/dist +/node_modules +/npm-debug.log +.DS_Store diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..4c2095a --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +.eslintrc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8524235 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 4 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..38d8969 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jason Miller + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd09fd0 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# vhtml + +[![NPM](https://img.shields.io/npm/v/vhtml.svg?style=flat)](https://www.npmjs.org/package/vhtml) +[![travis-ci](https://travis-ci.org/developit/vhtml.svg?branch=master)](https://travis-ci.org/developit/vhtml) + +### **Render JSX/Hyperscript to HTML strings, without VDOM** + +> Need to use HTML strings (angular?) but want to use JSX? vhtml's got your back. + + +--- + + +## Installation + +Via npm: + +`npm install --save vhtml` + + +--- + + +## Usage + +```js +// import the library: +import h from 'vhtml'; + +// tell babel to transpile JSX to h() calls: +/** @jsx h */ + +// now render JSX to an HTML string! +let items = ['one', 'two', 'three']; + +document.body.innerHTML = ( +
+

Hi!

+

Here is a list of {items.length} items:

+ +
+); +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..f716f83 --- /dev/null +++ b/package.json @@ -0,0 +1,71 @@ +{ + "name": "vhtml", + "amdName": "vhtml", + "version": "1.0.0", + "description": "Hyperscript reviver that constructs a sanitized HTML string.", + "main": "dist/vhtml.js", + "minified:main": "dist/vhtml.min.js", + "jsnext:main": "src/vhtml.js", + "scripts": { + "build": "npm-run-all transpile minify size", + "transpile": "rollup -c rollup.config.js", + "minify": "uglifyjs $npm_package_main -cm -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map", + "size": "echo \"gzip size: $(gzip-size $npm_package_minified_main | pretty-bytes)\"", + "test": "eslint {src,test} && mocha --compilers js:babel-register test/**/*.js", + "prepublish": "npm-run-all build test", + "release": "npm run -s build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" + }, + "babel": { + "presets": [ + "es2015-minimal", + "stage-0", + "react" + ], + "plugins": [ + "transform-object-rest-spread", + [ + "transform-react-jsx", + { + "pragma": "h" + } + ] + ] + }, + "keywords": [ + "hyperscript", + "html", + "renderer", + "strings" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/developit/vhtml.git" + }, + "author": "Jason Miller ", + "license": "MIT", + "bugs": { + "url": "https://github.com/developit/vhtml/issues" + }, + "homepage": "https://github.com/developit/vhtml", + "devDependencies": { + "babel-core": "^6.6.4", + "babel-eslint": "^5.0.0", + "babel-plugin-transform-object-rest-spread": "^6.6.4", + "babel-plugin-transform-react-jsx": "^6.6.5", + "babel-preset-es2015-minimal": "^1.1.0", + "babel-preset-es2015-minimal-rollup": "^1.1.0", + "babel-preset-react": "^6.5.0", + "babel-preset-stage-0": "^6.5.0", + "babel-register": "^6.7.2", + "chai": "^3.5.0", + "eslint": "~2.2.0", + "gzip-size-cli": "^1.0.0", + "mkdirp": "^0.5.1", + "mocha": "^2.4.5", + "npm-run-all": "^1.5.1", + "pretty-bytes-cli": "^1.0.0", + "rollup": "^0.25.4", + "rollup-plugin-babel": "^2.4.0", + "uglify-js": "^2.6.2" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..0de39c9 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,28 @@ +import path from 'path'; +import fs from 'fs'; +import babel from 'rollup-plugin-babel'; + +let pkg = JSON.parse(fs.readFileSync('./package.json')); +let external = Object.keys(pkg.peerDependencies || {}).concat(Object.keys(pkg.dependencies || {})); + +export default { + entry: pkg['jsnext:main'], + dest: pkg.main, + sourceMap: path.resolve(pkg.main), + moduleName: pkg.amdName, + format: 'umd', + external, + plugins: [ + babel({ + babelrc: false, + comments: false, + exclude: 'node_modules/**', + presets: [ + 'es2015-minimal-rollup' + ].concat(pkg.babel.presets.slice(1)), + plugins: require('babel-preset-es2015-minimal-rollup').plugins.concat([ + ['transform-react-jsx', { pragma:'h' }] + ]) + }) + ] +}; diff --git a/src/vhtml.js b/src/vhtml.js new file mode 100644 index 0000000..a63a134 --- /dev/null +++ b/src/vhtml.js @@ -0,0 +1,36 @@ +// escape an attribute +let esc = str => String(str).replace(/[&<>"']/g, s=>`&${map[s]};`); +let map = {'&':'amp','<':'lt','>':'gt','"':'quot',"'":'apos'}; + +// sanitize text children and filter out falsey values +let child = s => truthy(s) ? (sanitized[s]===true ? s : esc(s)) : ''; + +// check that a value is not false, undefined or null +let truthy = v => v!==false && v!=null; + +let sanitized = {}; + +/** Hyperscript reviver that constructs a sanitized HTML string. */ +export default function h(name, attrs, ...children) { + let s = `<${name}`; + if (attrs) for (let i in attrs) { + if (attrs.hasOwnProperty(i) && truthy(attrs[i])) { + s += ` ${esc(i)}="${esc(attrs[i])}"`; + } + } + s += `>${[].concat(...children).map(child).join('')}`; + sanitized[s] = true; + return s; +} + + + +// for fun: +/* +export default const h = (tag, attrs, ...kids) => ( + `<${tag}${h.attrs(attrs)}>${[].concat(...kids).join('')}` +); +h.attrs = a => Object.keys(a || {}).reduce( (s,i) => `${s} ${h.esc(i)}="${h.esc(a[i]+'')}"`, ''); +h.esc = str => str.replace(/[&<>"']/g, s=>`&${h.map[s]};`); +h.map = {'&':'amp','<':'lt','>':'gt','"':'quot',"'":'apos'}; +*/ diff --git a/test/vhtml.js b/test/vhtml.js new file mode 100644 index 0000000..51550df --- /dev/null +++ b/test/vhtml.js @@ -0,0 +1,42 @@ +import h from '../src/vhtml'; +import { expect } from 'chai'; +/** @jsx h */ +/*global describe,it*/ + +describe('vhtml', () => { + it('should stringify html', () => { + let items = ['one', 'two', 'three']; + expect( +
+

Hi!

+

Here is a list of {items.length} items:

+ +
+ ).to.equal( + `

Hi!

Here is a list of 3 items:

` + ); + }); + + it('should sanitize children', () => { + expect( +
+ { `blocked` } + allowed +
+ ).to.equal( + `
<strong>blocked</strong>allowed
` + ); + }); + + it('should sanitize attributes', () => { + expect( +
"'`} /> + ).to.equal( + `
` + ); + }); +});