`, '
');
83 | const mrect = svg.for({})`
`;
84 | assert(html.node`
${mrect}
`, '
');
85 | assert(html`
`, '
');
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}
`)}
`, '
');
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
µcontent
2 |
3 | [](https://travis-ci.com/WebReflection/ucontent) [](https://coveralls.io/github/WebReflection/ucontent?branch=master)
4 |
5 | 
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 `