ghtml 2.2.1 → 3.0.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/.eslintrc.json CHANGED
@@ -1,4 +1,8 @@
1
1
  {
2
2
  "root": true,
3
- "extends": ["plugin:grules/all"]
3
+ "extends": ["plugin:grules/all"],
4
+ "rules": {
5
+ "no-await-in-loop": "off",
6
+ "require-unicode-regexp": "off"
7
+ }
4
8
  }
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
- ta**ghtml** lets you replace your template engine with fast JavaScript by leveraging the power of [tagged templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates).
1
+ # ghtml ![img.shields.io/bundlephobia/minzip/ghtml](https://img.shields.io/bundlephobia/minzip/ghtml)
2
2
 
3
- Inspired by [html-template-tag](https://github.com/AntonioVdlC/html-template-tag).
3
+ ta**ghtml** lets you replace your template engine with fast JavaScript by leveraging the power of tagged templates.
4
+
5
+ Works in the browser. No runtime dependencies. [~30x faster than React. ~10x faster than common-tags.](#benchmarks)
6
+
7
+ ![ghtml.gif](./ghtml.gif)
4
8
 
5
9
  ## Installation
6
10
 
@@ -8,6 +12,12 @@ Inspired by [html-template-tag](https://github.com/AntonioVdlC/html-template-tag
8
12
  npm i ghtml
9
13
  ```
10
14
 
15
+ Or import directly from a CDN:
16
+
17
+ ```js
18
+ import { html } from "https://cdn.jsdelivr.net/npm/ghtml/+esm";
19
+ ```
20
+
11
21
  ## API
12
22
 
13
23
  ### `html`
@@ -32,7 +42,7 @@ Because they return generators instead of strings, a key difference of `htmlGene
32
42
 
33
43
  ### `includeFile`
34
44
 
35
- Available in Node.js, the `includeFile` function is a wrapper around `readFileSync`. It reads and returns the content of a file while also caching it in memory for faster future reuse.
45
+ Available in Node.js, the `includeFile` function is a wrapper around `readFileSync`. It reads and returns the content of a file while caching it in memory for faster future reuse.
36
46
 
37
47
  ## Usage
38
48
 
@@ -45,7 +55,7 @@ const username = '<img src="https://example.com/pwned.png">';
45
55
  const greeting = html`<h1>Hello, ${username}</h1>`;
46
56
 
47
57
  console.log(greeting);
48
- // Output: <h1>Hello, &#60;img src=&#34;https://example.com/pwned.png&#34;&#62;</h1>
58
+ // Output: <h1>Hello, &#60;img src&#61;&#34;https://example.com/pwned.png&#34;&#62;</h1>
49
59
  ```
50
60
 
51
61
  To bypass escaping:
@@ -169,6 +179,38 @@ console.log(logo);
169
179
  // Output: content of "static/logo.svg"
170
180
  ```
171
181
 
182
+ ## Benchmarks
183
+
184
+ Latest results [from Kita](https://github.com/kitajs/html/tree/cb7950c68489ff70dd0b0c130c9b70046c1543ea/benchmarks):
185
+
186
+ ```sh
187
+ benchmark time (avg) (min … max) p75 p99 p999
188
+ --------------------------------------------------- -----------------------------
189
+ • Real World Scenario
190
+ --------------------------------------------------- -----------------------------
191
+ KitaJS/Html 505 µs/iter (387 µs … 2'007 µs) 417 µs 1'209 µs 1'857 µs
192
+ Typed Html 1'844 µs/iter (1'604 µs … 2'415 µs) 2'088 µs 2'211 µs 2'415 µs
193
+ VHtml 2'424 µs/iter (2'250 µs … 2'864 µs) 2'462 µs 2'829 µs 2'864 µs
194
+ React JSX 6'416 µs/iter (5'893 µs … 9'399 µs) 6'840 µs 9'399 µs 9'399 µs
195
+ Preact 970 µs/iter (673 µs … 5'038 µs) 766 µs 2'224 µs 5'038 µs
196
+ React 6'319 µs/iter (5'885 µs … 7'306 µs) 6'678 µs 7'306 µs 7'306 µs
197
+ Common Tags 2'967 µs/iter (2'774 µs … 3'801 µs) 2'916 µs 3'794 µs 3'801 µs
198
+ Ghtml 225 µs/iter (184 µs … 1'567 µs) 206 µs 1'066 µs 1'450 µs
199
+ JSXTE 4'489 µs/iter (3'605 µs … 6'215 µs) 4'517 µs 6'062 µs 6'215 µs
200
+
201
+
202
+ summary for Real World Scenario
203
+ Ghtml
204
+ 2.25x faster than KitaJS/Html
205
+ 4.32x faster than Preact
206
+ 8.21x faster than Typed Html
207
+ 10.8x faster than VHtml
208
+ 13.22x faster than Common Tags
209
+ 20x faster than JSXTE
210
+ 28.15x faster than React
211
+ 28.58x faster than React JSX
212
+ ```
213
+
172
214
  ## Security
173
215
 
174
- Like [similar](https://github.com/mde/ejs/blob/main/SECURITY.md#out-of-scope-vulnerabilities) [tools](https://handlebarsjs.com/guide/#html-escaping), ghtml does not prevent all kinds of XSS attacks. It is the responsibility of developers to sanitize user inputs. Some inherently insecure uses include dynamically generating JavaScript, failing to quote HTML attribute values (especially when they contain expressions), and relying on unsanitized user-provided URLs.
216
+ Like [similar tools](https://github.com/mde/ejs/blob/a4770b8ff49b93387c7f2760d957446cd332531a/SECURITY.md#out-of-scope-vulnerabilities), ghtml does not prevent all kinds of XSS attacks. It is the responsibility of developers to sanitize user inputs. Some inherently insecure uses include dynamically generating JavaScript, failing to quote HTML attribute values, and relying on unsanitized user-provided URIs.
package/SECURITY.md CHANGED
@@ -5,7 +5,8 @@
5
5
  | Version | Supported |
6
6
  | ------- | ------------------ |
7
7
  | ^1 | :x: |
8
- | ^2 | :white_check_mark: |
8
+ | ^2 | :x: |
9
+ | ^3 | :white_check_mark: |
9
10
 
10
11
  ## Reporting a Vulnerability
11
12
 
package/bench/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable no-unused-vars */
2
- /* eslint-disable no-unused-expressions */
3
2
  import { html } from "../src/index.js";
4
3
  import { Bench } from "tinybench";
5
4
  import { writeFileSync } from "node:fs";
@@ -8,7 +7,6 @@ import { Buffer } from "node:buffer";
8
7
  let result = "";
9
8
  const bench = new Bench({ time: 500 });
10
9
 
11
- // Simple cases
12
10
  bench.add("simple HTML formatting", () => {
13
11
  result = html`<div>Hello, world!</div>`;
14
12
  });
@@ -17,7 +15,6 @@ bench.add("null and undefined expressions", () => {
17
15
  result = html`<p>${null} and ${undefined}</p>`;
18
16
  });
19
17
 
20
- // String expressions
21
18
  const username = "User";
22
19
  bench.add("single string expression", () => {
23
20
  result = html`<p>${username}</p>`;
@@ -27,7 +24,6 @@ bench.add("multiple string expressions", () => {
27
24
  result = html`<p>${username} and ${username}</p>`;
28
25
  });
29
26
 
30
- // Array expressions
31
27
  const items1 = ["Item 1", undefined, "Item 2", null, 2000, 1500.5];
32
28
  bench.add("array expressions", () => {
33
29
  result = html`<ul>
@@ -46,7 +42,6 @@ bench.add("array expressions with escapable chars", () => {
46
42
  </ul>`;
47
43
  });
48
44
 
49
- // Object expressions
50
45
  const user = { id: 1, name: "John Doe" };
51
46
  bench.add("object expressions", () => {
52
47
  result = html`
@@ -55,7 +50,6 @@ bench.add("object expressions", () => {
55
50
  `;
56
51
  });
57
52
 
58
- // Mixed expressions
59
53
  bench.add("multiple types of expressions", () => {
60
54
  result = html`
61
55
  ${undefined}
@@ -70,17 +64,15 @@ bench.add("multiple types of expressions", () => {
70
64
  `;
71
65
  });
72
66
 
73
- // Large strings
74
67
  const largeString = Array.from({ length: 1000 }).join("Lorem ipsum ");
75
68
  bench.add("large strings", () => {
76
69
  result = html`<p>${largeString}${largeString}</p>`;
77
70
  });
78
71
 
79
- // Escaped and unescaped expressions
80
72
  const rawHTML = "<em>Italic</em> and <strong>bold</strong>";
81
73
  const markup = "<mark>Highlighted</mark>";
82
74
  bench.add("unescaped expressions", () => {
83
- html`
75
+ result = html`
84
76
  <div>!${rawHTML}</div>
85
77
  <div>!${rawHTML}</div>
86
78
  <div>!${markup}</div>
@@ -91,7 +83,7 @@ bench.add("unescaped expressions", () => {
91
83
  });
92
84
 
93
85
  bench.add("escaped expressions", () => {
94
- html`
86
+ result = html`
95
87
  <div>${rawHTML}</div>
96
88
  <div>${rawHTML}</div>
97
89
  <div>${markup}</div>
@@ -102,7 +94,7 @@ bench.add("escaped expressions", () => {
102
94
  });
103
95
 
104
96
  bench.add("mixed escaped and unescaped expressions", () => {
105
- html`
97
+ result = html`
106
98
  <div>!${rawHTML}</div>
107
99
  <div>!${rawHTML}</div>
108
100
  <div>${markup}</div>
package/bin/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Lets you append unique hashes to assets referenced in your views to aggressively cache them while guaranteeing that clients receive the most recent versions.
1
+ Append unique hashes to assets referenced in your views to aggressively cache them while guaranteeing that clients receive the most recent versions.
2
2
 
3
3
  ## Usage
4
4
 
@@ -27,7 +27,7 @@ Add the `ghtml` command to the build script:
27
27
 
28
28
  ```json
29
29
  "scripts": {
30
- "build": "npx ghtml --roots=assets/ --refs=views/,routes/",
30
+ "build": "npx ghtml --roots=assets/ --refs=views/,routes/ --prefix=/p/assets/",
31
31
  },
32
32
  ```
33
33
 
@@ -0,0 +1,6 @@
1
+ {
2
+ "rules": {
3
+ "require-await": "off",
4
+ "n/no-missing-import": "off"
5
+ }
6
+ }
@@ -3,7 +3,7 @@
3
3
  "main": "./server.js",
4
4
  "scripts": {
5
5
  "start": "node server.js",
6
- "build": "ghtml --roots=assets/ --refs=routes/"
6
+ "build": "ghtml --roots=assets/ --refs=routes/ --prefix=/p/assets/"
7
7
  },
8
8
  "dependencies": {
9
9
  "@fastify/static": "^7.0.1",
@@ -1,5 +1,3 @@
1
- /* eslint-disable n/no-missing-import */
2
-
3
1
  import { html } from "ghtml";
4
2
 
5
3
  export default async (fastify) => {
@@ -1,5 +1,3 @@
1
- /* eslint-disable n/no-missing-import */
2
-
3
1
  import Fastify from "fastify";
4
2
 
5
3
  const fastify = Fastify();
package/bin/src/index.js CHANGED
@@ -1,41 +1,45 @@
1
1
  #!/usr/bin/env node
2
-
3
2
  import { generateHashesAndReplace } from "./utils.js";
4
3
  import process from "node:process";
5
4
 
6
5
  const parseArguments = (args) => {
7
6
  let roots = null;
8
7
  let refs = null;
8
+ let prefix = "/";
9
9
 
10
10
  for (const arg of args) {
11
11
  if (arg.startsWith("--roots=")) {
12
12
  roots = arg.split("=", 2)[1].split(",");
13
13
  } else if (arg.startsWith("--refs=")) {
14
14
  refs = arg.split("=", 2)[1].split(",");
15
+ } else if (arg.startsWith("--prefix=")) {
16
+ prefix = arg.split("=", 2)[1];
15
17
  }
16
18
  }
17
19
 
18
20
  if (!roots || !refs) {
19
21
  console.error(
20
- 'Usage: npx ghtml --roots="path/to/scan/assets1/,path/to/scan/assets2/" --refs="views/path/to/append/hashes1/,views/path/to/append/hashes2/"',
22
+ 'Usage: npx ghtml --roots="base/path/to/scan/assets/1/,base/path/to/scan/assets/2/" --refs="views/path/to/append/hashes/1/,views/path/to/append/hashes/2/" [--prefix="/optional/prefix/"]',
21
23
  );
22
24
  process.exit(1);
23
25
  }
24
26
 
25
- return { roots, refs };
27
+ return { roots, refs, prefix };
26
28
  };
27
29
 
28
30
  const main = async () => {
29
- const { roots, refs } = parseArguments(process.argv.slice(2));
31
+ const { roots, refs, prefix } = parseArguments(process.argv.slice(2));
30
32
 
31
33
  try {
32
34
  console.warn(`Generating hashes and updating file paths...`);
33
35
  console.warn(`Scanning files in: ${roots}`);
34
36
  console.warn(`Updating files in: ${refs}`);
37
+ console.warn(`Using prefix: ${prefix}`);
35
38
 
36
39
  await generateHashesAndReplace({
37
40
  roots,
38
41
  refs,
42
+ prefix,
39
43
  });
40
44
 
41
45
  console.warn("Hash generation and file updates completed successfully.");
package/bin/src/utils.js CHANGED
@@ -1,7 +1,7 @@
1
+ import { Glob } from "glob";
1
2
  import { createHash } from "node:crypto";
2
3
  import { readFile, writeFile } from "node:fs/promises";
3
4
  import { win32, posix } from "node:path";
4
- import { Glob } from "glob";
5
5
 
6
6
  const generateFileHash = async (filePath) => {
7
7
  try {
@@ -18,6 +18,7 @@ const generateFileHash = async (filePath) => {
18
18
  const updateFilePathsWithHashes = async (
19
19
  fileHashes,
20
20
  refs,
21
+ prefix,
21
22
  includeDotFiles,
22
23
  skipPatterns,
23
24
  ) => {
@@ -40,14 +41,15 @@ const updateFilePathsWithHashes = async (
40
41
  let content = await readFile(filePath, "utf8");
41
42
  let found = false;
42
43
 
43
- for (const [originalPath, hash] of fileHashes) {
44
- const escapedPath = originalPath.replace(
45
- /[$()*+.?[\\\]^{|}]/gu,
46
- "\\$&",
44
+ for (const [path, hash] of fileHashes) {
45
+ const fullPath = prefix + path;
46
+ const escapedPath = fullPath.replace(
47
+ /[$()*+.?[\\\]^{|}]/g,
48
+ String.raw`\$&`,
47
49
  );
48
50
  const regex = new RegExp(
49
51
  `(?<path>${escapedPath})(\\?(?<queryString>[^#"'\`]*))?`,
50
- "gu",
52
+ "g",
51
53
  );
52
54
 
53
55
  content = content.replace(
@@ -75,6 +77,7 @@ const updateFilePathsWithHashes = async (
75
77
  const generateHashesAndReplace = async ({
76
78
  roots,
77
79
  refs,
80
+ prefix,
78
81
  includeDotFiles = false,
79
82
  skipPatterns = ["**/node_modules/**"],
80
83
  }) => {
@@ -116,6 +119,7 @@ const generateHashesAndReplace = async ({
116
119
  await updateFilePathsWithHashes(
117
120
  fileHashes,
118
121
  refs,
122
+ prefix,
119
123
  includeDotFiles,
120
124
  skipPatterns,
121
125
  );
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": "2.2.1",
6
+ "version": "3.0.0",
7
7
  "type": "module",
8
8
  "bin": "./bin/src/index.js",
9
9
  "main": "./src/index.js",
@@ -26,7 +26,7 @@
26
26
  "devDependencies": {
27
27
  "@fastify/pre-commit": "^2.1.0",
28
28
  "c8": "^10.1.2",
29
- "grules": "^0.17.3",
29
+ "grules": "^0.23.0",
30
30
  "tinybench": "^2.8.0"
31
31
  },
32
32
  "repository": {
package/src/html.js CHANGED
@@ -1,4 +1,4 @@
1
- const escapeRegExp = /["&'<>`]/;
1
+ const escapeRegExp = /["&'<=>]/;
2
2
 
3
3
  const escapeFunction = (string) => {
4
4
  let escaped = "";
@@ -22,18 +22,18 @@ const escapeFunction = (string) => {
22
22
  escaped += string.slice(start, end) + "&#60;";
23
23
  start = end + 1;
24
24
  continue;
25
- case 62: // >
26
- escaped += string.slice(start, end) + "&#62;";
25
+ case 61: // =
26
+ escaped += string.slice(start, end) + "&#61;";
27
27
  start = end + 1;
28
28
  continue;
29
- case 96: // `
30
- escaped += string.slice(start, end) + "&#96;";
29
+ case 62: // >
30
+ escaped += string.slice(start, end) + "&#62;";
31
31
  start = end + 1;
32
32
  continue;
33
33
  }
34
34
  }
35
35
 
36
- escaped += string.slice(start, string.length);
36
+ escaped += string.slice(start);
37
37
 
38
38
  return escaped;
39
39
  };
@@ -47,16 +47,15 @@ const html = ({ raw: literals }, ...expressions) => {
47
47
  let accumulator = "";
48
48
 
49
49
  for (let i = 0; i !== expressions.length; ++i) {
50
- const expression = expressions[i];
51
50
  let literal = literals[i];
52
51
  let string =
53
- typeof expression === "string"
54
- ? expression
55
- : expression == null
52
+ typeof expressions[i] === "string"
53
+ ? expressions[i]
54
+ : expressions[i] == null
56
55
  ? ""
57
- : Array.isArray(expression)
58
- ? expression.join("")
59
- : `${expression}`;
56
+ : Array.isArray(expressions[i])
57
+ ? expressions[i].join("")
58
+ : `${expressions[i]}`;
60
59
 
61
60
  if (literal && literal.charCodeAt(literal.length - 1) === 33) {
62
61
  literal = literal.slice(0, -1);
package/test/index.js CHANGED
@@ -66,7 +66,15 @@ test("renders unsafe content", () => {
66
66
  test("renders unsafe content /2", () => {
67
67
  assert.strictEqual(
68
68
  html`<p>${`${descriptionUnsafe}"&\``}</p>`,
69
- `<p>&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;&#34;&#38;&#96;</p>`,
69
+ `<p>&#60;script&#62;alert(&#39;This is an unsafe description.&#39;)&#60;/script&#62;&#34;&#38;\`</p>`,
70
+ );
71
+ });
72
+
73
+ test("renders unsafe content /3", () => {
74
+ // prettier-ignore
75
+ assert.strictEqual(
76
+ html`<img src="https://picsum.photos/200/300" alt=${"altText onload=alert(String.fromCharCode(112,119,110,101,100))"} />`,
77
+ `<img src="https://picsum.photos/200/300" alt=altText onload&#61;alert(String.fromCharCode(112,119,110,101,100)) />`,
70
78
  );
71
79
  });
72
80