├── .gitignore ├── index.js ├── license ├── package-lock.json ├── package.json ├── readme.md └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var DOUBLE_QUOTES = /"/g 3 | var VOID = ['img', 'input', 'meta', 'br', 'wbr', 'embed', 'area', 'base', 'col', 4 | 'link', 'param', 'source', 'track', 'circle', 'ellipse', 'line', 'mesh', 5 | 'path', 'polygon', 'polyline', 'rect'] 6 | 7 | module.exports = function h (tag, data, children) { 8 | var attrs = '' 9 | for (var attr in data) { 10 | attrs += ' ' + attr + '="' + (data[attr] + '').replace(DOUBLE_QUOTES, '\\"') + '"' 11 | } 12 | 13 | var el = '<' + tag + attrs 14 | 15 | if (VOID.indexOf(tag) !== -1 && !children) { 16 | return el + '/>' 17 | } 18 | 19 | return el + '>' + (Array.isArray(children) ? children.join('') : children || '') + '' 20 | } 21 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Jamen Marz 3 | 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, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h2ml", 3 | "version": "1.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "0.4.2", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", 10 | "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.7", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", 16 | "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "0.4.2", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "concat-map": { 24 | "version": "0.0.1", 25 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 26 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 27 | "dev": true 28 | }, 29 | "deep-equal": { 30 | "version": "1.0.1", 31 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 32 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", 33 | "dev": true 34 | }, 35 | "define-properties": { 36 | "version": "1.1.2", 37 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", 38 | "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", 39 | "dev": true, 40 | "requires": { 41 | "foreach": "2.0.5", 42 | "object-keys": "1.0.11" 43 | } 44 | }, 45 | "defined": { 46 | "version": "1.0.0", 47 | "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", 48 | "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", 49 | "dev": true 50 | }, 51 | "es-abstract": { 52 | "version": "1.7.0", 53 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz", 54 | "integrity": "sha1-363ndOAb/Nl/lhgCmMRJyGI/uUw=", 55 | "dev": true, 56 | "requires": { 57 | "es-to-primitive": "1.1.1", 58 | "function-bind": "1.1.0", 59 | "is-callable": "1.1.3", 60 | "is-regex": "1.0.4" 61 | } 62 | }, 63 | "es-to-primitive": { 64 | "version": "1.1.1", 65 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", 66 | "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", 67 | "dev": true, 68 | "requires": { 69 | "is-callable": "1.1.3", 70 | "is-date-object": "1.0.1", 71 | "is-symbol": "1.0.1" 72 | } 73 | }, 74 | "for-each": { 75 | "version": "0.3.2", 76 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz", 77 | "integrity": "sha1-LEBFC5NI6X8oEyJZO6lnBLmr1NQ=", 78 | "dev": true, 79 | "requires": { 80 | "is-function": "1.0.1" 81 | } 82 | }, 83 | "foreach": { 84 | "version": "2.0.5", 85 | "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", 86 | "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", 87 | "dev": true 88 | }, 89 | "fs.realpath": { 90 | "version": "1.0.0", 91 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 92 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 93 | "dev": true 94 | }, 95 | "function-bind": { 96 | "version": "1.1.0", 97 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", 98 | "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", 99 | "dev": true 100 | }, 101 | "glob": { 102 | "version": "7.1.2", 103 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 104 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 105 | "dev": true, 106 | "requires": { 107 | "fs.realpath": "1.0.0", 108 | "inflight": "1.0.6", 109 | "inherits": "2.0.3", 110 | "minimatch": "3.0.4", 111 | "once": "1.4.0", 112 | "path-is-absolute": "1.0.1" 113 | } 114 | }, 115 | "has": { 116 | "version": "1.0.1", 117 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 118 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 119 | "dev": true, 120 | "requires": { 121 | "function-bind": "1.1.0" 122 | } 123 | }, 124 | "inflight": { 125 | "version": "1.0.6", 126 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 127 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 128 | "dev": true, 129 | "requires": { 130 | "once": "1.4.0", 131 | "wrappy": "1.0.2" 132 | } 133 | }, 134 | "inherits": { 135 | "version": "2.0.3", 136 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 137 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 138 | "dev": true 139 | }, 140 | "is-callable": { 141 | "version": "1.1.3", 142 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", 143 | "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", 144 | "dev": true 145 | }, 146 | "is-date-object": { 147 | "version": "1.0.1", 148 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 149 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 150 | "dev": true 151 | }, 152 | "is-function": { 153 | "version": "1.0.1", 154 | "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz", 155 | "integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=", 156 | "dev": true 157 | }, 158 | "is-regex": { 159 | "version": "1.0.4", 160 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 161 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 162 | "dev": true, 163 | "requires": { 164 | "has": "1.0.1" 165 | } 166 | }, 167 | "is-symbol": { 168 | "version": "1.0.1", 169 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", 170 | "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", 171 | "dev": true 172 | }, 173 | "minimatch": { 174 | "version": "3.0.4", 175 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 176 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 177 | "dev": true, 178 | "requires": { 179 | "brace-expansion": "1.1.7" 180 | } 181 | }, 182 | "minimist": { 183 | "version": "1.2.0", 184 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 185 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 186 | "dev": true 187 | }, 188 | "object-inspect": { 189 | "version": "1.2.2", 190 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.2.2.tgz", 191 | "integrity": "sha1-yCEV5PzIiK6hTWTCLk8X9qcNXlo=", 192 | "dev": true 193 | }, 194 | "object-keys": { 195 | "version": "1.0.11", 196 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", 197 | "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", 198 | "dev": true 199 | }, 200 | "once": { 201 | "version": "1.4.0", 202 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 203 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 204 | "dev": true, 205 | "requires": { 206 | "wrappy": "1.0.2" 207 | } 208 | }, 209 | "path-is-absolute": { 210 | "version": "1.0.1", 211 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 212 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 213 | "dev": true 214 | }, 215 | "resolve": { 216 | "version": "1.1.7", 217 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", 218 | "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", 219 | "dev": true 220 | }, 221 | "resumer": { 222 | "version": "0.0.0", 223 | "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", 224 | "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", 225 | "dev": true, 226 | "requires": { 227 | "through": "2.3.8" 228 | } 229 | }, 230 | "string.prototype.trim": { 231 | "version": "1.1.2", 232 | "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", 233 | "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", 234 | "dev": true, 235 | "requires": { 236 | "define-properties": "1.1.2", 237 | "es-abstract": "1.7.0", 238 | "function-bind": "1.1.0" 239 | } 240 | }, 241 | "tape": { 242 | "version": "4.6.3", 243 | "resolved": "https://registry.npmjs.org/tape/-/tape-4.6.3.tgz", 244 | "integrity": "sha1-Y353WB6ass4XV36b1M5PV1gG2LY=", 245 | "dev": true, 246 | "requires": { 247 | "deep-equal": "1.0.1", 248 | "defined": "1.0.0", 249 | "for-each": "0.3.2", 250 | "function-bind": "1.1.0", 251 | "glob": "7.1.2", 252 | "has": "1.0.1", 253 | "inherits": "2.0.3", 254 | "minimist": "1.2.0", 255 | "object-inspect": "1.2.2", 256 | "resolve": "1.1.7", 257 | "resumer": "0.0.0", 258 | "string.prototype.trim": "1.1.2", 259 | "through": "2.3.8" 260 | } 261 | }, 262 | "through": { 263 | "version": "2.3.8", 264 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 265 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 266 | "dev": true 267 | }, 268 | "wrappy": { 269 | "version": "1.0.2", 270 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 271 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 272 | "dev": true 273 | }, 274 | "xss-filters": { 275 | "version": "1.2.7", 276 | "resolved": "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.7.tgz", 277 | "integrity": "sha1-Wfod4gHzby80cNysX1jMwoMLCpo=", 278 | "dev": true 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h2ml", 3 | "description": "An h function that returns HTML (or XML) strings.", 4 | "author": "Jamen Marz (http://jamenmarz.com)", 5 | "version": "1.1.0", 6 | "repository": "jamen/h2ml", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "node test" 10 | }, 11 | "devDependencies": { 12 | "tape": "^4.6.3", 13 | "xss-filters": "^1.2.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # h2ml 3 | 4 | > An h function that returns HTML (or XML) strings. 5 | 6 | ```js 7 | const h = require('h2ml') 8 | 9 | h('div', null, [ 10 | h('span', { class: 'title' }, 'Hello world'), 11 | h('p', { class: 'body' }, 12 | 'Autem placeat illo libero voluptatem dolorem. ' + 13 | 'Ut ' + h('b', null, 'consequatur neque harum') + ' sed molestias.' 14 | ) 15 | ]) 16 | ``` 17 | 18 | Injected content *is not* XSS secured and should be combined with a library like [`xss-filters`](https://npmjs.com/xss-filters) 19 | 20 | ```js 21 | const h = require('h2ml') 22 | const secure = require('xss-filters') 23 | 24 | // Example if a user sent a script tag 25 | const data = '' 26 | 27 | h('span', null, secure.inHTMLData(data)) 28 | // => '<script>alert("hacked nerd")</script>' 29 | ``` 30 | 31 | ## Usage 32 | 33 | This package follows the [h2spec guidelines](https://github.com/hyper2/h2spec). 34 | 35 | ### `h(tag, data?, children?)` 36 | 37 | - `tag`: The element name. 38 | - `data` (optional): An object containing the attributes to set on the element. 39 | - `children` (optional): A string or array of strings. 40 | 41 | ```js 42 | h('div', { class: 'foo' }, 'hello world') 43 | // '
hello world
' 44 | 45 | h('div', null, [ 46 | h('span', 'foo'), 47 | h('span', 'bar') 48 | ]) 49 | // '
foobar
' 50 | ``` 51 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const h = require('./') 3 | const secure = require('xss-filters') 4 | 5 | test('html strings', t => { 6 | t.plan(8) 7 | 8 | t.is( 9 | h('div', { class: 'foo', type: 'test' }, 'bar'), 10 | '
bar
', 11 | 'single element' 12 | ) 13 | 14 | t.is( 15 | h('div', { class: 'bar', type: 'test' }, [ 16 | h('span', null, 'foo'), 17 | h('span', null, 'bar') 18 | ]), 19 | '
foobar
', 20 | 'basic nesting' 21 | ) 22 | 23 | t.is( 24 | h('img', { src: 'dat://example.com' }), 25 | '', 26 | 'void element' 27 | ) 28 | 29 | t.is( 30 | h('p', { test: 'foo "bar"' }, 'Hello world'), 31 | '

Hello world

', 32 | 'escapes quotes' 33 | ) 34 | 35 | t.is( 36 | h('canvas'), 37 | '', 38 | 'no body' 39 | ) 40 | 41 | t.is( 42 | h('div', null, [ 43 | h('span', { class: 'title' }, 'Hello world'), 44 | h('p', { class: 'body' }, 45 | 'Autem placeat illo libero voluptatem dolorem. ' + 46 | 'Ut ' + h('b', null, 'consequatur neque harum') + ' sed molestias.' 47 | ) 48 | ]), 49 | '
Hello world

Autem placeat illo libero voluptatem dolorem. Ut consequatur neque harum sed molestias.

', 50 | 'complex example' 51 | ) 52 | 53 | t.is( 54 | h('script', { async: true }, 'foo()'), 55 | '', 56 | 'got non-string data' 57 | ) 58 | 59 | t.is( 60 | h('path', { d: 'M0,100' }, [ h('boo') ]), 61 | '', 62 | 'allow overriding void elements' 63 | ) 64 | }) 65 | 66 | test('xss example', t => { 67 | t.plan(1) 68 | 69 | var data = '' 70 | 71 | t.is( 72 | h('span', null, secure.inHTMLData(data)), 73 | '<script>alert("hacked nerd")</script>' 74 | ) 75 | }) 76 | --------------------------------------------------------------------------------