ghtml 1.6.0 → 1.7.1

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
 
@@ -24,9 +24,9 @@ Keep in mind that, in Node.js, all else being equal, streaming a response using
24
24
 
25
25
  ### `htmlAsyncGenerator`
26
26
 
27
- 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.
28
28
 
29
- **Note:**
29
+ **Minor Note:**
30
30
 
31
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>`)}``.
32
32
 
@@ -134,11 +134,12 @@ import { Readable } from "node:stream";
134
134
  import http from "node:http";
135
135
 
136
136
  const asyncGenerator = async function* () {
137
- const helloWorld = await new Promise((resolve) => {
137
+ const helloWorld = new Promise((resolve) => {
138
138
  setTimeout(() => {
139
- resolve("Hello, World!");
140
- }, 1000);
139
+ resolve("<br /><br />Hello, World!");
140
+ }, 2500);
141
141
  });
142
+ yield await readFile("./.gitignore", "utf8");
142
143
  yield helloWorld;
143
144
  };
144
145
 
@@ -146,7 +147,7 @@ http
146
147
  .createServer((req, res) => {
147
148
  const htmlContent = html`<!doctype html>
148
149
  <html>
149
- <p>${asyncGenerator()}</p>
150
+ <p>!${asyncGenerator()}</p>
150
151
  <code>${readFile("./README.md", "utf8")}</code>
151
152
  <code>${createReadStream("./README.md", "utf8")}</code>
152
153
  </html>`;
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.0",
6
+ "version": "1.7.1",
7
7
  "type": "module",
8
8
  "main": "./src/index.js",
9
9
  "exports": {
@@ -22,8 +22,8 @@
22
22
  "devDependencies": {
23
23
  "@fastify/pre-commit": "^2.1.0",
24
24
  "c8": "^9.1.0",
25
- "grules": "^0.16.2",
26
- "tinybench": "^2.6.0"
25
+ "grules": "^0.17.1",
26
+ "tinybench": "^2.8.0"
27
27
  },
28
28
  "repository": {
29
29
  "type": "git",
package/src/html.js CHANGED
@@ -8,11 +8,24 @@ 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
+ if (escapedCharacter) {
23
+ escaped += string.slice(start, end - 1) + escapedCharacter;
24
+ start = end;
25
+ }
26
+ } while (end !== stringLength);
27
+
28
+ return escaped + string.slice(start, end);
16
29
  };
17
30
 
18
31
  const arrayIsArray = Array.isArray;
@@ -23,10 +36,11 @@ const arrayIsArray = Array.isArray;
23
36
  * @returns {string} The HTML string.
24
37
  */
25
38
  const html = ({ raw: literals }, ...expressions) => {
26
- let accumulator = "";
39
+ const expressionsLength = expressions.length;
27
40
  let index = 0;
41
+ let accumulator = "";
28
42
 
29
- for (; index !== expressions.length; ++index) {
43
+ for (; index !== expressionsLength; ++index) {
30
44
  const expression = expressions[index];
31
45
  let literal = literals[index];
32
46
  let string =
@@ -38,16 +52,16 @@ const html = ({ raw: literals }, ...expressions) => {
38
52
  ? expression.join("")
39
53
  : `${expression}`;
40
54
 
41
- if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
55
+ if (literal && literal.charCodeAt(literal.length - 1) === 33) {
42
56
  literal = literal.slice(0, -1);
43
- } else if (string.length) {
44
- string = string.replace(escapeRegExp, escapeFunction);
57
+ } else if (string && escapeRegExp.test(string)) {
58
+ string = escapeFunction(string);
45
59
  }
46
60
 
47
61
  accumulator += literal + string;
48
62
  }
49
63
 
50
- return (accumulator += literals[index]);
64
+ return accumulator + literals[index];
51
65
  };
52
66
 
53
67
  /**
@@ -56,9 +70,10 @@ const html = ({ raw: literals }, ...expressions) => {
56
70
  * @yields {string} The HTML strings.
57
71
  */
58
72
  const htmlGenerator = function* ({ raw: literals }, ...expressions) {
73
+ const expressionsLength = expressions.length;
59
74
  let index = 0;
60
75
 
61
- for (; index !== expressions.length; ++index) {
76
+ for (; index !== expressionsLength; ++index) {
62
77
  let expression = expressions[index];
63
78
  let literal = literals[index];
64
79
  let string;
@@ -70,13 +85,13 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
70
85
  } else {
71
86
  if (expression[Symbol.iterator]) {
72
87
  const isRaw =
73
- literal.length !== 0 && literal.charCodeAt(literal.length - 1) === 33;
88
+ literal !== "" && literal.charCodeAt(literal.length - 1) === 33;
74
89
 
75
90
  if (isRaw) {
76
91
  literal = literal.slice(0, -1);
77
92
  }
78
93
 
79
- if (literal.length) {
94
+ if (literal) {
80
95
  yield literal;
81
96
  }
82
97
 
@@ -96,9 +111,9 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
96
111
 
97
112
  string = `${expression}`;
98
113
 
99
- if (string.length) {
100
- if (!isRaw) {
101
- string = string.replace(escapeRegExp, escapeFunction);
114
+ if (string) {
115
+ if (!isRaw && escapeRegExp.test(string)) {
116
+ string = escapeFunction(string);
102
117
  }
103
118
 
104
119
  yield string;
@@ -111,9 +126,9 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
111
126
  string = `${expression}`;
112
127
  }
113
128
 
114
- if (string.length) {
115
- if (!isRaw) {
116
- string = string.replace(escapeRegExp, escapeFunction);
129
+ if (string) {
130
+ if (!isRaw && escapeRegExp.test(string)) {
131
+ string = escapeFunction(string);
117
132
  }
118
133
 
119
134
  yield string;
@@ -126,18 +141,18 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
126
141
  string = `${expression}`;
127
142
  }
128
143
 
129
- if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
144
+ if (literal && literal.charCodeAt(literal.length - 1) === 33) {
130
145
  literal = literal.slice(0, -1);
131
- } else if (string.length) {
132
- string = string.replace(escapeRegExp, escapeFunction);
146
+ } else if (string && escapeRegExp.test(string)) {
147
+ string = escapeFunction(string);
133
148
  }
134
149
 
135
- if (literal.length || string.length) {
150
+ if (literal || string) {
136
151
  yield literal + string;
137
152
  }
138
153
  }
139
154
 
140
- if (literals[index].length) {
155
+ if (literals[index]) {
141
156
  yield literals[index];
142
157
  }
143
158
  };
@@ -148,9 +163,10 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
148
163
  * @yields {string} The HTML strings.
149
164
  */
150
165
  const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
166
+ const expressionsLength = expressions.length;
151
167
  let index = 0;
152
168
 
153
- for (; index !== expressions.length; ++index) {
169
+ for (; index !== expressionsLength; ++index) {
154
170
  let expression = await expressions[index];
155
171
  let literal = literals[index];
156
172
  let string;
@@ -162,13 +178,13 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
162
178
  } else {
163
179
  if (expression[Symbol.iterator] || expression[Symbol.asyncIterator]) {
164
180
  const isRaw =
165
- literal.length !== 0 && literal.charCodeAt(literal.length - 1) === 33;
181
+ literal !== "" && literal.charCodeAt(literal.length - 1) === 33;
166
182
 
167
183
  if (isRaw) {
168
184
  literal = literal.slice(0, -1);
169
185
  }
170
186
 
171
- if (literal.length) {
187
+ if (literal) {
172
188
  yield literal;
173
189
  }
174
190
 
@@ -191,9 +207,9 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
191
207
 
192
208
  string = `${expression}`;
193
209
 
194
- if (string.length) {
195
- if (!isRaw) {
196
- string = string.replace(escapeRegExp, escapeFunction);
210
+ if (string) {
211
+ if (!isRaw && escapeRegExp.test(string)) {
212
+ string = escapeFunction(string);
197
213
  }
198
214
 
199
215
  yield string;
@@ -206,9 +222,9 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
206
222
  string = `${expression}`;
207
223
  }
208
224
 
209
- if (string.length) {
210
- if (!isRaw) {
211
- string = string.replace(escapeRegExp, escapeFunction);
225
+ if (string) {
226
+ if (!isRaw && escapeRegExp.test(string)) {
227
+ string = escapeFunction(string);
212
228
  }
213
229
 
214
230
  yield string;
@@ -221,18 +237,18 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
221
237
  string = `${expression}`;
222
238
  }
223
239
 
224
- if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
240
+ if (literal && literal.charCodeAt(literal.length - 1) === 33) {
225
241
  literal = literal.slice(0, -1);
226
- } else if (string.length) {
227
- string = string.replace(escapeRegExp, escapeFunction);
242
+ } else if (string && escapeRegExp.test(string)) {
243
+ string = escapeFunction(string);
228
244
  }
229
245
 
230
- if (literal.length || string.length) {
246
+ if (literal || string) {
231
247
  yield literal + string;
232
248
  }
233
249
  }
234
250
 
235
- if (literals[index].length) {
251
+ if (literals[index]) {
236
252
  yield literals[index];
237
253
  }
238
254
  };
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>`;