ghtml 1.7.2 → 2.0.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
@@ -12,7 +12,7 @@ npm i ghtml
12
12
 
13
13
  ### `html`
14
14
 
15
- The `html` function is designed to tag template literals and automatically escape their expressions to prevent XSS attacks. To intentionally bypass escaping for a specific expression, prefix it with `!`.
15
+ The `html` function is designed to tag template literals and automatically escape their expressions. To intentionally bypass escaping a specific expression, prefix it with `!`.
16
16
 
17
17
  ### `htmlGenerator`
18
18
 
@@ -20,11 +20,11 @@ The `htmlGenerator` function acts as the generator version of the `html` functio
20
20
 
21
21
  **Note:**
22
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.
23
+ Keep in mind that, in Node.js, all else being equal, streaming a response using synchronous generators is **always** 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 does indeed improve TTFB.
24
24
 
25
25
  ### `htmlAsyncGenerator`
26
26
 
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.
27
+ This version of HTML generator should be preferred for asynchronous and streaming use cases. The output is generated as the promise expressions resolve or stream expressions send data.
28
28
 
29
29
  **Minor Note:**
30
30
 
@@ -32,7 +32,7 @@ Because they return generators instead of strings, a key difference of `htmlGene
32
32
 
33
33
  ### `includeFile`
34
34
 
35
- Available for Node.js users, the `includeFile` function is a wrapper around `readFileSync`. It reads and outputs the content of a file while also caching it in memory for faster future reuse.
35
+ Available in Node.js, the `includeFile` function is a wrapper around `readFileSync`. It reads and outputs the content of a file while also caching it in memory for faster future reuse.
36
36
 
37
37
  ## Usage
38
38
 
@@ -41,11 +41,11 @@ Available for Node.js users, the `includeFile` function is a wrapper around `rea
41
41
  ```js
42
42
  import { html } from "ghtml";
43
43
 
44
- const username = '<img src="https://example.com/hacker.png">';
45
- const greeting = html`<h1>Hello, ${username}!</h1>`;
44
+ const username = '<img src="https://example.com/pwned.png">';
45
+ const greeting = html`<h1>Hello, ${username}</h1>`;
46
46
 
47
47
  console.log(greeting);
48
- // Output: <h1>Hello, &lt;img src=&quot;https://example.com/hacker.png&quot;&gt;</h1>
48
+ // Output: <h1>Hello, &#60;img src=&#34;https://example.com/pwned.png&#34;&#62;</h1>
49
49
  ```
50
50
 
51
51
  To bypass escaping:
@@ -58,7 +58,7 @@ console.log(container);
58
58
  // Output: <div><img src="https://example.com/safe.png"></div>
59
59
  ```
60
60
 
61
- When nesting multiple `html` expressions, always use `!` as they will do their own escaping:
61
+ When nesting multiple `html` expressions, make sure to use `!` as the inner calls do their own escaping:
62
62
 
63
63
  ```js
64
64
  const someCondition = Math.random() >= 0.5;
@@ -168,3 +168,7 @@ const logo = includeFile("static/logo.svg");
168
168
  console.log(logo);
169
169
  // Output: content of "static/logo.svg"
170
170
  ```
171
+
172
+ ## Security
173
+
174
+ Like [similar](https://handlebarsjs.com/guide/#html-escaping) [tools](https://github.com/mde/ejs/blob/main/SECURITY.md#out-of-scope-vulnerabilities), `ghtml` does not prevent all kinds of XSS attacks. It is the responsibility of consumers to sanitize user inputs. Some inherently insecure uses include dynamically generating JavaScript, failing to quote HTML attribute values (especially when they contain expressions), and using unsanitized user-provided URLs.
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.7.2",
6
+ "version": "2.0.1",
7
7
  "type": "module",
8
8
  "main": "./src/index.js",
9
9
  "exports": {
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "devDependencies": {
23
23
  "@fastify/pre-commit": "^2.1.0",
24
- "c8": "^9.1.0",
25
- "grules": "^0.17.1",
24
+ "c8": "^10.0.0",
25
+ "grules": "^0.17.2",
26
26
  "tinybench": "^2.8.0"
27
27
  },
28
28
  "repository": {
package/src/html.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const escapeDictionary = {
2
- '"': "&quot;",
3
- "'": "&apos;",
4
- "&": "&amp;",
5
- "<": "&lt;",
6
- ">": "&gt;",
2
+ '"': "&#34;",
3
+ "&": "&#38;",
4
+ "'": "&#39;",
5
+ "<": "&#60;",
6
+ ">": "&#62;",
7
+ "`": "&#96;",
7
8
  };
8
9
 
9
10
  const escapeRegExp = new RegExp(
@@ -19,6 +20,7 @@ const escapeFunction = (string) => {
19
20
 
20
21
  do {
21
22
  const escapedCharacter = escapeDictionary[string[end++]];
23
+
22
24
  if (escapedCharacter) {
23
25
  escaped += string.slice(start, end - 1) + escapedCharacter;
24
26
  start = end;
@@ -105,11 +107,15 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
105
107
 
106
108
  if (expression[Symbol.iterator]) {
107
109
  for (expression of expression) {
108
- if (expression === undefined || expression === null) {
109
- continue;
110
- }
110
+ if (typeof expression === "string") {
111
+ string = expression;
112
+ } else {
113
+ if (expression === undefined || expression === null) {
114
+ continue;
115
+ }
111
116
 
112
- string = `${expression}`;
117
+ string = `${expression}`;
118
+ }
113
119
 
114
120
  if (string) {
115
121
  if (!isRaw && escapeRegExp.test(string)) {
@@ -201,11 +207,15 @@ const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
201
207
  expression[Symbol.asyncIterator]
202
208
  ) {
203
209
  for await (expression of expression) {
204
- if (expression === undefined || expression === null) {
205
- continue;
206
- }
210
+ if (typeof expression === "string") {
211
+ string = expression;
212
+ } else {
213
+ if (expression === undefined || expression === null) {
214
+ continue;
215
+ }
207
216
 
208
- string = `${expression}`;
217
+ string = `${expression}`;
218
+ }
209
219
 
210
220
  if (string) {
211
221
  if (!isRaw && escapeRegExp.test(string)) {
package/test/index.js CHANGED
@@ -59,14 +59,14 @@ test("renders safe content", () => {
59
59
  test("renders unsafe content", () => {
60
60
  assert.strictEqual(
61
61
  html`<p>${descriptionUnsafe}</p>`,
62
- `<p>&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</p>`,
62
+ `<p>&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;</p>`,
63
63
  );
64
64
  });
65
65
 
66
66
  test("renders arrays", () => {
67
67
  assert.strictEqual(
68
68
  html`<p>${[descriptionSafe, descriptionUnsafe]}</p>`,
69
- "<p>This is a safe description.&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</p>",
69
+ "<p>This is a safe description.&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;</p>",
70
70
  );
71
71
  });
72
72
 
@@ -81,7 +81,7 @@ test("renders nested html calls", () => {
81
81
  // prettier-ignore
82
82
  assert.strictEqual(
83
83
  html`<p>!${conditionTrue ? html`<strong>${descriptionUnsafe}</strong>` : ""}</p>`,
84
- "<p><strong>&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</strong></p>",
84
+ "<p><strong>&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;</strong></p>",
85
85
  );
86
86
  });
87
87
 
@@ -156,7 +156,7 @@ test("htmlGenerator renders unsafe content", () => {
156
156
 
157
157
  assert.strictEqual(
158
158
  accumulator,
159
- "<p>This is a safe description.&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;12345255</p>",
159
+ "<p>This is a safe description.&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;12345255</p>",
160
160
  );
161
161
  });
162
162
 
@@ -199,7 +199,7 @@ test("htmlGenerator works with other generators (escaped)", () => {
199
199
 
200
200
  assert.strictEqual(
201
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>",
202
+ "<div>&#60;p&#62;This is a safe description.&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;12345255&#60;/p&#62;</div>",
203
203
  );
204
204
  assert.strictEqual(generator.next().done, true);
205
205
  });
@@ -229,7 +229,7 @@ test("htmlGenerator works with other generators within an array (escaped)", () =
229
229
 
230
230
  assert.strictEqual(
231
231
  accumulator,
232
- "<div>&lt;p&gt;This is a safe description.&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;1,2,3,4,5255&lt;/p&gt;</div>",
232
+ "<div>&#60;p&#62;This is a safe description.&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;1,2,3,4,5255&#60;/p&#62;</div>",
233
233
  );
234
234
  assert.strictEqual(generator.next().done, true);
235
235
  });
@@ -258,7 +258,7 @@ test("htmlAsyncGenerator renders unsafe content", async () => {
258
258
 
259
259
  assert.strictEqual(
260
260
  accumulator,
261
- "<p>This is a safe description.&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;12345255</p>",
261
+ "<p>This is a safe description.&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;12345255</p>",
262
262
  );
263
263
  });
264
264
 
@@ -286,7 +286,7 @@ test("htmlAsyncGenerator works with other generators (escaped)", async () => {
286
286
 
287
287
  assert.strictEqual(
288
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>",
289
+ "<div>&#60;p&#62;This is a safe description.&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;12345255&#60;/p&#62;</div>",
290
290
  );
291
291
  });
292
292
 
@@ -302,7 +302,7 @@ test("htmlAsyncGenerator works with nested htmlAsyncGenerator calls in an array"
302
302
 
303
303
  assert.strictEqual(
304
304
  accumulator.replaceAll("\n", "").trim(),
305
- "1: <p># test.md&gt;</p>2: <p># test.md&gt;</p>3: <p># test.md&gt;</p>",
305
+ "1: <p># test.md&#62;</p>2: <p># test.md&#62;</p>3: <p># test.md&#62;</p>",
306
306
  );
307
307
  });
308
308
 
@@ -312,7 +312,7 @@ test("htmlAsyncGenerator renders chunks with promises (escaped)", async () => {
312
312
  })}</ul>`;
313
313
  const fileContent = readFileSync("test/test.md", "utf8").replaceAll(
314
314
  ">",
315
- "&gt;",
315
+ "&#62;",
316
316
  );
317
317
 
318
318
  let value = await generator.next();
@@ -372,7 +372,7 @@ test("htmlAsyncGenerator redners in chuncks", async () => {
372
372
  assert.strictEqual(value.value, "<ul>");
373
373
 
374
374
  value = await generator.next();
375
- assert.strictEqual(value.value, "&lt;p&gt;");
375
+ assert.strictEqual(value.value, "&#60;p&#62;");
376
376
 
377
377
  value = await generator.next();
378
378
  assert.strictEqual(value.value, "12");