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 +0 -1
- package/.github/workflows/benchmark.yml +93 -0
- package/.github/workflows/{ci.yml → npm-test.yml} +2 -2
- package/README.md +2 -4
- package/bench/index.js +101 -0
- package/package.json +3 -2
- package/src/html.js +9 -15
- package/test/index.js +23 -17
package/.eslintrc.json
CHANGED
|
@@ -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
|
+
});
|
package/README.md
CHANGED
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.
|
|
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.
|
|
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
|
-
|
|
28
|
-
let literal = literals
|
|
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
|
|
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
|
-
|
|
62
|
-
let literal = literals
|
|
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
|
|
124
|
-
yield literals
|
|
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/
|
|
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`${
|
|
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("
|
|
46
|
+
test("renders unsafe content", () => {
|
|
44
47
|
assert.strictEqual(
|
|
45
48
|
html`<p>${descriptionUnsafe}</p>`,
|
|
46
49
|
`<p><script>alert('This is an unsafe description.')</script></p>`,
|
|
@@ -54,14 +57,14 @@ test("renders arrays", () => {
|
|
|
54
57
|
);
|
|
55
58
|
});
|
|
56
59
|
|
|
57
|
-
test("
|
|
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
|
|
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}!${
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assert.strictEqual(
|
|
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
|
|
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,
|