ghtml 1.5.1 → 1.6.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 +34 -11
- package/bench/index.js +50 -10
- package/package.json +1 -1
- package/src/html.js +25 -19
- package/test.js +0 -27
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ 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
27
|
This version of HTML generator should be preferred for asynchronous use cases. The output will be generated as the promise expressions resolve.
|
|
@@ -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,22 @@ 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 = await new Promise((resolve) => {
|
|
116
138
|
setTimeout(() => {
|
|
117
|
-
resolve("Hello");
|
|
139
|
+
resolve("Hello, World!");
|
|
118
140
|
}, 1000);
|
|
119
141
|
});
|
|
120
|
-
yield
|
|
142
|
+
yield helloWorld;
|
|
121
143
|
};
|
|
122
144
|
|
|
123
145
|
http
|
|
124
146
|
.createServer((req, res) => {
|
|
125
|
-
const htmlContent = html
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
147
|
+
const htmlContent = html`<!doctype html>
|
|
148
|
+
<html>
|
|
149
|
+
<p>${asyncGenerator()}</p>
|
|
150
|
+
<code>${readFile("./README.md", "utf8")}</code>
|
|
151
|
+
<code>${createReadStream("./README.md", "utf8")}</code>
|
|
152
|
+
</html>`;
|
|
130
153
|
const readableStream = Readable.from(htmlContent);
|
|
131
154
|
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
132
155
|
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.6.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.js",
|
|
9
9
|
"exports": {
|
package/src/html.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const escapeDictionary = {
|
|
2
2
|
'"': """,
|
|
3
3
|
"'": "'",
|
|
4
4
|
"&": "&",
|
|
@@ -6,12 +6,17 @@ const escapeDict = {
|
|
|
6
6
|
">": ">",
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const escapeRegExp = new RegExp(
|
|
10
|
+
`[${Object.keys(escapeDictionary).join("")}]`,
|
|
11
|
+
"gu",
|
|
12
|
+
);
|
|
10
13
|
|
|
11
|
-
const
|
|
12
|
-
return
|
|
14
|
+
const escapeFunction = (key) => {
|
|
15
|
+
return escapeDictionary[key];
|
|
13
16
|
};
|
|
14
17
|
|
|
18
|
+
const arrayIsArray = Array.isArray;
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* @param {{ raw: string[] }} literals Tagged template literals.
|
|
17
22
|
* @param {...any} expressions Expressions to interpolate.
|
|
@@ -22,20 +27,21 @@ const html = ({ raw: literals }, ...expressions) => {
|
|
|
22
27
|
let index = 0;
|
|
23
28
|
|
|
24
29
|
for (; index !== expressions.length; ++index) {
|
|
30
|
+
const expression = expressions[index];
|
|
25
31
|
let literal = literals[index];
|
|
26
32
|
let string =
|
|
27
|
-
|
|
33
|
+
expression === undefined || expression === null
|
|
28
34
|
? ""
|
|
29
|
-
: typeof
|
|
30
|
-
?
|
|
31
|
-
:
|
|
32
|
-
?
|
|
33
|
-
: `${
|
|
35
|
+
: typeof expression === "string"
|
|
36
|
+
? expression
|
|
37
|
+
: arrayIsArray(expression)
|
|
38
|
+
? expression.join("")
|
|
39
|
+
: `${expression}`;
|
|
34
40
|
|
|
35
41
|
if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
|
|
36
42
|
literal = literal.slice(0, -1);
|
|
37
43
|
} else if (string.length) {
|
|
38
|
-
string = string.replace(
|
|
44
|
+
string = string.replace(escapeRegExp, escapeFunction);
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
accumulator += literal + string;
|
|
@@ -53,8 +59,8 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
53
59
|
let index = 0;
|
|
54
60
|
|
|
55
61
|
for (; index !== expressions.length; ++index) {
|
|
56
|
-
let literal = literals[index];
|
|
57
62
|
let expression = expressions[index];
|
|
63
|
+
let literal = literals[index];
|
|
58
64
|
let string;
|
|
59
65
|
|
|
60
66
|
if (expression === undefined || expression === null) {
|
|
@@ -92,7 +98,7 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
92
98
|
|
|
93
99
|
if (string.length) {
|
|
94
100
|
if (!isRaw) {
|
|
95
|
-
string = string.replace(
|
|
101
|
+
string = string.replace(escapeRegExp, escapeFunction);
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
yield string;
|
|
@@ -107,7 +113,7 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
107
113
|
|
|
108
114
|
if (string.length) {
|
|
109
115
|
if (!isRaw) {
|
|
110
|
-
string = string.replace(
|
|
116
|
+
string = string.replace(escapeRegExp, escapeFunction);
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
yield string;
|
|
@@ -123,7 +129,7 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
123
129
|
if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
|
|
124
130
|
literal = literal.slice(0, -1);
|
|
125
131
|
} else if (string.length) {
|
|
126
|
-
string = string.replace(
|
|
132
|
+
string = string.replace(escapeRegExp, escapeFunction);
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
if (literal.length || string.length) {
|
|
@@ -145,8 +151,8 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
145
151
|
let index = 0;
|
|
146
152
|
|
|
147
153
|
for (; index !== expressions.length; ++index) {
|
|
148
|
-
let literal = literals[index];
|
|
149
154
|
let expression = await expressions[index];
|
|
155
|
+
let literal = literals[index];
|
|
150
156
|
let string;
|
|
151
157
|
|
|
152
158
|
if (expression === undefined || expression === null) {
|
|
@@ -187,7 +193,7 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
187
193
|
|
|
188
194
|
if (string.length) {
|
|
189
195
|
if (!isRaw) {
|
|
190
|
-
string = string.replace(
|
|
196
|
+
string = string.replace(escapeRegExp, escapeFunction);
|
|
191
197
|
}
|
|
192
198
|
|
|
193
199
|
yield string;
|
|
@@ -202,7 +208,7 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
202
208
|
|
|
203
209
|
if (string.length) {
|
|
204
210
|
if (!isRaw) {
|
|
205
|
-
string = string.replace(
|
|
211
|
+
string = string.replace(escapeRegExp, escapeFunction);
|
|
206
212
|
}
|
|
207
213
|
|
|
208
214
|
yield string;
|
|
@@ -218,7 +224,7 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
|
218
224
|
if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
|
|
219
225
|
literal = literal.slice(0, -1);
|
|
220
226
|
} else if (string.length) {
|
|
221
|
-
string = string.replace(
|
|
227
|
+
string = string.replace(escapeRegExp, escapeFunction);
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
if (literal.length || string.length) {
|
package/test.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { htmlAsyncGenerator as html } from "ghtml";
|
|
2
|
-
import { createReadStream } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { Readable } from "node:stream";
|
|
5
|
-
import http from "node:http";
|
|
6
|
-
|
|
7
|
-
const asyncGenerator = async function* () {
|
|
8
|
-
const Hello = await new Promise((resolve) => {
|
|
9
|
-
setTimeout(() => {
|
|
10
|
-
resolve("Hello");
|
|
11
|
-
}, 1000);
|
|
12
|
-
});
|
|
13
|
-
yield `${Hello}!`;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
http
|
|
17
|
-
.createServer((req, res) => {
|
|
18
|
-
const htmlContent = html`<html>
|
|
19
|
-
<p>${asyncGenerator()}</p>
|
|
20
|
-
<code>${readFile("./README.md", "utf8")}</code>
|
|
21
|
-
<code>${createReadStream("./README.md", "utf8")}</code>
|
|
22
|
-
</html>`;
|
|
23
|
-
const readableStream = Readable.from(htmlContent);
|
|
24
|
-
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
25
|
-
readableStream.pipe(res);
|
|
26
|
-
})
|
|
27
|
-
.listen(3000);
|