ghtml 1.2.0 → 1.2.2

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,5 +1,4 @@
1
1
  {
2
2
  "root": true,
3
- "env": { "node": true },
4
3
  "extends": ["plugin:grules/all"]
5
4
  }
@@ -0,0 +1,93 @@
1
+ name: Benchmark Comparison
2
+
3
+ permissions:
4
+ contents: read
5
+ pull-requests: write
6
+
7
+ on:
8
+ pull_request:
9
+ branches:
10
+ - main
11
+
12
+ jobs:
13
+ benchmark-comparison:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ # Checkout PR code
17
+ - name: Checkout PR code
18
+ uses: actions/checkout@v4
19
+
20
+ # Setup Node.js
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: "20"
25
+
26
+ # Install dependencies
27
+ - name: Install dependencies
28
+ run: npm install
29
+
30
+ # Run benchmark on PR code
31
+ - name: Run benchmark on PR code
32
+ run: node bench/index.js
33
+ id: benchmark_pr
34
+
35
+ # Save PR benchmark results
36
+ - name: Save PR benchmark results
37
+ run: echo "PR_RESULTS=$(cat bench/results.json)" >> "$GITHUB_ENV"
38
+
39
+ # Prepare for main branch benchmark
40
+ - name: Backup PR src directory
41
+ run: mkdir _pr_branch && mv src _pr_branch/src
42
+
43
+ # Checkout main branch src directory
44
+ - name: Checkout main branch src directory
45
+ uses: actions/checkout@v4
46
+ with:
47
+ ref: "main"
48
+ path: "_main_branch"
49
+
50
+ # Replace PR src with main src
51
+ - name: Replace PR src with main src
52
+ run: |
53
+ rm -rf src
54
+ cp -R _main_branch/src src
55
+
56
+ # Run benchmark on main branch src with PR's benchmark tooling
57
+ - name: Run benchmark on main branch src
58
+ run: node bench/index.js
59
+ id: benchmark_main
60
+
61
+ # Save main benchmark results
62
+ - name: Save main benchmark results
63
+ run: echo "MAIN_RESULTS=$(cat bench/results.json)" >> "$GITHUB_ENV"
64
+
65
+ # Comment PR with benchmark results comparison
66
+ - name: Comment PR with benchmark results comparison
67
+ uses: actions/github-script@v7
68
+ with:
69
+ github-token: ${{secrets.GITHUB_TOKEN}}
70
+ script: |
71
+ const prResults = JSON.parse(Buffer.from(process.env.PR_RESULTS, 'base64').toString('utf8'));
72
+ const mainResults = JSON.parse(Buffer.from(process.env.MAIN_RESULTS, 'base64').toString('utf8'));
73
+
74
+ const commentBody = `
75
+ Benchmark Results Comparison (${context.sha}):
76
+
77
+ **PR Branch:**
78
+ \`\`\`json
79
+ ${JSON.stringify(prResults, null, 2)}
80
+ \`\`\`
81
+
82
+ **Main Branch:**
83
+ \`\`\`json
84
+ ${JSON.stringify(mainResults, null, 2)}
85
+ \`\`\`
86
+ `;
87
+
88
+ github.rest.issues.createComment({
89
+ issue_number: context.issue.number,
90
+ owner: context.repo.owner,
91
+ repo: context.repo.repo,
92
+ body: commentBody
93
+ });
@@ -1,4 +1,4 @@
1
- name: CI
1
+ name: npm-test
2
2
 
3
3
  on:
4
4
  push:
@@ -23,4 +23,4 @@ jobs:
23
23
  with:
24
24
  node-version: ${{ matrix.node-version }}
25
25
  - run: npm install
26
- - run: npm run test
26
+ - run: npm test
package/README.md CHANGED
@@ -61,10 +61,8 @@ const htmlString = html`
61
61
  ? html`
62
62
  <p>Data:</p>
63
63
  <ul>
64
- ${Object.values(data).map(
65
- ([key, val]) => `
66
- ${key}: ${val}
67
- `,
64
+ !${Object.values(data).map(
65
+ ([key, val]) => html`<li>${key}: ${val}</li>`,
68
66
  )}
69
67
  </ul>
70
68
  `
package/bench/index.js ADDED
@@ -0,0 +1,101 @@
1
+ /* eslint-disable no-unused-expressions */
2
+ import { html } from "../src/html.js";
3
+ import { Bench } from "tinybench";
4
+ import { writeFileSync } from "node:fs";
5
+ import { Buffer } from "node:buffer";
6
+
7
+ const bench = new Bench({ time: 500 });
8
+
9
+ bench.add("Simple formatting", () => {
10
+ html`<div>Hello, world!</div>`;
11
+ });
12
+
13
+ const username = "User";
14
+ bench.add("Using string variable", () => {
15
+ html`<p>${username}</p>`;
16
+ });
17
+
18
+ const value = null;
19
+ const undef = undefined;
20
+ bench.add("Handling null and undefined", () => {
21
+ html`<p>${value} and ${undef}</p>`;
22
+ });
23
+
24
+ const user = { id: 1, name: "John Doe" };
25
+ bench.add("Multiple types of expressions", () => {
26
+ html`
27
+ ${undefined}
28
+ <div>User: <span>${user.name}</span></div>
29
+ <div>Id: <span>${user.id}</span></div>
30
+ ${null}
31
+ `;
32
+ });
33
+
34
+ const items = ["Item 1", "Item 2", "Item 3"];
35
+ bench.add("Arrays and iteration", () => {
36
+ html`<ul>
37
+ ${items.map((item) => {
38
+ return html`<li>${item}</li>`;
39
+ })}
40
+ </ul>`;
41
+ });
42
+
43
+ const items2 = ["Item 1", undefined, "Item 2", null, 2000];
44
+ bench.add("Arrays and iteration with multiple types", () => {
45
+ html`<ul>
46
+ ${items2.map((item) => {
47
+ return html`<li>${item}</li>`;
48
+ })}
49
+ </ul>`;
50
+ });
51
+
52
+ const loggedIn = true;
53
+ bench.add("Complex/nested expressions", () => {
54
+ html`<nav>
55
+ ${loggedIn
56
+ ? html`<a href="/logout">Logout</a>`
57
+ : html`<a href="/login">Login</a>`}
58
+ </nav>`;
59
+ });
60
+
61
+ const largeString = Array.from({ length: 1000 }).join("Lorem ipsum ");
62
+ bench.add("Large strings", () => {
63
+ html`<p>${largeString}</p>`;
64
+ });
65
+
66
+ bench.add("High iteration count", () => {
67
+ for (let i = 0; i < 1000; i++) {
68
+ html`<span>${i}</span>`;
69
+ }
70
+ });
71
+
72
+ const scriptContent =
73
+ "<script>console.log('This should not execute');</script>";
74
+ bench.add("Escape HTML", () => {
75
+ html`<div>${scriptContent} ${scriptContent}</div>`;
76
+ });
77
+
78
+ // Render raw HTML
79
+ const rawHTML = "<em>Italic</em> and <strong>bold</strong>";
80
+ const markup = "<mark>Highlighted</mark>";
81
+ bench.add("Unescaped expressions", () => {
82
+ html`
83
+ <div>!${rawHTML}</div>
84
+ <div>!${rawHTML}</div>
85
+ <div>!${markup}</div>
86
+ <div>!${markup}</div>
87
+ <div>!${rawHTML}</div>
88
+ <div>!${rawHTML}</div>
89
+ `;
90
+ });
91
+
92
+ await bench.warmup();
93
+ await bench.run();
94
+
95
+ const table = bench.table();
96
+ console.table(table);
97
+
98
+ writeFileSync(
99
+ "bench/results.json",
100
+ Buffer.from(JSON.stringify(table), "utf8").toString("base64"),
101
+ );
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.2.0",
6
+ "version": "1.2.2",
7
7
  "type": "module",
8
8
  "main": "./src/index.js",
9
9
  "exports": {
@@ -20,7 +20,8 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@fastify/pre-commit": "^2.1.0",
23
- "grules": "^0.13.0"
23
+ "grules": "^0.15.0",
24
+ "tinybench": "^2.6.0"
24
25
  },
25
26
  "repository": {
26
27
  "type": "git",
package/src/html.js CHANGED
@@ -20,12 +20,12 @@ const escapeFunction = (key) => {
20
20
  * @param {...any} expressions Expressions to interpolate.
21
21
  * @returns {string} The HTML string.
22
22
  */
23
- const html = (literals, ...expressions) => {
23
+ const html = ({ raw: literals }, ...expressions) => {
24
24
  let accumulator = "";
25
25
  let index = 0;
26
26
 
27
- while (index < expressions.length) {
28
- let literal = literals.raw[index];
27
+ for (; index < expressions.length; ++index) {
28
+ let literal = literals[index];
29
29
  let expression =
30
30
  typeof expressions[index] === "string"
31
31
  ? expressions[index]
@@ -42,12 +42,9 @@ const html = (literals, ...expressions) => {
42
42
  }
43
43
 
44
44
  accumulator += literal + expression;
45
- ++index;
46
45
  }
47
46
 
48
- accumulator += literals.raw[index];
49
-
50
- return accumulator;
47
+ return (accumulator += literals[index]);
51
48
  };
52
49
 
53
50
  /**
@@ -55,11 +52,11 @@ const html = (literals, ...expressions) => {
55
52
  * @param {...any} expressions Expressions to interpolate.
56
53
  * @yields {string} The HTML strings.
57
54
  */
58
- const htmlGenerator = function* (literals, ...expressions) {
55
+ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
59
56
  let index = 0;
60
57
 
61
- while (index < expressions.length) {
62
- let literal = literals.raw[index];
58
+ for (; index < expressions.length; ++index) {
59
+ let literal = literals[index];
63
60
  let expression;
64
61
 
65
62
  if (typeof expressions[index] === "string") {
@@ -100,7 +97,6 @@ const htmlGenerator = function* (literals, ...expressions) {
100
97
  }
101
98
  }
102
99
 
103
- ++index;
104
100
  continue;
105
101
  }
106
102
 
@@ -116,12 +112,10 @@ const htmlGenerator = function* (literals, ...expressions) {
116
112
  if (literal.length || expression.length) {
117
113
  yield literal + expression;
118
114
  }
119
-
120
- ++index;
121
115
  }
122
116
 
123
- if (literals.raw[index].length) {
124
- yield literals.raw[index];
117
+ if (literals[index].length) {
118
+ yield literals[index];
125
119
  }
126
120
  };
127
121
 
package/test/index.js CHANGED
@@ -1,15 +1,14 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert";
3
- import { html, htmlGenerator } from "../src/index.js";
3
+ import { html, htmlGenerator } from "../src/html.js";
4
4
 
5
+ const conditionTrue = true;
6
+ const conditionFalse = false;
5
7
  const username = "Paul";
6
8
  const descriptionSafe = "This is a safe description.";
7
9
  const descriptionUnsafe =
8
10
  "<script>alert('This is an unsafe description.')</script>";
9
11
  const array1 = [1, 2, 3, 4, 5];
10
- const conditionTrue = true;
11
- const conditionFalse = false;
12
- const emptyString = "";
13
12
 
14
13
  const generatorExample = function* () {
15
14
  yield "<p>";
@@ -26,13 +25,17 @@ test("renders empty input", () => {
26
25
  });
27
26
 
28
27
  test("renders empty input", () => {
29
- assert.strictEqual(html`${emptyString}`, "");
28
+ assert.strictEqual(html`${""}`, "");
30
29
  });
31
30
 
32
31
  test("renders normal input", () => {
33
32
  assert.strictEqual(html`Hey, ${username}!`, `Hey, ${username}!`);
34
33
  });
35
34
 
35
+ test("renders undefined and null as empty string", () => {
36
+ assert.strictEqual(html`<p>${null}${undefined}</p>`, "<p></p>");
37
+ });
38
+
36
39
  test("renders safe content", () => {
37
40
  assert.strictEqual(
38
41
  html`<p>${descriptionSafe}</p>`,
@@ -40,7 +43,7 @@ test("renders safe content", () => {
40
43
  );
41
44
  });
42
45
 
43
- test("escapes unsafe content", () => {
46
+ test("renders unsafe content", () => {
44
47
  assert.strictEqual(
45
48
  html`<p>${descriptionUnsafe}</p>`,
46
49
  `<p>&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</p>`,
@@ -54,14 +57,14 @@ test("renders arrays", () => {
54
57
  );
55
58
  });
56
59
 
57
- test("bypass escaping", () => {
60
+ test("bypasses escaping", () => {
58
61
  assert.strictEqual(
59
62
  html`<p>!${[descriptionSafe, descriptionUnsafe]}</p>`,
60
63
  "<p>This is a safe description.<script>alert('This is an unsafe description.')</script></p>",
61
64
  );
62
65
  });
63
66
 
64
- test("renders wrapped html calls", () => {
67
+ test("renders nested html calls", () => {
65
68
  // prettier-ignore
66
69
  assert.strictEqual(
67
70
  html`<p>!${conditionTrue ? html`<strong>${descriptionUnsafe}</strong>` : ""}</p>`,
@@ -117,17 +120,20 @@ test("renders multiple html calls with different expression types", () => {
117
120
  });
118
121
 
119
122
  test("htmlGenerator renders safe content", () => {
120
- const generator = htmlGenerator`<p>${descriptionSafe}!${descriptionSafe}G!${htmlGenerator`${array1}`}!${null}${255}</p>`;
121
- assert.strictEqual(generator.next().value, "<p>This is a safe description.");
122
- assert.strictEqual(generator.next().value, "This is a safe description.");
123
- assert.strictEqual(generator.next().value, "G");
124
- assert.strictEqual(generator.next().value, "12345");
125
- assert.strictEqual(generator.next().value, "255");
126
- assert.strictEqual(generator.next().value, "</p>");
127
- assert.strictEqual(generator.next().done, true);
123
+ const generator = htmlGenerator`<p>${descriptionSafe}!${descriptionUnsafe}G!${htmlGenerator`${array1}`}!${null}${255}</p>`;
124
+ let accumulator = "";
125
+
126
+ for (const value of generator) {
127
+ accumulator += value;
128
+ }
129
+
130
+ assert.strictEqual(
131
+ accumulator,
132
+ "<p>This is a safe description.<script>alert('This is an unsafe description.')</script>G12345255</p>",
133
+ );
128
134
  });
129
135
 
130
- test("htmlGenerator escapes unsafe content", () => {
136
+ test("htmlGenerator renders unsafe content", () => {
131
137
  const generator = htmlGenerator`<p>${descriptionUnsafe}${descriptionUnsafe}${htmlGenerator`${array1}`}${null}${255}</p>`;
132
138
  assert.strictEqual(
133
139
  generator.next().value,