├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── cjs ├── index.js ├── package.json ├── ucontent.js └── utils.js ├── esm ├── index.js ├── ucontent.js └── utils.js ├── package.json ├── test ├── base.js ├── benchmark.js ├── counter-fe.js ├── counter.js ├── index.js ├── package.json ├── pelo-app.js ├── pelo.js └── view.js └── ucontent-head.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | rollup/ 4 | test/ 5 | package-lock.json 6 | uhtml-head.jpg 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | after_success: 10 | - "npm run coveralls" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # µcontent 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/ucontent.svg?branch=master)](https://travis-ci.com/WebReflection/ucontent) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/ucontent/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/ucontent?branch=master) 4 | 5 | ![sunflowers](./ucontent-head.jpg) 6 | 7 | **Social Media Photo by [Bonnie Kittle](https://unsplash.com/@bonniekdesign) on [Unsplash](https://unsplash.com/)** 8 | 9 | ### 📣 Community Announcement 10 | 11 | Please ask questions in the [dedicated forum](https://webreflection.boards.net/) to help the community around this project grow ♥ 12 | 13 | --- 14 | 15 | A micro **SSR** oriented HTML/SVG content generator, but if you are looking for a micro **FE** content generator, check _[µhtml](https://github.com/WebReflection/uhtml#readme)_ out. 16 | 17 | ```js 18 | const {render, html} = require('ucontent'); 19 | const fs = require('fs'); 20 | 21 | const stream = fs.createWriteStream('test.html'); 22 | stream.once('open', () => { 23 | render( 24 | stream, 25 | html`

It's ${new Date}!

` 26 | ).end(); 27 | }); 28 | ``` 29 | 30 | ### V2 Breaking Change 31 | 32 | The recently introduced `data` helper [could conflict](https://github.com/WebReflection/uhtml/issues/14) with some node such as ``, hence it has been replaced by the `.dataset` utility. Since `element.dataset = object` is an invalid operation, the sugar to simplify `data-` attributes is now never ambiguous and future-proof: `` it is. 33 | 34 | This is aligned with _µhtml_ and _lighterhtml_ recent changes too. 35 | 36 | 37 | ## API 38 | 39 | * a `render(writable, what)` utility, to render in a `response` or `stream` object, via `writable.write(content)`, or through a callback, the content provided by one of the tags. The function returns the result of `callback(content)` invoke, or the the passed first parameter as is (i.e. the `response` or the `stream`). Please note this helper is _not mandatory_ to render content, as any content is an instance of `String`, so that if you prefer to render it manually, you can always use directly `content.toString()` instead, as every tag returns a specialized instance of _String_. This API _doesn't_ set any explicit headers for `response` objects based on `what`. 40 | * a `html` tag, to render _HTML_ content. Each interpolation passed as layout content, can be either a result from `html`, `css`, `js`, `svg`, or `raw` tag, as well as primitives, such as `string`, `boolean`, `number`, or even `null` or `undefined`. The result is a specialized instance of `String` with a `.min()` method to produce eventually minified _HTML_ content via [html-minifier](https://www.npmjs.com/package/html-minifier). All layout content, if not specialized, will be safely escaped, while attributes will always be escaped to avoid layout malfunctions. 41 | * a `svg` tag, identical to the `html` one, except minification would preserve any self-closing tag, as in ``. 42 | * a `css` tag, to create _CSS_ content. Its interpolations will be stringified, and it returns a specialized instance of `String` with a `.min()` method to produce eventually minified _CSS_ content via [csso](https://www.npmjs.com/package/csso). If passed as `html` or `svg` tag interpolation content, `.min()` will be automatically invoked. 43 | * a `js` tag, to create _JS_ content. Its interpolations will be stringified, and it returns a specialized instance of `String` with a `.min()` method to produce eventually minified _JS_ content via [terser](https://www.npmjs.com/package/terser). If passed as `html` or `svg` tag interpolation content, `.min()` will be automatically invoked. 44 | * a `raw` tag, to pass along interpolated _HTML_ or _SVG_ values any kind of content, even partial one, or a broken, layout. 45 | 46 | Both `html` and `svg` supports [µhtml](https://github.com/WebReflection/uhtml#readme) utilities but exclusively for feature parity (`html.for(...)` and `html.node` are simply aliases for the `html` function). 47 | 48 | Except for `html` and `svg` tags, all other tags can be used as regular functions, as long as the passed value is a string, or a specialized instance. 49 | 50 | This allow content to be retrieved a part and then be used as is within these tags. 51 | 52 | ```js 53 | import {readFileSync} from 'fs'; 54 | const code = js(readFileSync('./code.js')); 55 | const style = css(readFileSync('./style.css')); 56 | const partial = raw(readFileSync('./partial.html')); 57 | 58 | const head = title => html` 59 | 60 | ${title} 61 | 62 | 63 | 64 | `; 65 | 66 | const body = () => html`${partial}`; 67 | 68 | const page = title => html` 69 | 70 | 71 | ${head(title)} 72 | ${body()} 73 | 74 | `; 75 | ``` 76 | 77 | All pre-generated content can be passed along, automatically avoiding minification of the same content per each request. 78 | 79 | ```js 80 | // will be re-used and minified only once 81 | const jsContent = js`/* same JS code to serve */`; 82 | const cssContent = css`/* same CSS content to serve */`; 83 | 84 | require('http') 85 | .createServer((request, response) => { 86 | response.writeHead(200, {'content-type': 'text/html;charset=utf-8'}); 87 | render(response, html` 88 | 89 | 90 | 91 | µcontent 92 | 93 | 94 | 95 | 96 | `.min()).end(); 97 | }) 98 | .listen(8080); 99 | ``` 100 | 101 | If one of the _HTML_ interpolations is `null` or `undefined`, an empty string will be placed instead. 102 | 103 | > _Note:_ When writing to `stream` objects using the `render()` API make sure to call end on it 104 | 105 | 106 | 107 | ## Production: HTML + SVG Implicit Minification 108 | 109 | While both utilities expose a `.min()` helper, repeated minification of big chunks of layout can be quite expensive. 110 | 111 | As the template literal is the key to map updates, which happen before `.min()` gets invoked, it is necessary to tell upfront if such template should be minified or not, so that reusing the same template later on, would result into a pre-minified set of chunks. 112 | 113 | In order to do so, `html` and `svg` expose a `minified` boolean property, which is `false` by default, but it can be switched to `true` in production. 114 | 115 | ```js 116 | import {render, html, svg} from 'ucontent'; 117 | 118 | // enable pre minified chunks 119 | const {PRODUCTION} = process.env; 120 | html.minified = !!PRODUCTION; 121 | svg.minified = !!PRODUCTION; 122 | 123 | const page = () => html` 124 | 125 | 126 |

127 | This will always be minified 128 |

129 |

130 | ${Date.now()} + ${Math.random()} 131 |

132 | 133 | `; 134 | // note, no .min() necessary 135 | 136 | render(response, page()).end(); 137 | ``` 138 | 139 | In this way, local tests would have a clean layout, while production code will always be minified, where each template literal will be minified once, instead of each time `.min()` is invoked. 140 | 141 | 142 | 143 | ## Attributes Logic 144 | 145 | * as it is for _µhtml_ too, sparse attributes are not supported: this is ok `attr=${value}`, but this is wrong: `attr="${x} and ${y}"`. 146 | * all attributes are safely escaped by default. 147 | * if an attribute value is `null` or `undefined`, the attribute won't show up in the layout. 148 | * `aria=${object}` attributes are assigned _hyphenized_ as `aria-a11y` attributes. The `role` is passed instead as `role=...`. 149 | * `style=${css...}` attributes are minified, if the interpolation value is passed as `css` tag. 150 | * `.dataset=${object}` setter is assigned _hyphenized_ as `data-user-land` attributes. 151 | * `.contentEditable=${...}`, `.disabled=${...}` and any attribute defined as setter, will not be in the layout if the passed value is `null`, `undefined`, or `false`, it will be in the layout if the passed value is `true`, it will contain escaped value in other cases. The attribute is normalized without the dot prefix, and lower-cased. 152 | * `on...=${'...'}` events passed as string or passed as `js` tag will be preserved, and in the `js` tag case, minified. 153 | * `on...=${...}` events that pass a callback will be ignored, as it's impossible to bring scope in the layout. 154 | 155 | 156 | 157 | ## Benchmark 158 | 159 | Directly from [pelo](https://github.com/shuhei/pelo#readme) project but without listeners, as these are mostly useless for SSR. 160 | 161 | Rendering a simple view 10,000 times: 162 | 163 | ```js 164 | node test/pelo.js 165 | ``` 166 | 167 | | tag | time (ms) | 168 | | -------- | ---------- | 169 | | ucontent | 117.668ms | 170 | | pelo | 129.332ms | 171 | 172 | 173 | 174 | ## How To Live Test 175 | 176 | Create a `test.js` file in any folder you like, then `npm i ucontent` in that very same folder. 177 | 178 | Write the following in the `test.js` file and save it: 179 | 180 | ```js 181 | const {render, html} = require('ucontent'); 182 | 183 | require('http').createServer((req, res) => { 184 | res.writeHead(200, {'content-type': 'text/html;charset=utf-8'}); 185 | render(res, html` 186 | 187 | 188 | 189 | 190 | 191 | ucontent 192 | 193 | ${html` 194 |

Hello There

195 |

196 | Thank you for trying µcontent at ${new Date()} 197 |

198 | `} 199 | 200 | `) 201 | .end(); 202 | }).listen(8080); 203 | ``` 204 | 205 | You can now `node test.js` and reach [localhost:8080](http://localhost:8080/), to see the page layout generated. 206 | 207 | If you'd like to test the minified version of that output, invoke `.min()` after the closing `` template tag: 208 | 209 | ```js 210 | render(res, html` 211 | 212 | 213 | ... 214 | 215 | `.min() 216 | ).end(); 217 | ``` 218 | 219 | You can also use `html.minified = true` on top, and see similar results. 220 | 221 | 222 | 223 | ### API Summary Example 224 | 225 | ```js 226 | import {render, css, js, html, raw} from 'ucontent'; 227 | 228 | // turn on implicit html minification (production) 229 | html.minified = true; 230 | 231 | // optionally 232 | // svg.minified = true; 233 | 234 | render(content => response.end(content), html` 235 | 236 | 237 | 238 | 239 | ${meta.map(({name, content}) => 240 | html``)} 241 | 242 | 249 | 250 | 257 | 258 | 259 | ignored()}> 260 |
268 | Hello ${userName}! 269 | ${raw` valid, or even ${'broken'}, `} 270 |
271 | 272 | 273 | `); 274 | ``` 275 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const umap = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('umap')); 3 | const {CSS, HTML, JS, Raw, SVG} = require('./ucontent.js'); 4 | const {parse} = require('./utils.js'); 5 | 6 | const {isArray} = Array; 7 | 8 | const cache = umap(new WeakMap); 9 | 10 | const content = (template, values, svg, minified) => { 11 | const {length} = values; 12 | const updates = cache.get(template) || 13 | cache.set(template, parse(template, length, svg, minified)); 14 | return length ? values.map(update, updates).join('') : updates[0](); 15 | }; 16 | 17 | const join = (template, values) => ( 18 | template[0] + values.map(chunks, template).join('') 19 | ); 20 | 21 | const stringify = (template, values) => 22 | isArray(template) ? join(template, values) : template; 23 | 24 | const uhtmlParity = fn => { 25 | // both `.node` and `.for` are for feature parity with uhtml 26 | // but don't do anything different from regular function call 27 | fn.node = fn; 28 | fn.for = () => fn; 29 | fn.minified = false; 30 | return fn; 31 | }; 32 | 33 | /** 34 | * A tag to represent CSS content. 35 | * @param {string|string[]|CSS} template The template array passed as tag, 36 | * or an instance of CSS content, or just some CSS string. 37 | * @param {any[]} [values] Optional spread arguments passed when used as tag. 38 | * @returns {CSS} An instance of CSS content. 39 | */ 40 | const css = (template, ...values) => new CSS( 41 | stringify(template, values) 42 | ); 43 | exports.css = css; 44 | 45 | /** 46 | * A tag to represent JS content. 47 | * @param {string|string[]|JS} template The template array passed as tag, 48 | * or an instance of JS content, or just some JS string. 49 | * @param {any[]} [values] Optional spread arguments passed when used as tag. 50 | * @returns {JS} An instance of JS content. 51 | */ 52 | const js = (template, ...values) => new JS( 53 | stringify(template, values) 54 | ); 55 | exports.js = js; 56 | 57 | /** 58 | * A tag to represent Raw content. 59 | * @param {string|string[]|Raw} template The template array passed as tag, 60 | * or an instance of Raw content, or just some Raw string. 61 | * @param {any[]} [values] Optional spread arguments passed when used as tag. 62 | * @returns {Raw} An instance of Raw content. 63 | */ 64 | const raw = (template, ...values) => new Raw( 65 | stringify(template, values) 66 | ); 67 | exports.raw = raw; 68 | 69 | /** 70 | * A tag to represent HTML content. 71 | * @param {string[]} template The template array passed as tag. 72 | * The `html` tag can be used only as template literal tag. 73 | * @param {any[]} values The spread arguments passed when used as tag. 74 | * @returns {HTML} An instance of HTML content. 75 | */ 76 | const html = uhtmlParity((template, ...values) => new HTML( 77 | content(template, values, false, html.minified) 78 | )); 79 | exports.html = html; 80 | 81 | /** 82 | * A tag to represent SVG content. 83 | * @param {string[]} template The template array passed as tag. 84 | * The `svg` tag can be used only as template literal tag. 85 | * @param {any[]} values The spread arguments passed when used as tag. 86 | * @returns {SVG} An instance of SVG content. 87 | */ 88 | const svg = uhtmlParity((template, ...values) => new SVG( 89 | content(template, values, true, svg.minified) 90 | )); 91 | exports.svg = svg; 92 | 93 | /** 94 | * Render some content via a response.write(content) or via callback(content). 95 | * @param {object|function} where Where to render the content. 96 | * If it's an object, it assumes it has a `.write(content)` method. 97 | * If it's a callback, it will receive the content as string. 98 | * @param {CSS|HTML|JS|Raw|function} what What to render as content. 99 | * If it's an instance of CSS, HTML, JS, or Raw, it will be stringified. 100 | * If it's a callback, it will be invoked and its result will be rendered. 101 | * The returned value can be an instance of CSS, HTML, JS, Raw, or string. 102 | * @return {function|object} It returns the result of `callback(content)` 103 | * invoke, or the the passed first parameter as is (i.e. the `response`) 104 | */ 105 | const render = (where, what) => { 106 | const content = (typeof what === 'function' ? what() : what).toString(); 107 | return typeof where === 'function' ? 108 | where(content) : 109 | (where.write(content), where); 110 | }; 111 | exports.render = render; 112 | 113 | function chunks(value, i) { 114 | return value + this[i + 1]; 115 | } 116 | 117 | function update(value, i) { 118 | return this[i](value); 119 | } 120 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /cjs/ucontent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const csso = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('csso')); 3 | const html = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('html-minifier')); 4 | const Terser = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('terser')); 5 | const umap = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('umap')); 6 | 7 | const {assign} = Object; 8 | 9 | const cache = umap(new WeakMap); 10 | 11 | const commonOptions = { 12 | collapseWhitespace: true, 13 | preserveLineBreaks: true, 14 | preventAttributesEscaping: true, 15 | removeAttributeQuotes: true, 16 | removeComments: true 17 | }; 18 | 19 | const htmlOptions = assign({html5: true}, commonOptions); 20 | 21 | const jsOptions = {output: {comments: /^!/}}; 22 | 23 | const svgOptions = assign({keepClosingSlash: true}, commonOptions); 24 | 25 | 26 | /** 27 | * The base class for CSS, HTML, JS, and Raw. 28 | * @private 29 | */ 30 | class UContent extends String { 31 | /** 32 | * 33 | * @param {string} content The string representing some content. 34 | * @param {boolean} [minified] The optional flag to avoid duplicated `min()`. 35 | */ 36 | constructor(content, minified = false) { 37 | super(String(content)).minified = minified; 38 | } 39 | }; 40 | 41 | 42 | /** 43 | * The class that represents CSS content. 44 | */ 45 | class CSS extends UContent { 46 | /** 47 | * @returns {CSS} The CSS instance as minified. 48 | */ 49 | min() { 50 | return this.minified ? this : ( 51 | cache.get(this) || 52 | cache.set( 53 | this, 54 | new CSS(csso.minify(this.toString()).css, true) 55 | ) 56 | ); 57 | } 58 | } 59 | exports.CSS = CSS; 60 | 61 | 62 | /** 63 | * The class that represents HTML content. 64 | */ 65 | class HTML extends UContent { 66 | /** 67 | * @returns {HTML} The HTML instance as minified. 68 | */ 69 | min() { 70 | return this.minified ? this : ( 71 | cache.get(this) || 72 | cache.set( 73 | this, 74 | new HTML(html.minify(this.toString(), htmlOptions), true) 75 | ) 76 | ); 77 | } 78 | } 79 | exports.HTML = HTML; 80 | 81 | 82 | /** 83 | * The class that represents JS content. 84 | */ 85 | class JS extends UContent { 86 | /** 87 | * @returns {JS} The JS instance as minified. 88 | */ 89 | min() { 90 | return this.minified ? this : ( 91 | cache.get(this) || 92 | cache.set( 93 | this, 94 | new JS(Terser.minify(this.toString(), jsOptions).code, true) 95 | ) 96 | ); 97 | } 98 | } 99 | exports.JS = JS; 100 | 101 | 102 | /** 103 | * The class that represents Raw content. 104 | */ 105 | class Raw extends UContent { 106 | /** 107 | * @returns {Raw} The Raw content as is. 108 | */ 109 | min() { 110 | return this; 111 | } 112 | } 113 | exports.Raw = Raw; 114 | 115 | 116 | /** 117 | * The class that represents SVG content. 118 | */ 119 | class SVG extends UContent { 120 | /** 121 | * @returns {SVG} The SVG instance as minified. 122 | */ 123 | min() { 124 | return this.minified ? this : ( 125 | cache.get(this) || 126 | cache.set( 127 | this, 128 | new SVG(html.minify(this.toString(), svgOptions), true) 129 | ) 130 | ); 131 | } 132 | } 133 | exports.SVG = SVG; 134 | -------------------------------------------------------------------------------- /cjs/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {escape} = require('html-escaper'); 3 | const html = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('html-minifier')); 4 | const uhyphen = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('uhyphen')); 5 | const instrument = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('uparser')); 6 | const umap = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('umap')); 7 | 8 | const {CSS, HTML, JS, Raw, SVG} = require('./ucontent.js'); 9 | 10 | const {toString} = Function; 11 | const {assign, keys} = Object; 12 | 13 | const inlineStyle = umap(new WeakMap); 14 | 15 | const prefix = 'isµ' + Date.now(); 16 | const interpolation = new RegExp( 17 | `(|\\s*${prefix}(\\d+)=('|")([^\\4]+?)\\4)`, 'g' 18 | ); 19 | 20 | // const attrs = new RegExp(`(${prefix}\\d+)=([^'">\\s]+)`, 'g'); 21 | 22 | const commonOptions = { 23 | collapseWhitespace: true, 24 | conservativeCollapse: true, 25 | preserveLineBreaks: true, 26 | preventAttributesEscaping: true, 27 | removeAttributeQuotes: false, 28 | removeComments: true, 29 | ignoreCustomComments: [new RegExp(`${prefix}\\d+`)] 30 | }; 31 | 32 | const htmlOptions = assign({html5: true}, commonOptions); 33 | 34 | const svgOptions = assign({keepClosingSlash: true}, commonOptions); 35 | 36 | const attribute = (name, quote, value) => 37 | ` ${name}=${quote}${escape(value)}${quote}`; 38 | 39 | const getValue = value => { 40 | switch (typeof value) { 41 | case 'string': 42 | return escape(value); 43 | case 'boolean': 44 | case 'number': 45 | return String(value); 46 | case 'object': 47 | switch (true) { 48 | case value instanceof Array: 49 | return value.map(getValue).join(''); 50 | case value instanceof HTML: 51 | case value instanceof Raw: 52 | return value.toString(); 53 | case value instanceof CSS: 54 | case value instanceof JS: 55 | case value instanceof SVG: 56 | return value.min().toString(); 57 | } 58 | } 59 | return value == null ? '' : escape(String(value)); 60 | }; 61 | 62 | const minify = ($, svg) => html.minify($, svg ? svgOptions : htmlOptions); 63 | 64 | const parse = (template, expectedLength, svg, minified) => { 65 | const text = instrument(template, prefix, svg); 66 | const html = minified ? minify(text, svg) : text; 67 | const updates = []; 68 | let i = 0; 69 | let match = null; 70 | while (match = interpolation.exec(html)) { 71 | const pre = html.slice(i, match.index); 72 | i = match.index + match[0].length; 73 | if (match[2]) 74 | updates.push(value => (pre + getValue(value))); 75 | else { 76 | const name = match[5]; 77 | const quote = match[4]; 78 | switch (true) { 79 | case name === 'aria': 80 | updates.push(value => (pre + keys(value).map(aria, value).join(''))); 81 | break; 82 | case name === 'data': 83 | updates.push(value => (pre + keys(value).map(data, value).join(''))); 84 | break; 85 | case name === 'style': 86 | updates.push(value => { 87 | let result = pre; 88 | if (typeof value === 'string') 89 | result += attribute(name, quote, value); 90 | if (value instanceof CSS) { 91 | result += attribute( 92 | name, 93 | quote, 94 | inlineStyle.get(value) || 95 | inlineStyle.set( 96 | value, 97 | new CSS(`style{${value}}`).min().slice(6, -1) 98 | ) 99 | ); 100 | } 101 | return result; 102 | }); 103 | break; 104 | // setters as boolean attributes (.disabled .contentEditable) 105 | case name[0] === '.': 106 | const lower = name.slice(1).toLowerCase(); 107 | updates.push(lower === 'dataset' ? 108 | (value => (pre + keys(value).map(data, value).join(''))) : 109 | (value => { 110 | let result = pre; 111 | // null, undefined, and false are not shown at all 112 | if (value != null && value !== false) { 113 | // true means boolean attribute, just show the name 114 | if (value === true) 115 | result += ` ${lower}`; 116 | // in all other cases, just escape it in quotes 117 | else 118 | result += attribute(lower, quote, value); 119 | } 120 | return result; 121 | }) 122 | ); 123 | break; 124 | case name.slice(0, 2) === 'on': 125 | updates.push(value => { 126 | let result = pre; 127 | // allow handleEvent based objects that 128 | // follow the `onMethod` convention 129 | // allow listeners only if passed as string, 130 | // as functions with a special toString method, 131 | // as objects with handleEvents and a method, 132 | // or as instance of JS 133 | switch (typeof value) { 134 | case 'object': 135 | if (value instanceof JS) { 136 | result += attribute(name, quote, value.min()); 137 | break; 138 | } 139 | if (!(name in value)) 140 | break; 141 | value = value[name]; 142 | if (typeof value !== 'function') 143 | break; 144 | case 'function': 145 | if (value.toString === toString) 146 | break; 147 | case 'string': 148 | result += attribute(name, quote, value); 149 | break; 150 | } 151 | return result; 152 | }); 153 | break; 154 | default: 155 | updates.push(value => { 156 | let result = pre; 157 | if (value != null) 158 | result += attribute(name, quote, value); 159 | return result; 160 | }); 161 | break; 162 | } 163 | } 164 | } 165 | const {length} = updates; 166 | if (length !== expectedLength) 167 | throw new Error(`invalid template ${template}`); 168 | if (length) { 169 | const last = updates[length - 1]; 170 | const chunk = html.slice(i); 171 | updates[length - 1] = value => (last(value) + chunk); 172 | } 173 | else 174 | updates.push(() => html); 175 | return updates; 176 | }; 177 | exports.parse = parse; 178 | 179 | // declarations 180 | function aria(key) { 181 | const value = escape(this[key]); 182 | return key === 'role' ? 183 | ` role="${value}"` : 184 | ` aria-${key.toLowerCase()}="${value}"`; 185 | } 186 | 187 | function data(key) { 188 | return ` data-${uhyphen(key)}="${escape(this[key])}"`; 189 | } 190 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import umap from 'umap'; 2 | import {CSS, HTML, JS, Raw, SVG} from './ucontent.js'; 3 | import {parse} from './utils.js'; 4 | 5 | const {isArray} = Array; 6 | 7 | const cache = umap(new WeakMap); 8 | 9 | const content = (template, values, svg, minified) => { 10 | const {length} = values; 11 | const updates = cache.get(template) || 12 | cache.set(template, parse(template, length, svg, minified)); 13 | return length ? values.map(update, updates).join('') : updates[0](); 14 | }; 15 | 16 | const join = (template, values) => ( 17 | template[0] + values.map(chunks, template).join('') 18 | ); 19 | 20 | const stringify = (template, values) => 21 | isArray(template) ? join(template, values) : template; 22 | 23 | const uhtmlParity = fn => { 24 | // both `.node` and `.for` are for feature parity with uhtml 25 | // but don't do anything different from regular function call 26 | fn.node = fn; 27 | fn.for = () => fn; 28 | fn.minified = false; 29 | return fn; 30 | }; 31 | 32 | /** 33 | * A tag to represent CSS content. 34 | * @param {string|string[]|CSS} template The template array passed as tag, 35 | * or an instance of CSS content, or just some CSS string. 36 | * @param {any[]} [values] Optional spread arguments passed when used as tag. 37 | * @returns {CSS} An instance of CSS content. 38 | */ 39 | export const css = (template, ...values) => new CSS( 40 | stringify(template, values) 41 | ); 42 | 43 | /** 44 | * A tag to represent JS content. 45 | * @param {string|string[]|JS} template The template array passed as tag, 46 | * or an instance of JS content, or just some JS string. 47 | * @param {any[]} [values] Optional spread arguments passed when used as tag. 48 | * @returns {JS} An instance of JS content. 49 | */ 50 | export const js = (template, ...values) => new JS( 51 | stringify(template, values) 52 | ); 53 | 54 | /** 55 | * A tag to represent Raw content. 56 | * @param {string|string[]|Raw} template The template array passed as tag, 57 | * or an instance of Raw content, or just some Raw string. 58 | * @param {any[]} [values] Optional spread arguments passed when used as tag. 59 | * @returns {Raw} An instance of Raw content. 60 | */ 61 | export const raw = (template, ...values) => new Raw( 62 | stringify(template, values) 63 | ); 64 | 65 | /** 66 | * A tag to represent HTML content. 67 | * @param {string[]} template The template array passed as tag. 68 | * The `html` tag can be used only as template literal tag. 69 | * @param {any[]} values The spread arguments passed when used as tag. 70 | * @returns {HTML} An instance of HTML content. 71 | */ 72 | export const html = uhtmlParity((template, ...values) => new HTML( 73 | content(template, values, false, html.minified) 74 | )); 75 | 76 | /** 77 | * A tag to represent SVG content. 78 | * @param {string[]} template The template array passed as tag. 79 | * The `svg` tag can be used only as template literal tag. 80 | * @param {any[]} values The spread arguments passed when used as tag. 81 | * @returns {SVG} An instance of SVG content. 82 | */ 83 | export const svg = uhtmlParity((template, ...values) => new SVG( 84 | content(template, values, true, svg.minified) 85 | )); 86 | 87 | /** 88 | * Render some content via a response.write(content) or via callback(content). 89 | * @param {object|function} where Where to render the content. 90 | * If it's an object, it assumes it has a `.write(content)` method. 91 | * If it's a callback, it will receive the content as string. 92 | * @param {CSS|HTML|JS|Raw|function} what What to render as content. 93 | * If it's an instance of CSS, HTML, JS, or Raw, it will be stringified. 94 | * If it's a callback, it will be invoked and its result will be rendered. 95 | * The returned value can be an instance of CSS, HTML, JS, Raw, or string. 96 | * @return {function|object} It returns the result of `callback(content)` 97 | * invoke, or the the passed first parameter as is (i.e. the `response`) 98 | */ 99 | export const render = (where, what) => { 100 | const content = (typeof what === 'function' ? what() : what).toString(); 101 | return typeof where === 'function' ? 102 | where(content) : 103 | (where.write(content), where); 104 | }; 105 | 106 | function chunks(value, i) { 107 | return value + this[i + 1]; 108 | } 109 | 110 | function update(value, i) { 111 | return this[i](value); 112 | } 113 | -------------------------------------------------------------------------------- /esm/ucontent.js: -------------------------------------------------------------------------------- 1 | import csso from 'csso'; 2 | import html from 'html-minifier'; 3 | import Terser from 'terser'; 4 | import umap from 'umap'; 5 | 6 | const {assign} = Object; 7 | 8 | const cache = umap(new WeakMap); 9 | 10 | const commonOptions = { 11 | collapseWhitespace: true, 12 | preserveLineBreaks: true, 13 | preventAttributesEscaping: true, 14 | removeAttributeQuotes: true, 15 | removeComments: true 16 | }; 17 | 18 | const htmlOptions = assign({html5: true}, commonOptions); 19 | 20 | const jsOptions = {output: {comments: /^!/}}; 21 | 22 | const svgOptions = assign({keepClosingSlash: true}, commonOptions); 23 | 24 | 25 | /** 26 | * The base class for CSS, HTML, JS, and Raw. 27 | * @private 28 | */ 29 | class UContent extends String { 30 | /** 31 | * 32 | * @param {string} content The string representing some content. 33 | * @param {boolean} [minified] The optional flag to avoid duplicated `min()`. 34 | */ 35 | constructor(content, minified = false) { 36 | super(String(content)).minified = minified; 37 | } 38 | }; 39 | 40 | 41 | /** 42 | * The class that represents CSS content. 43 | */ 44 | export class CSS extends UContent { 45 | /** 46 | * @returns {CSS} The CSS instance as minified. 47 | */ 48 | min() { 49 | return this.minified ? this : ( 50 | cache.get(this) || 51 | cache.set( 52 | this, 53 | new CSS(csso.minify(this.toString()).css, true) 54 | ) 55 | ); 56 | } 57 | }; 58 | 59 | 60 | /** 61 | * The class that represents HTML content. 62 | */ 63 | export class HTML extends UContent { 64 | /** 65 | * @returns {HTML} The HTML instance as minified. 66 | */ 67 | min() { 68 | return this.minified ? this : ( 69 | cache.get(this) || 70 | cache.set( 71 | this, 72 | new HTML(html.minify(this.toString(), htmlOptions), true) 73 | ) 74 | ); 75 | } 76 | }; 77 | 78 | 79 | /** 80 | * The class that represents JS content. 81 | */ 82 | export class JS extends UContent { 83 | /** 84 | * @returns {JS} The JS instance as minified. 85 | */ 86 | min() { 87 | return this.minified ? this : ( 88 | cache.get(this) || 89 | cache.set( 90 | this, 91 | new JS(Terser.minify(this.toString(), jsOptions).code, true) 92 | ) 93 | ); 94 | } 95 | }; 96 | 97 | 98 | /** 99 | * The class that represents Raw content. 100 | */ 101 | export class Raw extends UContent { 102 | /** 103 | * @returns {Raw} The Raw content as is. 104 | */ 105 | min() { 106 | return this; 107 | } 108 | }; 109 | 110 | 111 | /** 112 | * The class that represents SVG content. 113 | */ 114 | export class SVG extends UContent { 115 | /** 116 | * @returns {SVG} The SVG instance as minified. 117 | */ 118 | min() { 119 | return this.minified ? this : ( 120 | cache.get(this) || 121 | cache.set( 122 | this, 123 | new SVG(html.minify(this.toString(), svgOptions), true) 124 | ) 125 | ); 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /esm/utils.js: -------------------------------------------------------------------------------- 1 | import {escape} from 'html-escaper'; 2 | import html from 'html-minifier'; 3 | import uhyphen from 'uhyphen'; 4 | import instrument from 'uparser'; 5 | import umap from 'umap'; 6 | 7 | import {CSS, HTML, JS, Raw, SVG} from './ucontent.js'; 8 | 9 | const {toString} = Function; 10 | const {assign, keys} = Object; 11 | 12 | const inlineStyle = umap(new WeakMap); 13 | 14 | const prefix = 'isµ' + Date.now(); 15 | const interpolation = new RegExp( 16 | `(|\\s*${prefix}(\\d+)=('|")([^\\4]+?)\\4)`, 'g' 17 | ); 18 | 19 | // const attrs = new RegExp(`(${prefix}\\d+)=([^'">\\s]+)`, 'g'); 20 | 21 | const commonOptions = { 22 | collapseWhitespace: true, 23 | conservativeCollapse: true, 24 | preserveLineBreaks: true, 25 | preventAttributesEscaping: true, 26 | removeAttributeQuotes: false, 27 | removeComments: true, 28 | ignoreCustomComments: [new RegExp(`${prefix}\\d+`)] 29 | }; 30 | 31 | const htmlOptions = assign({html5: true}, commonOptions); 32 | 33 | const svgOptions = assign({keepClosingSlash: true}, commonOptions); 34 | 35 | const attribute = (name, quote, value) => 36 | ` ${name}=${quote}${escape(value)}${quote}`; 37 | 38 | const getValue = value => { 39 | switch (typeof value) { 40 | case 'string': 41 | return escape(value); 42 | case 'boolean': 43 | case 'number': 44 | return String(value); 45 | case 'object': 46 | switch (true) { 47 | case value instanceof Array: 48 | return value.map(getValue).join(''); 49 | case value instanceof HTML: 50 | case value instanceof Raw: 51 | return value.toString(); 52 | case value instanceof CSS: 53 | case value instanceof JS: 54 | case value instanceof SVG: 55 | return value.min().toString(); 56 | } 57 | } 58 | return value == null ? '' : escape(String(value)); 59 | }; 60 | 61 | const minify = ($, svg) => html.minify($, svg ? svgOptions : htmlOptions); 62 | 63 | export const parse = (template, expectedLength, svg, minified) => { 64 | const text = instrument(template, prefix, svg); 65 | const html = minified ? minify(text, svg) : text; 66 | const updates = []; 67 | let i = 0; 68 | let match = null; 69 | while (match = interpolation.exec(html)) { 70 | const pre = html.slice(i, match.index); 71 | i = match.index + match[0].length; 72 | if (match[2]) 73 | updates.push(value => (pre + getValue(value))); 74 | else { 75 | const name = match[5]; 76 | const quote = match[4]; 77 | switch (true) { 78 | case name === 'aria': 79 | updates.push(value => (pre + keys(value).map(aria, value).join(''))); 80 | break; 81 | case name === 'data': 82 | updates.push(value => (pre + keys(value).map(data, value).join(''))); 83 | break; 84 | case name === 'style': 85 | updates.push(value => { 86 | let result = pre; 87 | if (typeof value === 'string') 88 | result += attribute(name, quote, value); 89 | if (value instanceof CSS) { 90 | result += attribute( 91 | name, 92 | quote, 93 | inlineStyle.get(value) || 94 | inlineStyle.set( 95 | value, 96 | new CSS(`style{${value}}`).min().slice(6, -1) 97 | ) 98 | ); 99 | } 100 | return result; 101 | }); 102 | break; 103 | // setters as boolean attributes (.disabled .contentEditable) 104 | case name[0] === '.': 105 | const lower = name.slice(1).toLowerCase(); 106 | updates.push(lower === 'dataset' ? 107 | (value => (pre + keys(value).map(data, value).join(''))) : 108 | (value => { 109 | let result = pre; 110 | // null, undefined, and false are not shown at all 111 | if (value != null && value !== false) { 112 | // true means boolean attribute, just show the name 113 | if (value === true) 114 | result += ` ${lower}`; 115 | // in all other cases, just escape it in quotes 116 | else 117 | result += attribute(lower, quote, value); 118 | } 119 | return result; 120 | }) 121 | ); 122 | break; 123 | case name.slice(0, 2) === 'on': 124 | updates.push(value => { 125 | let result = pre; 126 | // allow handleEvent based objects that 127 | // follow the `onMethod` convention 128 | // allow listeners only if passed as string, 129 | // as functions with a special toString method, 130 | // as objects with handleEvents and a method, 131 | // or as instance of JS 132 | switch (typeof value) { 133 | case 'object': 134 | if (value instanceof JS) { 135 | result += attribute(name, quote, value.min()); 136 | break; 137 | } 138 | if (!(name in value)) 139 | break; 140 | value = value[name]; 141 | if (typeof value !== 'function') 142 | break; 143 | case 'function': 144 | if (value.toString === toString) 145 | break; 146 | case 'string': 147 | result += attribute(name, quote, value); 148 | break; 149 | } 150 | return result; 151 | }); 152 | break; 153 | default: 154 | updates.push(value => { 155 | let result = pre; 156 | if (value != null) 157 | result += attribute(name, quote, value); 158 | return result; 159 | }); 160 | break; 161 | } 162 | } 163 | } 164 | const {length} = updates; 165 | if (length !== expectedLength) 166 | throw new Error(`invalid template ${template}`); 167 | if (length) { 168 | const last = updates[length - 1]; 169 | const chunk = html.slice(i); 170 | updates[length - 1] = value => (last(value) + chunk); 171 | } 172 | else 173 | updates.push(() => html); 174 | return updates; 175 | }; 176 | 177 | // declarations 178 | function aria(key) { 179 | const value = escape(this[key]); 180 | return key === 'role' ? 181 | ` role="${value}"` : 182 | ` aria-${key.toLowerCase()}="${value}"`; 183 | } 184 | 185 | function data(key) { 186 | return ` data-${uhyphen(key)}="${escape(this[key])}"`; 187 | } 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ucontent", 3 | "version": "2.0.0", 4 | "description": "An SSR oriented HTML content generator", 5 | "main": "cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run test && npm run bench", 8 | "bench": "node test/benchmark.js", 9 | "cjs": "ascjs esm cjs", 10 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 11 | "test": "nyc node test/index.js" 12 | }, 13 | "keywords": [ 14 | "html", 15 | "ssr", 16 | "content" 17 | ], 18 | "type": "module", 19 | "exports": { 20 | "import": "./esm/index.js", 21 | "default": "./cjs/index.js" 22 | }, 23 | "author": "Andrea Giammarchi", 24 | "license": "ISC", 25 | "devDependencies": { 26 | "ascjs": "^4.0.1", 27 | "coveralls": "^3.1.0", 28 | "nyc": "^15.1.0", 29 | "pelo": "^0.1.0", 30 | "stringified-handler": "^0.4.3" 31 | }, 32 | "module": "esm/index.js", 33 | "dependencies": { 34 | "csso": "^4.0.3", 35 | "html-escaper": "^3.0.0", 36 | "html-minifier": "^4.0.0", 37 | "terser": "^4.7.0", 38 | "uhyphen": "^0.1.0", 39 | "umap": "^1.0.2", 40 | "uparser": "^0.2.1" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/WebReflection/ucontent.git" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/WebReflection/ucontent/issues" 48 | }, 49 | "homepage": "https://github.com/WebReflection/ucontent#readme" 50 | } 51 | -------------------------------------------------------------------------------- /test/base.js: -------------------------------------------------------------------------------- 1 | const {render, html} = require('../cjs'); 2 | 3 | html.minified = true; 4 | 5 | require('http').createServer((req, res) => { 6 | res.writeHead(200, {'content-type': 'text/html;charset=utf-8'}); 7 | render(content => res.end(content), html` 8 | 9 | 10 | 11 | 12 | 13 | ucontent 14 | 15 | ${html` 16 |

Hello There

17 |

18 | Thank you for visiting uhtml at ${new Date()} 19 |

20 | `} 21 | 22 | `); 23 | }).listen(8080); 24 | 25 | console.log('http://localhost:8080/'); 26 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | const {css, js, html, raw} = require('../cjs'); 2 | 3 | const generateHTML = () => html` 4 | 5 | 10 | 15 |
'whatever'} 19 | onmouseover="${'callback(event)'}" 20 | data=${{test: 1, otherTest: 2}} 21 | > 22 | ${[ 23 | html`

Some ${raw`"content"`}


`, 24 | html`
`, 25 | raw`` 26 | ]} 27 |
28 | `; 29 | 30 | const generateContent = () => html` 31 | 32 | 38 | 44 |
'whatever'} 48 | onmouseover="${'callback(event)'}" 49 | data=${{test: 1, otherTest: 2}} 50 | > 51 | ${[ 52 | html`

Some ${raw`"content"`}


`, 53 | html`
`, 54 | raw`` 55 | ]} 56 |
57 | `; 58 | 59 | console.time('without CSS/JS optimizations - cold'); 60 | const htmlOnly = generateHTML(); 61 | console.timeEnd('without CSS/JS optimizations - cold'); 62 | 63 | console.time('without CSS/JS optimizations - hot'); 64 | const htmlHot = generateHTML(); 65 | console.timeEnd('without CSS/JS optimizations - hot'); 66 | 67 | console.time('with CSS/JS optimizations - cold'); 68 | const cold = generateContent(); 69 | console.timeEnd('with CSS/JS optimizations - cold'); 70 | 71 | console.time('with CSS/JS optimizations - hot'); 72 | const hot = generateContent(); 73 | console.timeEnd('with CSS/JS optimizations - hot'); 74 | 75 | console.time('CSS/JS opt + minified - cold'); 76 | const min = cold.min(); 77 | console.timeEnd('CSS/JS opt + minified - cold'); 78 | 79 | console.time('CSS/JS opt + minified - hot'); 80 | const minHot = cold.min(); 81 | console.timeEnd('CSS/JS opt + minified - hot'); 82 | 83 | // console.log(cold.toString()); 84 | // console.log(min); 85 | 86 | require('./pelo'); 87 | -------------------------------------------------------------------------------- /test/counter-fe.js: -------------------------------------------------------------------------------- 1 | const StringifiedHandler = require('stringified-handler'); 2 | 3 | const {css, html, js} = require('../cjs'); 4 | 5 | const handler = StringifiedHandler({ 6 | increment({currentTarget: {previousElementSibling}}) { 7 | previousElementSibling.textContent++; 8 | }, 9 | decrement({currentTarget: {nextElementSibling}}) { 10 | nextElementSibling.textContent--; 11 | } 12 | }); 13 | 14 | const style = css` 15 | div.counter { 16 | font-size: 200%; 17 | } 18 | div.counter span { 19 | width: 4rem; 20 | display: inline-block; 21 | text-align: center; 22 | } 23 | div.counter button { 24 | width: 64px; 25 | height: 64px; 26 | border: none; 27 | border-radius: 10px; 28 | background-color: seagreen; 29 | color: white; 30 | } 31 | `; 32 | 33 | const view = html` 34 |
35 | 36 | 0 37 | 38 |
39 | `; 40 | 41 | module.exports = { 42 | script: js(handler), 43 | style, 44 | view 45 | }; 46 | -------------------------------------------------------------------------------- /test/counter.js: -------------------------------------------------------------------------------- 1 | const {createServer} = require('http'); 2 | 3 | const {render, html} = require('../cjs'); 4 | 5 | const counter = require('./counter-fe.js'); 6 | 7 | const header = {'content-type': 'text/html;charset=utf-8'}; 8 | 9 | const page = html` 10 | 11 | 12 | 13 | SSR Component 14 | 15 | 16 | 17 | 18 | 19 | 20 | ${counter.view} 21 | 22 | 23 | `.min(); 24 | 25 | createServer( 26 | (_, response) => { 27 | response.writeHead(200, header); 28 | render(response, page).end(); 29 | } 30 | ) 31 | .listen( 32 | 8080, 33 | () => console.log('http://localhost:8080/') 34 | ); 35 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const {render, css, js, html, raw, svg} = require('../cjs'); 2 | 3 | const assert = (ucontent, output) => { 4 | console.assert(ucontent == output, ucontent.toString()); 5 | }; 6 | 7 | assert(html`
`, '
'); 8 | assert(html`
`, '
'); 9 | assert(html`
`, '
'); 10 | assert(svg`
`, '
'); 11 | assert(html`
`, '
'); 12 | assert(html`
`, '
'); 13 | assert(html`
`, '
'); 14 | assert(html`
`, '
'); 15 | assert(html`
`, '
'); 16 | const rect = svg.for({})``; 17 | assert(html.node`
${rect}
`, '
'); 18 | assert(html`${rect}`, ''); 19 | assert(html`${rect.min().min()}`, ''); 20 | assert(html`
${Buffer.from('"')}
`, '
"
'); 21 | assert(html`
${new String('"')}
`, '
"
'); 22 | assert(html`
`, '
'); 23 | assert(html`
`, '
'); 24 | assert(html``, ''); 25 | assert(html`
`, '
'); 26 | assert(html`
${[1,2].map(n => html`

${n}

`)}
`, '

1

2

'); 27 | assert(html`
${[1,2].map(n => `

${n}

`)}
`, '
<p>1</p><p>2</p>
'); 28 | assert(html`
${{}}
`, '
[object Object]
'); 29 | assert(html`
${null}
`, '
'); 30 | assert(html`
${void 0}
`, '
'); 31 | assert(html`
${true}
`, '
true
'); 32 | assert(html.for({})`
${123}
`, '
123
'); 33 | 34 | assert(html``, ''); 35 | assert(html``, ''); 36 | assert(html``, ''); 37 | assert(html`
${raw``.min().min()}
`, '
'); 38 | assert(html`
${raw``}
`, '
'); 39 | assert(html`
${raw('')}
`, '
'); 40 | assert(html`
`.min().min(), '
'); 41 | assert(html`
`.min(), '
'); 42 | assert(html`
`.min(), '
'); 43 | const inlineStyle = css`font-family: sans-serif`; 44 | assert(html`
`, '
'); 45 | assert(html`
`, '
'); 46 | assert(html`
`, '
'); 47 | 48 | const fn = () => {}; 49 | fn.toString = () => 'console.log("test")'; 50 | assert(html`
`, '
'); 51 | 52 | try { 53 | html(['', '']); 54 | console.assert(false, 'not throwing with bad template'); 55 | } 56 | catch (e) {} 57 | 58 | let outerContent = ''; 59 | const callback = content => (outerContent = content); 60 | const server = {write(content) { this.content = content; }}; 61 | 62 | assert(render(callback, html`

`), outerContent); 63 | assert(outerContent, '

'); 64 | 65 | render(server, html`

`); 66 | assert(server.content, '

'); 67 | 68 | assert(render(server, () => html`
`), server); 69 | assert(server.content, '
'); 70 | 71 | html.minified = true; 72 | svg.minified = true; 73 | 74 | assert(html`
`, '
'); 75 | assert(html`
`, '
'); 76 | assert(html`
`, '
'); 77 | assert(svg`
`, '
'); 78 | assert(html`
`, '
'); 79 | assert(html`
`, '
'); 80 | assert(html`
`, '
'); 81 | assert(html`
`, '
'); 82 | assert(html`
`, '
'); 83 | const mrect = svg.for({})``; 84 | assert(html.node`
${mrect}
`, '
'); 85 | assert(html`${mrect}`, ''); 86 | assert(html`
${Buffer.from('"')}
`, '
"
'); 87 | assert(html`
${new String('"')}
`, '
"
'); 88 | assert(html`
`, '
'); 89 | assert(html`
`, '
'); 90 | assert(html`
${[1,2].map(n => html`

${n}

`)}
`, '

1

2

'); 91 | assert(html`
${[1,2].map(n => `

${n}

`)}
`, '
<p>1</p><p>2</p>
'); 92 | assert(html`
${{}}
`, '
[object Object]
'); 93 | assert(html`
${null}
`, '
'); 94 | assert(html`
${void 0}
`, '
'); 95 | assert(html`
${true}
`, '
true
'); 96 | assert(html.for({})`
${123}
`, '
123
'); 97 | 98 | const handler1 = {}; 99 | assert(html`
`, '
'); 100 | 101 | const onclick = () => {}; 102 | onclick.toString = () => 'test'; 103 | const handler2 = {onclick}; 104 | assert(html`
`, '
'); 105 | 106 | const handler3 = {onclick: true}; 107 | assert(html`
`, '
'); 108 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/pelo-app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (html, button) { 4 | 5 | const greeting = 'Hello'; 6 | const name = 'special characters, <, >, &'; 7 | const drinks = [ 8 | { name: 'Cafe Latte', price: 3.0, sold: false }, 9 | { name: 'Cappucino', price: 2.9, sold: true }, 10 | { name: 'Club Mate', price: 2.2, sold: true }, 11 | { name: 'Berliner Weiße', price: 3.5, sold: false } 12 | ]; 13 | 14 | function drinkView(drink) { 15 | return html` 16 |
  • 17 | ${drink.name} is € ${drink.price} 18 | ${button(html)} 19 |
  • 20 | `; 21 | } 22 | 23 | function mainView(greeting, name, drinks) { 24 | return html` 25 |
    26 |

    ${greeting}, ${name}!

    27 | ${drinks.length > 0 ? html` 28 |
      29 | ${drinks.map(drink => drinkView(drink))} 30 |
    31 | ` : html` 32 |

    All drinks are gone!

    33 | `} 34 |

    35 | attributes: 36 |

    37 |
    38 | `; 39 | } 40 | 41 | return function render() { 42 | return mainView(greeting, name, drinks) 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /test/pelo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pelo = require('pelo'); 4 | const ucontent = require('../cjs'); 5 | 6 | const createApp = require('./pelo-app'); 7 | 8 | const warmup = 100; 9 | const iteration = 10000; 10 | 11 | console.log(`# benchmark ${iteration} iterations`); 12 | 13 | const ucontentApp = createApp( 14 | ucontent.html, 15 | html => html`` 16 | ); 17 | for (let i = 0; i < warmup; i++) { 18 | ucontentApp().toString(); 19 | } 20 | console.time('ucontent'); 21 | for (let i = 0; i < iteration; i++) { 22 | ucontentApp().toString(); 23 | } 24 | console.timeEnd('ucontent'); 25 | 26 | const peloApp = createApp( 27 | pelo, 28 | html => html`` 29 | ); 30 | for (let i = 0; i < warmup; i++) { 31 | peloApp().toString(); 32 | } 33 | console.time('pelo'); 34 | for (let i = 0; i < iteration; i++) { 35 | peloApp().toString(); 36 | } 37 | console.timeEnd('pelo'); 38 | -------------------------------------------------------------------------------- /test/view.js: -------------------------------------------------------------------------------- 1 | 2 | exports.use = ({html}) => (props = {}) => html` 3 | props: ${JSON.stringify(props)} 4 | `; 5 | -------------------------------------------------------------------------------- /ucontent-head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/ucontent/cb168a3340e6536ad8a073e4c667101104d511aa/ucontent-head.jpg --------------------------------------------------------------------------------