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 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
- ```shell
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`<html>
96
- <p>${generator()}</p>
97
- </html>`;
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 Hello = await new Promise((resolve) => {
137
+ const helloWorld = new Promise((resolve) => {
116
138
  setTimeout(() => {
117
- resolve("Hello");
118
- }, 1000);
139
+ resolve("<br /><br />Hello, World!");
140
+ }, 2500);
119
141
  });
120
- yield `${Hello}!`;
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`<html>
126
- <p>${asyncGenerator()}</p>
127
- <code>${readFile("./README.md", "utf8")}</code>
128
- <code>${createReadStream("./README.md", "utf8")}</code>
129
- </html>`;
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 expressions", () => {
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
- const items2 = ["Item 1", "Item 2", "Item 3"];
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
- const scriptContent =
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.5.2",
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
- "gu",
11
+ "u",
12
12
  );
13
13
 
14
- const escapeFunction = (key) => {
15
- return escapeDictionary[key];
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
- : Array.isArray(expression)
51
+ : arrayIsArray(expression)
36
52
  ? expression.join("")
37
53
  : `${expression}`;
38
54
 
39
- if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
55
+ if (literal && literal.charCodeAt(literal.length - 1) === 33) {
40
56
  literal = literal.slice(0, -1);
41
- } else if (string.length) {
42
- string = string.replace(escapeRegExp, escapeFunction);
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 (accumulator += literals[index]);
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.length !== 0 && literal.charCodeAt(literal.length - 1) === 33;
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.length) {
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.length) {
98
- if (!isRaw) {
99
- string = string.replace(escapeRegExp, escapeFunction);
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.length) {
113
- if (!isRaw) {
114
- string = string.replace(escapeRegExp, escapeFunction);
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.length && literal.charCodeAt(literal.length - 1) === 33) {
143
+ if (literal && literal.charCodeAt(literal.length - 1) === 33) {
128
144
  literal = literal.slice(0, -1);
129
- } else if (string.length) {
130
- string = string.replace(escapeRegExp, escapeFunction);
145
+ } else if (string && escapeRegExp.test(string)) {
146
+ string = escapeFunction(string);
131
147
  }
132
148
 
133
- if (literal.length || string.length) {
149
+ if (literal || string) {
134
150
  yield literal + string;
135
151
  }
136
152
  }
137
153
 
138
- if (literals[index].length) {
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.length !== 0 && literal.charCodeAt(literal.length - 1) === 33;
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.length) {
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.length) {
193
- if (!isRaw) {
194
- string = string.replace(escapeRegExp, escapeFunction);
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.length) {
208
- if (!isRaw) {
209
- string = string.replace(escapeRegExp, escapeFunction);
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.length && literal.charCodeAt(literal.length - 1) === 33) {
238
+ if (literal && literal.charCodeAt(literal.length - 1) === 33) {
223
239
  literal = literal.slice(0, -1);
224
- } else if (string.length) {
225
- string = string.replace(escapeRegExp, escapeFunction);
240
+ } else if (string && escapeRegExp.test(string)) {
241
+ string = escapeFunction(string);
226
242
  }
227
243
 
228
- if (literal.length || string.length) {
244
+ if (literal || string) {
229
245
  yield literal + string;
230
246
  }
231
247
  }
232
248
 
233
- if (literals[index].length) {
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>&lt;p&gt;This is a safe description.&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;12345255&lt;/p&gt;</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>&lt;p&gt;This is a safe description.&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;12345255&lt;/p&gt;</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>`;