ghtml 1.5.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -15
- package/bench/index.js +50 -10
- package/package.json +1 -1
- package/src/html.js +51 -35
- package/test/index.js +44 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Inspired by [html-template-tag](https://github.com/AntonioVdlC/html-template-tag
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
```
|
|
7
|
+
```sh
|
|
8
8
|
npm i ghtml
|
|
9
9
|
```
|
|
10
10
|
|
|
@@ -18,11 +18,15 @@ The `html` function is designed to tag template literals and automatically escap
|
|
|
18
18
|
|
|
19
19
|
The `htmlGenerator` function acts as the generator version of the `html` function. It facilitates the creation of HTML fragments iteratively, making it ideal for parsing large templates or constructing HTML content dynamically.
|
|
20
20
|
|
|
21
|
+
**Note:**
|
|
22
|
+
|
|
23
|
+
Keep in mind that, in Node.js, all else being equal, streaming a response using synchronous generators will **always** be slower than processing everything directly and sending it at once — [this also applies to TTFB](https://github.com/mcollina/fastify-html/issues/11#issuecomment-2069385895). However, if a template includes promises that do asynchronous operations (I/O, etc.), then `htmlAsyncGenerator` can be used to stream the response as those promises get resolved, which will indeed improve TTFB.
|
|
24
|
+
|
|
21
25
|
### `htmlAsyncGenerator`
|
|
22
26
|
|
|
23
|
-
This version of HTML generator should be preferred for asynchronous use cases. The output will be generated as the promise expressions resolve.
|
|
27
|
+
This version of HTML generator should be preferred for asynchronous and streaming use cases. The output will be generated as the promise expressions resolve or stream expressions send data.
|
|
24
28
|
|
|
25
|
-
**Note:**
|
|
29
|
+
**Minor Note:**
|
|
26
30
|
|
|
27
31
|
Because they return generators instead of strings, a key difference of `htmlGenerator` and `htmlAsyncGenerator` is their ability to recognize and properly handle iterable elements within array expressions. This is to detect nested `htmlGenerator` and `htmlAsyncGenerator` usage, enabling scenarios such as ``${[1, 2, 3].map(i => htmlGenerator`<li>${i}</li>`)}``.
|
|
28
32
|
|
|
@@ -79,6 +83,23 @@ const htmlString = html`
|
|
|
79
83
|
`;
|
|
80
84
|
```
|
|
81
85
|
|
|
86
|
+
```js
|
|
87
|
+
import { html } from "ghtml";
|
|
88
|
+
import http from "node:http";
|
|
89
|
+
|
|
90
|
+
http
|
|
91
|
+
.createServer((req, res) => {
|
|
92
|
+
const htmlContent = html`<!doctype html>
|
|
93
|
+
<html>
|
|
94
|
+
<p>You are at: ${req.url}</p>
|
|
95
|
+
</html>`;
|
|
96
|
+
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
97
|
+
res.write(htmlContent);
|
|
98
|
+
res.end();
|
|
99
|
+
})
|
|
100
|
+
.listen(3000);
|
|
101
|
+
```
|
|
102
|
+
|
|
82
103
|
### `htmlGenerator`
|
|
83
104
|
|
|
84
105
|
```js
|
|
@@ -92,9 +113,10 @@ const generator = function* () {
|
|
|
92
113
|
|
|
93
114
|
http
|
|
94
115
|
.createServer((req, res) => {
|
|
95
|
-
const htmlContent = html
|
|
96
|
-
<
|
|
97
|
-
|
|
116
|
+
const htmlContent = html`<!doctype html>
|
|
117
|
+
<html>
|
|
118
|
+
<p>${generator()}</p>
|
|
119
|
+
</html>`;
|
|
98
120
|
const readableStream = Readable.from(htmlContent);
|
|
99
121
|
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
100
122
|
readableStream.pipe(res);
|
|
@@ -112,21 +134,23 @@ import { Readable } from "node:stream";
|
|
|
112
134
|
import http from "node:http";
|
|
113
135
|
|
|
114
136
|
const asyncGenerator = async function* () {
|
|
115
|
-
const
|
|
137
|
+
const helloWorld = new Promise((resolve) => {
|
|
116
138
|
setTimeout(() => {
|
|
117
|
-
resolve("Hello");
|
|
118
|
-
},
|
|
139
|
+
resolve("<br /><br />Hello, World!");
|
|
140
|
+
}, 2500);
|
|
119
141
|
});
|
|
120
|
-
yield
|
|
142
|
+
yield await readFile("./.gitignore", "utf8");
|
|
143
|
+
yield helloWorld;
|
|
121
144
|
};
|
|
122
145
|
|
|
123
146
|
http
|
|
124
147
|
.createServer((req, res) => {
|
|
125
|
-
const htmlContent = html
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
const htmlContent = html`<!doctype html>
|
|
149
|
+
<html>
|
|
150
|
+
<p>!${asyncGenerator()}</p>
|
|
151
|
+
<code>${readFile("./README.md", "utf8")}</code>
|
|
152
|
+
<code>${createReadStream("./README.md", "utf8")}</code>
|
|
153
|
+
</html>`;
|
|
130
154
|
const readableStream = Readable.from(htmlContent);
|
|
131
155
|
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
132
156
|
readableStream.pipe(res);
|
package/bench/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Buffer } from "node:buffer";
|
|
|
8
8
|
let result = "";
|
|
9
9
|
const bench = new Bench({ time: 500 });
|
|
10
10
|
|
|
11
|
+
// Simple cases
|
|
11
12
|
bench.add("simple HTML formatting", () => {
|
|
12
13
|
result = html`<div>Hello, world!</div>`;
|
|
13
14
|
});
|
|
@@ -16,11 +17,17 @@ bench.add("null and undefined expressions", () => {
|
|
|
16
17
|
result = html`<p>${null} and ${undefined}</p>`;
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
// String expressions
|
|
19
21
|
const username = "User";
|
|
20
|
-
bench.add("string
|
|
22
|
+
bench.add("single string expression", () => {
|
|
23
|
+
result = html`<p>${username}</p>`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
bench.add("multiple string expressions", () => {
|
|
21
27
|
result = html`<p>${username} and ${username}</p>`;
|
|
22
28
|
});
|
|
23
29
|
|
|
30
|
+
// Array expressions
|
|
24
31
|
const items1 = ["Item 1", undefined, "Item 2", null, 2000, 1500.5];
|
|
25
32
|
bench.add("array expressions", () => {
|
|
26
33
|
result = html`<ul>
|
|
@@ -30,8 +37,25 @@ bench.add("array expressions", () => {
|
|
|
30
37
|
</ul>`;
|
|
31
38
|
});
|
|
32
39
|
|
|
40
|
+
const items2 = ["Item 1", "Item <1.5>", "Item 2", "Item <2.5>", "Item 3"];
|
|
41
|
+
bench.add("array expressions with escapable chars", () => {
|
|
42
|
+
result = html`<ul>
|
|
43
|
+
${items2.map((item) => {
|
|
44
|
+
return html`<li>"${item}" & '${item}'</li>`;
|
|
45
|
+
})}
|
|
46
|
+
</ul>`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Object expressions
|
|
33
50
|
const user = { id: 1, name: "John Doe" };
|
|
34
|
-
|
|
51
|
+
bench.add("object expressions", () => {
|
|
52
|
+
result = html`
|
|
53
|
+
<div>User: <span>${user.name}</span></div>
|
|
54
|
+
<div>Id: <span>${user.id}</span></div>
|
|
55
|
+
`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Mixed expressions
|
|
35
59
|
bench.add("multiple types of expressions", () => {
|
|
36
60
|
result = html`
|
|
37
61
|
${undefined}
|
|
@@ -46,19 +70,13 @@ bench.add("multiple types of expressions", () => {
|
|
|
46
70
|
`;
|
|
47
71
|
});
|
|
48
72
|
|
|
73
|
+
// Large strings
|
|
49
74
|
const largeString = Array.from({ length: 1000 }).join("Lorem ipsum ");
|
|
50
75
|
bench.add("large strings", () => {
|
|
51
76
|
result = html`<p>${largeString}${largeString}</p>`;
|
|
52
77
|
});
|
|
53
78
|
|
|
54
|
-
|
|
55
|
-
"<script>console.log('This should not execute');</script>";
|
|
56
|
-
bench.add("high iteration count", () => {
|
|
57
|
-
for (let i = 0; i !== 100; i++) {
|
|
58
|
-
result = html`<span>${i}: ${scriptContent}</span>`;
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
79
|
+
// Escaped and unescaped expressions
|
|
62
80
|
const rawHTML = "<em>Italic</em> and <strong>bold</strong>";
|
|
63
81
|
const markup = "<mark>Highlighted</mark>";
|
|
64
82
|
bench.add("unescaped expressions", () => {
|
|
@@ -72,6 +90,28 @@ bench.add("unescaped expressions", () => {
|
|
|
72
90
|
`;
|
|
73
91
|
});
|
|
74
92
|
|
|
93
|
+
bench.add("escaped expressions", () => {
|
|
94
|
+
html`
|
|
95
|
+
<div>${rawHTML}</div>
|
|
96
|
+
<div>${rawHTML}</div>
|
|
97
|
+
<div>${markup}</div>
|
|
98
|
+
<div>${markup}</div>
|
|
99
|
+
<div>${rawHTML}</div>
|
|
100
|
+
<div>${rawHTML}</div>
|
|
101
|
+
`;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
bench.add("mixed escaped and unescaped expressions", () => {
|
|
105
|
+
html`
|
|
106
|
+
<div>!${rawHTML}</div>
|
|
107
|
+
<div>!${rawHTML}</div>
|
|
108
|
+
<div>${markup}</div>
|
|
109
|
+
<div>${markup}</div>
|
|
110
|
+
<div>!${rawHTML}</div>
|
|
111
|
+
<div>!${rawHTML}</div>
|
|
112
|
+
`;
|
|
113
|
+
});
|
|
114
|
+
|
|
75
115
|
await bench.warmup();
|
|
76
116
|
await bench.run();
|
|
77
117
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Replace your template engine with fast JavaScript by leveraging the power of tagged templates.",
|
|
4
4
|
"author": "Gürgün Dayıoğlu",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.7.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.js",
|
|
9
9
|
"exports": {
|
package/src/html.js
CHANGED
|
@@ -8,21 +8,37 @@ const escapeDictionary = {
|
|
|
8
8
|
|
|
9
9
|
const escapeRegExp = new RegExp(
|
|
10
10
|
`[${Object.keys(escapeDictionary).join("")}]`,
|
|
11
|
-
"
|
|
11
|
+
"u",
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
-
const escapeFunction = (
|
|
15
|
-
|
|
14
|
+
const escapeFunction = (string) => {
|
|
15
|
+
const stringLength = string.length;
|
|
16
|
+
let start = 0;
|
|
17
|
+
let end = 0;
|
|
18
|
+
let escaped = "";
|
|
19
|
+
|
|
20
|
+
do {
|
|
21
|
+
const escapedCharacter = escapeDictionary[string[end++]];
|
|
22
|
+
|
|
23
|
+
if (escapedCharacter) {
|
|
24
|
+
escaped += string.slice(start, end - 1) + escapedCharacter;
|
|
25
|
+
start = end;
|
|
26
|
+
}
|
|
27
|
+
} while (end !== stringLength);
|
|
28
|
+
|
|
29
|
+
return escaped + string.slice(start, end);
|
|
16
30
|
};
|
|
17
31
|
|
|
32
|
+
const arrayIsArray = Array.isArray;
|
|
33
|
+
|
|
18
34
|
/**
|
|
19
35
|
* @param {{ raw: string[] }} literals Tagged template literals.
|
|
20
36
|
* @param {...any} expressions Expressions to interpolate.
|
|
21
37
|
* @returns {string} The HTML string.
|
|
22
38
|
*/
|
|
23
39
|
const html = ({ raw: literals }, ...expressions) => {
|
|
24
|
-
let accumulator = "";
|
|
25
40
|
let index = 0;
|
|
41
|
+
let accumulator = "";
|
|
26
42
|
|
|
27
43
|
for (; index !== expressions.length; ++index) {
|
|
28
44
|
const expression = expressions[index];
|
|
@@ -32,20 +48,20 @@ const html = ({ raw: literals }, ...expressions) => {
|
|
|
32
48
|
? ""
|
|
33
49
|
: typeof expression === "string"
|
|
34
50
|
? expression
|
|
35
|
-
:
|
|
51
|
+
: arrayIsArray(expression)
|
|
36
52
|
? expression.join("")
|
|
37
53
|
: `${expression}`;
|
|
38
54
|
|
|
39
|
-
if (literal
|
|
55
|
+
if (literal && literal.charCodeAt(literal.length - 1) === 33) {
|
|
40
56
|
literal = literal.slice(0, -1);
|
|
41
|
-
} else if (string.
|
|
42
|
-
string = string
|
|
57
|
+
} else if (string && escapeRegExp.test(string)) {
|
|
58
|
+
string = escapeFunction(string);
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
accumulator += literal + string;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
return
|
|
64
|
+
return accumulator + literals[index];
|
|
49
65
|
};
|
|
50
66
|
|
|
51
67
|
/**
|
|
@@ -68,13 +84,13 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
68
84
|
} else {
|
|
69
85
|
if (expression[Symbol.iterator]) {
|
|
70
86
|
const isRaw =
|
|
71
|
-
literal
|
|
87
|
+
literal !== "" && literal.charCodeAt(literal.length - 1) === 33;
|
|
72
88
|
|
|
73
89
|
if (isRaw) {
|
|
74
90
|
literal = literal.slice(0, -1);
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
if (literal
|
|
93
|
+
if (literal) {
|
|
78
94
|
yield literal;
|
|
79
95
|
}
|
|
80
96
|
|
|
@@ -94,9 +110,9 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
94
110
|
|
|
95
111
|
string = `${expression}`;
|
|
96
112
|
|
|
97
|
-
if (string
|
|
98
|
-
if (!isRaw) {
|
|
99
|
-
string = string
|
|
113
|
+
if (string) {
|
|
114
|
+
if (!isRaw && escapeRegExp.test(string)) {
|
|
115
|
+
string = escapeFunction(string);
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
yield string;
|
|
@@ -109,9 +125,9 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
109
125
|
string = `${expression}`;
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
if (string
|
|
113
|
-
if (!isRaw) {
|
|
114
|
-
string = string
|
|
128
|
+
if (string) {
|
|
129
|
+
if (!isRaw && escapeRegExp.test(string)) {
|
|
130
|
+
string = escapeFunction(string);
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
yield string;
|
|
@@ -124,18 +140,18 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
124
140
|
string = `${expression}`;
|
|
125
141
|
}
|
|
126
142
|
|
|
127
|
-
if (literal
|
|
143
|
+
if (literal && literal.charCodeAt(literal.length - 1) === 33) {
|
|
128
144
|
literal = literal.slice(0, -1);
|
|
129
|
-
} else if (string.
|
|
130
|
-
string = string
|
|
145
|
+
} else if (string && escapeRegExp.test(string)) {
|
|
146
|
+
string = escapeFunction(string);
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
if (literal
|
|
149
|
+
if (literal || string) {
|
|
134
150
|
yield literal + string;
|
|
135
151
|
}
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
if (literals[index]
|
|
154
|
+
if (literals[index]) {
|
|
139
155
|
yield literals[index];
|
|
140
156
|
}
|
|
141
157
|
};
|
|
@@ -160,13 +176,13 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
160
176
|
} else {
|
|
161
177
|
if (expression[Symbol.iterator] || expression[Symbol.asyncIterator]) {
|
|
162
178
|
const isRaw =
|
|
163
|
-
literal
|
|
179
|
+
literal !== "" && literal.charCodeAt(literal.length - 1) === 33;
|
|
164
180
|
|
|
165
181
|
if (isRaw) {
|
|
166
182
|
literal = literal.slice(0, -1);
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
if (literal
|
|
185
|
+
if (literal) {
|
|
170
186
|
yield literal;
|
|
171
187
|
}
|
|
172
188
|
|
|
@@ -189,9 +205,9 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
189
205
|
|
|
190
206
|
string = `${expression}`;
|
|
191
207
|
|
|
192
|
-
if (string
|
|
193
|
-
if (!isRaw) {
|
|
194
|
-
string = string
|
|
208
|
+
if (string) {
|
|
209
|
+
if (!isRaw && escapeRegExp.test(string)) {
|
|
210
|
+
string = escapeFunction(string);
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
yield string;
|
|
@@ -204,9 +220,9 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
204
220
|
string = `${expression}`;
|
|
205
221
|
}
|
|
206
222
|
|
|
207
|
-
if (string
|
|
208
|
-
if (!isRaw) {
|
|
209
|
-
string = string
|
|
223
|
+
if (string) {
|
|
224
|
+
if (!isRaw && escapeRegExp.test(string)) {
|
|
225
|
+
string = escapeFunction(string);
|
|
210
226
|
}
|
|
211
227
|
|
|
212
228
|
yield string;
|
|
@@ -219,18 +235,18 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
219
235
|
string = `${expression}`;
|
|
220
236
|
}
|
|
221
237
|
|
|
222
|
-
if (literal
|
|
238
|
+
if (literal && literal.charCodeAt(literal.length - 1) === 33) {
|
|
223
239
|
literal = literal.slice(0, -1);
|
|
224
|
-
} else if (string.
|
|
225
|
-
string = string
|
|
240
|
+
} else if (string && escapeRegExp.test(string)) {
|
|
241
|
+
string = escapeFunction(string);
|
|
226
242
|
}
|
|
227
243
|
|
|
228
|
-
if (literal
|
|
244
|
+
if (literal || string) {
|
|
229
245
|
yield literal + string;
|
|
230
246
|
}
|
|
231
247
|
}
|
|
232
248
|
|
|
233
|
-
if (literals[index]
|
|
249
|
+
if (literals[index]) {
|
|
234
250
|
yield literals[index];
|
|
235
251
|
}
|
|
236
252
|
};
|
package/test/index.js
CHANGED
|
@@ -174,7 +174,7 @@ test("htmlGenerator works with nested htmlGenerator calls in an array", () => {
|
|
|
174
174
|
assert.strictEqual(generator.next().done, true);
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
test("htmlGenerator works with other generators", () => {
|
|
177
|
+
test("htmlGenerator works with other generators (raw)", () => {
|
|
178
178
|
const generator = htmlGenerator`<div>!${generatorExample()}</div>`;
|
|
179
179
|
let accumulator = "";
|
|
180
180
|
|
|
@@ -189,6 +189,21 @@ test("htmlGenerator works with other generators", () => {
|
|
|
189
189
|
assert.strictEqual(generator.next().done, true);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
test("htmlGenerator works with other generators (escaped)", () => {
|
|
193
|
+
const generator = htmlGenerator`<div>${generatorExample()}</div>`;
|
|
194
|
+
let accumulator = "";
|
|
195
|
+
|
|
196
|
+
for (const value of generator) {
|
|
197
|
+
accumulator += value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
assert.strictEqual(
|
|
201
|
+
accumulator,
|
|
202
|
+
"<div><p>This is a safe description.<script>alert('This is an unsafe description.')</script>12345255</p></div>",
|
|
203
|
+
);
|
|
204
|
+
assert.strictEqual(generator.next().done, true);
|
|
205
|
+
});
|
|
206
|
+
|
|
192
207
|
test("htmlGenerator works with other generators within an array (raw)", () => {
|
|
193
208
|
const generator = htmlGenerator`<div>!${[generatorExample()]}</div>`;
|
|
194
209
|
let accumulator = "";
|
|
@@ -247,6 +262,34 @@ test("htmlAsyncGenerator renders unsafe content", async () => {
|
|
|
247
262
|
);
|
|
248
263
|
});
|
|
249
264
|
|
|
265
|
+
test("htmlAsyncGenerator works with other generators (raw)", async () => {
|
|
266
|
+
const generator = htmlAsyncGenerator`<div>!${generatorExample()}</div>`;
|
|
267
|
+
let accumulator = "";
|
|
268
|
+
|
|
269
|
+
for await (const value of generator) {
|
|
270
|
+
accumulator += value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
assert.strictEqual(
|
|
274
|
+
accumulator,
|
|
275
|
+
"<div><p>This is a safe description.<script>alert('This is an unsafe description.')</script>12345255</p></div>",
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("htmlAsyncGenerator works with other generators (escaped)", async () => {
|
|
280
|
+
const generator = htmlAsyncGenerator`<div>${generatorExample()}</div>`;
|
|
281
|
+
let accumulator = "";
|
|
282
|
+
|
|
283
|
+
for await (const value of generator) {
|
|
284
|
+
accumulator += value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
assert.strictEqual(
|
|
288
|
+
accumulator,
|
|
289
|
+
"<div><p>This is a safe description.<script>alert('This is an unsafe description.')</script>12345255</p></div>",
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
250
293
|
test("htmlAsyncGenerator works with nested htmlAsyncGenerator calls in an array", async () => {
|
|
251
294
|
const generator = htmlAsyncGenerator`!${[1, 2, 3].map((i) => {
|
|
252
295
|
return htmlAsyncGenerator`${i}: <p>${readFile("test/test.md", "utf8")}</p>`;
|