ghtml 1.2.5 → 1.5.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/.eslintrc.json +4 -1
- package/.github/workflows/benchmark.yml +21 -14
- package/.github/workflows/npm-test.yml +1 -1
- package/README.md +45 -6
- package/bench/index.js +33 -50
- package/package.json +4 -3
- package/src/html.js +150 -44
- package/src/includeFile.js +4 -6
- package/src/index.js +1 -1
- package/test/index.js +179 -18
- package/test/test.md +1 -0
- package/test.js +27 -0
package/.eslintrc.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: benchmark
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request_target:
|
|
5
|
+
types: [labeled]
|
|
2
6
|
|
|
3
7
|
permissions:
|
|
4
8
|
contents: read
|
|
5
9
|
pull-requests: write
|
|
6
10
|
|
|
7
|
-
on:
|
|
8
|
-
pull_request:
|
|
9
|
-
branches:
|
|
10
|
-
- main
|
|
11
|
-
|
|
12
11
|
jobs:
|
|
13
|
-
benchmark
|
|
12
|
+
benchmark:
|
|
13
|
+
if: ${{ github.event.label.name == 'benchmark' }}
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
15
|
steps:
|
|
16
16
|
# Checkout PR code
|
|
@@ -25,11 +25,11 @@ jobs:
|
|
|
25
25
|
|
|
26
26
|
# Install dependencies
|
|
27
27
|
- name: Install dependencies
|
|
28
|
-
run: npm install
|
|
28
|
+
run: npm install --ignore-scripts
|
|
29
29
|
|
|
30
30
|
# Run benchmark on PR code
|
|
31
31
|
- name: Run benchmark on PR code
|
|
32
|
-
run:
|
|
32
|
+
run: npm run benchmark
|
|
33
33
|
id: benchmark_pr
|
|
34
34
|
|
|
35
35
|
# Save PR benchmark results
|
|
@@ -55,7 +55,7 @@ jobs:
|
|
|
55
55
|
|
|
56
56
|
# Run benchmark on main branch src with PR's benchmark tooling
|
|
57
57
|
- name: Run benchmark on main branch src
|
|
58
|
-
run:
|
|
58
|
+
run: npm run benchmark
|
|
59
59
|
id: benchmark_main
|
|
60
60
|
|
|
61
61
|
# Save main benchmark results
|
|
@@ -70,24 +70,31 @@ jobs:
|
|
|
70
70
|
script: |
|
|
71
71
|
const prResults = JSON.parse(Buffer.from(process.env.PR_RESULTS, 'base64').toString('utf8'));
|
|
72
72
|
const mainResults = JSON.parse(Buffer.from(process.env.MAIN_RESULTS, 'base64').toString('utf8'));
|
|
73
|
-
|
|
74
73
|
const commentBody = `
|
|
75
74
|
Benchmark Results Comparison (${context.sha}):
|
|
76
|
-
|
|
77
75
|
**PR Branch:**
|
|
78
76
|
\`\`\`json
|
|
79
77
|
${JSON.stringify(prResults, null, 2)}
|
|
80
78
|
\`\`\`
|
|
81
|
-
|
|
82
79
|
**Main Branch:**
|
|
83
80
|
\`\`\`json
|
|
84
81
|
${JSON.stringify(mainResults, null, 2)}
|
|
85
82
|
\`\`\`
|
|
86
83
|
`;
|
|
87
|
-
|
|
88
84
|
github.rest.issues.createComment({
|
|
89
85
|
issue_number: context.issue.number,
|
|
90
86
|
owner: context.repo.owner,
|
|
91
87
|
repo: context.repo.repo,
|
|
92
88
|
body: commentBody
|
|
93
89
|
});
|
|
90
|
+
|
|
91
|
+
remove-label:
|
|
92
|
+
needs: benchmark
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
steps:
|
|
95
|
+
- name: Remove benchmark label
|
|
96
|
+
uses: octokit/request-action@v2.x
|
|
97
|
+
with:
|
|
98
|
+
route: DELETE /repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/benchmark
|
|
99
|
+
env:
|
|
100
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
package/README.md
CHANGED
|
@@ -18,11 +18,13 @@ The `html` function is designed to tag template literals and automatically escap
|
|
|
18
18
|
|
|
19
19
|
The `htmlGenerator` function acts as the generator version of the `html` function. It facilitates the creation of HTML fragments iteratively, making it ideal for parsing large templates or constructing HTML content dynamically.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### `htmlAsyncGenerator`
|
|
22
|
+
|
|
23
|
+
This version of HTML generator should be preferred for asynchronous use cases. The output will be generated as the promise expressions resolve.
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
**Note:**
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
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>`)}``.
|
|
26
28
|
|
|
27
29
|
### `includeFile`
|
|
28
30
|
|
|
@@ -82,12 +84,49 @@ const htmlString = html`
|
|
|
82
84
|
```js
|
|
83
85
|
import { htmlGenerator as html } from "ghtml";
|
|
84
86
|
import { Readable } from "node:stream";
|
|
87
|
+
import http from "node:http";
|
|
88
|
+
|
|
89
|
+
const generator = function* () {
|
|
90
|
+
yield "Hello, World!";
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
http
|
|
94
|
+
.createServer((req, res) => {
|
|
95
|
+
const htmlContent = html`<html>
|
|
96
|
+
<p>${generator()}</p>
|
|
97
|
+
</html>`;
|
|
98
|
+
const readableStream = Readable.from(htmlContent);
|
|
99
|
+
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
100
|
+
readableStream.pipe(res);
|
|
101
|
+
})
|
|
102
|
+
.listen(3000);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `htmlAsyncGenerator`
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import { htmlAsyncGenerator as html } from "ghtml";
|
|
109
|
+
import { createReadStream } from "node:fs";
|
|
110
|
+
import { readFile } from "node:fs/promises";
|
|
111
|
+
import { Readable } from "node:stream";
|
|
112
|
+
import http from "node:http";
|
|
113
|
+
|
|
114
|
+
const asyncGenerator = async function* () {
|
|
115
|
+
const Hello = await new Promise((resolve) => {
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
resolve("Hello");
|
|
118
|
+
}, 1000);
|
|
119
|
+
});
|
|
120
|
+
yield `${Hello}!`;
|
|
121
|
+
};
|
|
85
122
|
|
|
86
123
|
http
|
|
87
124
|
.createServer((req, res) => {
|
|
88
|
-
const htmlContent =
|
|
89
|
-
|
|
90
|
-
|
|
125
|
+
const htmlContent = html`<html>
|
|
126
|
+
<p>${asyncGenerator()}</p>
|
|
127
|
+
<code>${readFile("./README.md", "utf8")}</code>
|
|
128
|
+
<code>${createReadStream("./README.md", "utf8")}</code>
|
|
129
|
+
</html>`;
|
|
91
130
|
const readableStream = Readable.from(htmlContent);
|
|
92
131
|
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
93
132
|
readableStream.pipe(res);
|
package/bench/index.js
CHANGED
|
@@ -1,84 +1,67 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
1
2
|
/* eslint-disable no-unused-expressions */
|
|
2
3
|
import { html } from "../src/index.js";
|
|
3
4
|
import { Bench } from "tinybench";
|
|
4
5
|
import { writeFileSync } from "node:fs";
|
|
5
6
|
import { Buffer } from "node:buffer";
|
|
6
7
|
|
|
8
|
+
let result = "";
|
|
7
9
|
const bench = new Bench({ time: 500 });
|
|
8
10
|
|
|
9
|
-
bench.add("
|
|
10
|
-
html`<div>Hello, world!</div>`;
|
|
11
|
+
bench.add("simple HTML formatting", () => {
|
|
12
|
+
result = html`<div>Hello, world!</div>`;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
bench.add("null and undefined expressions", () => {
|
|
16
|
+
result = html`<p>${null} and ${undefined}</p>`;
|
|
11
17
|
});
|
|
12
18
|
|
|
13
19
|
const username = "User";
|
|
14
|
-
bench.add("
|
|
15
|
-
html`<p>${username}</p>`;
|
|
20
|
+
bench.add("string expressions", () => {
|
|
21
|
+
result = html`<p>${username} and ${username}</p>`;
|
|
16
22
|
});
|
|
17
23
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const items1 = ["Item 1", undefined, "Item 2", null, 2000, 1500.5];
|
|
25
|
+
bench.add("array expressions", () => {
|
|
26
|
+
result = html`<ul>
|
|
27
|
+
${items1.map((item) => {
|
|
28
|
+
return html`<li>${item}</li>`;
|
|
29
|
+
})}
|
|
30
|
+
</ul>`;
|
|
22
31
|
});
|
|
23
32
|
|
|
24
33
|
const user = { id: 1, name: "John Doe" };
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
const items2 = ["Item 1", "Item 2", "Item 3"];
|
|
35
|
+
bench.add("multiple types of expressions", () => {
|
|
36
|
+
result = html`
|
|
27
37
|
${undefined}
|
|
28
38
|
<div>User: <span>${user.name}</span></div>
|
|
29
39
|
<div>Id: <span>${user.id}</span></div>
|
|
30
|
-
${null}
|
|
40
|
+
${null}${123}${456n}
|
|
41
|
+
<ul>
|
|
42
|
+
!${items2.map((item) => {
|
|
43
|
+
return html`<li>${item}</li>`;
|
|
44
|
+
})}
|
|
45
|
+
</ul>
|
|
31
46
|
`;
|
|
32
47
|
});
|
|
33
48
|
|
|
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
49
|
const largeString = Array.from({ length: 1000 }).join("Lorem ipsum ");
|
|
62
|
-
bench.add("
|
|
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
|
-
}
|
|
50
|
+
bench.add("large strings", () => {
|
|
51
|
+
result = html`<p>${largeString}${largeString}</p>`;
|
|
70
52
|
});
|
|
71
53
|
|
|
72
54
|
const scriptContent =
|
|
73
55
|
"<script>console.log('This should not execute');</script>";
|
|
74
|
-
bench.add("
|
|
75
|
-
|
|
56
|
+
bench.add("high iteration count", () => {
|
|
57
|
+
for (let i = 0; i !== 100; i++) {
|
|
58
|
+
result = html`<span>${i}: ${scriptContent}</span>`;
|
|
59
|
+
}
|
|
76
60
|
});
|
|
77
61
|
|
|
78
|
-
// Render raw HTML
|
|
79
62
|
const rawHTML = "<em>Italic</em> and <strong>bold</strong>";
|
|
80
63
|
const markup = "<mark>Highlighted</mark>";
|
|
81
|
-
bench.add("
|
|
64
|
+
bench.add("unescaped expressions", () => {
|
|
82
65
|
html`
|
|
83
66
|
<div>!${rawHTML}</div>
|
|
84
67
|
<div>!${rawHTML}</div>
|
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
|
+
"version": "1.5.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.js",
|
|
9
9
|
"exports": {
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
"node": ">=18"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"
|
|
17
|
+
"benchmark": "node bench/index.js",
|
|
18
|
+
"test": "npm run lint && c8 --100 node --test test/*.js",
|
|
18
19
|
"lint": "eslint . && prettier --check .",
|
|
19
20
|
"lint:fix": "eslint --fix . && prettier --write ."
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@fastify/pre-commit": "^2.1.0",
|
|
23
24
|
"c8": "^9.1.0",
|
|
24
|
-
"grules": "^0.
|
|
25
|
+
"grules": "^0.16.2",
|
|
25
26
|
"tinybench": "^2.6.0"
|
|
26
27
|
},
|
|
27
28
|
"repository": {
|
package/src/html.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const escapeDict = {
|
|
2
2
|
'"': """,
|
|
3
3
|
"'": "'",
|
|
4
4
|
"&": "&",
|
|
@@ -6,13 +6,10 @@ const escapeDictionary = {
|
|
|
6
6
|
">": ">",
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
`[${Object.keys(escapeDictionary).join("")}]`,
|
|
11
|
-
"gu",
|
|
12
|
-
);
|
|
9
|
+
const escapeRE = new RegExp(`[${Object.keys(escapeDict).join("")}]`, "gu");
|
|
13
10
|
|
|
14
|
-
const
|
|
15
|
-
return
|
|
11
|
+
const escapeFn = (key) => {
|
|
12
|
+
return escapeDict[key];
|
|
16
13
|
};
|
|
17
14
|
|
|
18
15
|
/**
|
|
@@ -26,22 +23,22 @@ const html = ({ raw: literals }, ...expressions) => {
|
|
|
26
23
|
|
|
27
24
|
for (; index !== expressions.length; ++index) {
|
|
28
25
|
let literal = literals[index];
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
?
|
|
32
|
-
:
|
|
33
|
-
?
|
|
26
|
+
let string =
|
|
27
|
+
expressions[index] === undefined || expressions[index] === null
|
|
28
|
+
? ""
|
|
29
|
+
: typeof expressions[index] === "string"
|
|
30
|
+
? expressions[index]
|
|
34
31
|
: Array.isArray(expressions[index])
|
|
35
32
|
? expressions[index].join("")
|
|
36
33
|
: `${expressions[index]}`;
|
|
37
34
|
|
|
38
35
|
if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
|
|
39
36
|
literal = literal.slice(0, -1);
|
|
40
|
-
} else if (
|
|
41
|
-
|
|
37
|
+
} else if (string.length) {
|
|
38
|
+
string = string.replace(escapeRE, escapeFn);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
accumulator += literal +
|
|
41
|
+
accumulator += literal + string;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
return (accumulator += literals[index]);
|
|
@@ -57,17 +54,15 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
57
54
|
|
|
58
55
|
for (; index !== expressions.length; ++index) {
|
|
59
56
|
let literal = literals[index];
|
|
60
|
-
let expression;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
) {
|
|
68
|
-
expression = "";
|
|
57
|
+
let expression = expressions[index];
|
|
58
|
+
let string;
|
|
59
|
+
|
|
60
|
+
if (expression === undefined || expression === null) {
|
|
61
|
+
string = "";
|
|
62
|
+
} else if (typeof expression === "string") {
|
|
63
|
+
string = expression;
|
|
69
64
|
} else {
|
|
70
|
-
if (
|
|
65
|
+
if (expression[Symbol.iterator]) {
|
|
71
66
|
const isRaw =
|
|
72
67
|
literal.length !== 0 && literal.charCodeAt(literal.length - 1) === 33;
|
|
73
68
|
|
|
@@ -79,44 +74,155 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
79
74
|
yield literal;
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
for (
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
for (expression of expression) {
|
|
78
|
+
if (expression === undefined || expression === null) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof expression === "string") {
|
|
83
|
+
string = expression;
|
|
84
|
+
} else {
|
|
85
|
+
if (expression[Symbol.iterator]) {
|
|
86
|
+
for (expression of expression) {
|
|
87
|
+
if (expression === undefined || expression === null) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
string = `${expression}`;
|
|
92
|
+
|
|
93
|
+
if (string.length) {
|
|
94
|
+
if (!isRaw) {
|
|
95
|
+
string = string.replace(escapeRE, escapeFn);
|
|
96
|
+
}
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
yield string;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
continue;
|
|
92
103
|
}
|
|
104
|
+
|
|
105
|
+
string = `${expression}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (string.length) {
|
|
109
|
+
if (!isRaw) {
|
|
110
|
+
string = string.replace(escapeRE, escapeFn);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
yield string;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
string = `${expression}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
|
|
124
|
+
literal = literal.slice(0, -1);
|
|
125
|
+
} else if (string.length) {
|
|
126
|
+
string = string.replace(escapeRE, escapeFn);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (literal.length || string.length) {
|
|
130
|
+
yield literal + string;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (literals[index].length) {
|
|
135
|
+
yield literals[index];
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {{ raw: string[] }} literals Tagged template literals.
|
|
141
|
+
* @param {...any} expressions Expressions to interpolate.
|
|
142
|
+
* @yields {string} The HTML strings.
|
|
143
|
+
*/
|
|
144
|
+
const htmlAsyncGenerator = async function* ({ raw: literals }, ...expressions) {
|
|
145
|
+
let index = 0;
|
|
146
|
+
|
|
147
|
+
for (; index !== expressions.length; ++index) {
|
|
148
|
+
let literal = literals[index];
|
|
149
|
+
let expression = await expressions[index];
|
|
150
|
+
let string;
|
|
151
|
+
|
|
152
|
+
if (expression === undefined || expression === null) {
|
|
153
|
+
string = "";
|
|
154
|
+
} else if (typeof expression === "string") {
|
|
155
|
+
string = expression;
|
|
156
|
+
} else {
|
|
157
|
+
if (expression[Symbol.iterator] || expression[Symbol.asyncIterator]) {
|
|
158
|
+
const isRaw =
|
|
159
|
+
literal.length !== 0 && literal.charCodeAt(literal.length - 1) === 33;
|
|
160
|
+
|
|
161
|
+
if (isRaw) {
|
|
162
|
+
literal = literal.slice(0, -1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (literal.length) {
|
|
166
|
+
yield literal;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for await (expression of expression) {
|
|
170
|
+
if (expression === undefined || expression === null) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof expression === "string") {
|
|
175
|
+
string = expression;
|
|
93
176
|
} else {
|
|
94
|
-
|
|
177
|
+
if (
|
|
178
|
+
expression[Symbol.iterator] ||
|
|
179
|
+
expression[Symbol.asyncIterator]
|
|
180
|
+
) {
|
|
181
|
+
for await (expression of expression) {
|
|
182
|
+
if (expression === undefined || expression === null) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
string = `${expression}`;
|
|
187
|
+
|
|
188
|
+
if (string.length) {
|
|
189
|
+
if (!isRaw) {
|
|
190
|
+
string = string.replace(escapeRE, escapeFn);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
yield string;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
string = `${expression}`;
|
|
95
201
|
}
|
|
96
202
|
|
|
97
|
-
if (
|
|
203
|
+
if (string.length) {
|
|
98
204
|
if (!isRaw) {
|
|
99
|
-
|
|
205
|
+
string = string.replace(escapeRE, escapeFn);
|
|
100
206
|
}
|
|
101
207
|
|
|
102
|
-
yield
|
|
208
|
+
yield string;
|
|
103
209
|
}
|
|
104
210
|
}
|
|
105
211
|
|
|
106
212
|
continue;
|
|
107
213
|
}
|
|
108
214
|
|
|
109
|
-
|
|
215
|
+
string = `${expression}`;
|
|
110
216
|
}
|
|
111
217
|
|
|
112
218
|
if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
|
|
113
219
|
literal = literal.slice(0, -1);
|
|
114
|
-
} else if (
|
|
115
|
-
|
|
220
|
+
} else if (string.length) {
|
|
221
|
+
string = string.replace(escapeRE, escapeFn);
|
|
116
222
|
}
|
|
117
223
|
|
|
118
|
-
if (literal.length ||
|
|
119
|
-
yield literal +
|
|
224
|
+
if (literal.length || string.length) {
|
|
225
|
+
yield literal + string;
|
|
120
226
|
}
|
|
121
227
|
}
|
|
122
228
|
|
|
@@ -125,4 +231,4 @@ const htmlGenerator = function* ({ raw: literals }, ...expressions) {
|
|
|
125
231
|
}
|
|
126
232
|
};
|
|
127
233
|
|
|
128
|
-
export { html, htmlGenerator };
|
|
234
|
+
export { html, htmlGenerator, htmlAsyncGenerator };
|
package/src/includeFile.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const fileCache = new Map();
|
|
3
|
+
const cache = new Map();
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* @param {string} path The path to the file to render.
|
|
9
7
|
* @returns {string} The cached content of the file.
|
|
10
8
|
*/
|
|
11
9
|
const includeFile = (path) => {
|
|
12
|
-
let file =
|
|
10
|
+
let file = cache.get(path);
|
|
13
11
|
|
|
14
12
|
if (file === undefined) {
|
|
15
|
-
file = readFileSync(path,
|
|
16
|
-
|
|
13
|
+
file = readFileSync(path, "utf8");
|
|
14
|
+
cache.set(path, file);
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
return file;
|
package/src/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { html, htmlGenerator } from "./html.js";
|
|
1
|
+
export { html, htmlGenerator, htmlAsyncGenerator } from "./html.js";
|
package/test/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { html, htmlGenerator } from "../src/index.js";
|
|
1
|
+
import { html, htmlGenerator, htmlAsyncGenerator } from "../src/index.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
2
4
|
import test from "node:test";
|
|
3
5
|
import assert from "node:assert";
|
|
4
6
|
|
|
@@ -20,6 +22,17 @@ const generatorExample = function* () {
|
|
|
20
22
|
yield "</p>";
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
const generatorPromiseExample = function* () {
|
|
26
|
+
yield [
|
|
27
|
+
new Promise((resolve) => {
|
|
28
|
+
resolve("<p>");
|
|
29
|
+
}),
|
|
30
|
+
null,
|
|
31
|
+
12n,
|
|
32
|
+
];
|
|
33
|
+
yield;
|
|
34
|
+
};
|
|
35
|
+
|
|
23
36
|
test("renders empty input", () => {
|
|
24
37
|
assert.strictEqual(html({ raw: [""] }), "");
|
|
25
38
|
});
|
|
@@ -163,38 +176,186 @@ test("htmlGenerator works with nested htmlGenerator calls in an array", () => {
|
|
|
163
176
|
|
|
164
177
|
test("htmlGenerator works with other generators", () => {
|
|
165
178
|
const generator = htmlGenerator`<div>!${generatorExample()}</div>`;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
let accumulator = "";
|
|
180
|
+
|
|
181
|
+
for (const value of generator) {
|
|
182
|
+
accumulator += value;
|
|
183
|
+
}
|
|
184
|
+
|
|
169
185
|
assert.strictEqual(
|
|
170
|
-
|
|
171
|
-
"<script>alert('This is an unsafe description.')</script>",
|
|
186
|
+
accumulator,
|
|
187
|
+
"<div><p>This is a safe description.<script>alert('This is an unsafe description.')</script>12345255</p></div>",
|
|
172
188
|
);
|
|
173
|
-
assert.strictEqual(generator.next().value, "12345");
|
|
174
|
-
assert.strictEqual(generator.next().value, "255");
|
|
175
|
-
assert.strictEqual(generator.next().value, "</p>");
|
|
176
|
-
assert.strictEqual(generator.next().value, "</div>");
|
|
177
189
|
assert.strictEqual(generator.next().done, true);
|
|
178
190
|
});
|
|
179
191
|
|
|
180
192
|
test("htmlGenerator works with other generators within an array (raw)", () => {
|
|
181
193
|
const generator = htmlGenerator`<div>!${[generatorExample()]}</div>`;
|
|
182
|
-
|
|
194
|
+
let accumulator = "";
|
|
195
|
+
|
|
196
|
+
for (const value of generator) {
|
|
197
|
+
accumulator += value;
|
|
198
|
+
}
|
|
199
|
+
|
|
183
200
|
assert.strictEqual(
|
|
184
|
-
|
|
185
|
-
"<p>This is a safe description.<script>alert('This is an unsafe description.')</script>1,2,3,4,5255</p>",
|
|
201
|
+
accumulator,
|
|
202
|
+
"<div><p>This is a safe description.<script>alert('This is an unsafe description.')</script>1,2,3,4,5255</p></div>",
|
|
186
203
|
);
|
|
187
|
-
assert.strictEqual(generator.next().value, "</div>");
|
|
188
204
|
assert.strictEqual(generator.next().done, true);
|
|
189
205
|
});
|
|
190
206
|
|
|
191
207
|
test("htmlGenerator works with other generators within an array (escaped)", () => {
|
|
192
208
|
const generator = htmlGenerator`<div>${[generatorExample()]}</div>`;
|
|
193
|
-
|
|
209
|
+
let accumulator = "";
|
|
210
|
+
|
|
211
|
+
for (const value of generator) {
|
|
212
|
+
accumulator += value;
|
|
213
|
+
}
|
|
214
|
+
|
|
194
215
|
assert.strictEqual(
|
|
195
|
-
|
|
196
|
-
"
|
|
216
|
+
accumulator,
|
|
217
|
+
"<div><p>This is a safe description.<script>alert('This is an unsafe description.')</script>1,2,3,4,5255</p></div>",
|
|
197
218
|
);
|
|
198
|
-
assert.strictEqual(generator.next().value, "</div>");
|
|
199
219
|
assert.strictEqual(generator.next().done, true);
|
|
200
220
|
});
|
|
221
|
+
|
|
222
|
+
test("htmlAsyncGenerator renders safe content", async () => {
|
|
223
|
+
const generator = htmlAsyncGenerator`<p>${descriptionSafe}!${descriptionUnsafe}G!${htmlAsyncGenerator`${array1}`}!${null}${255}</p>`;
|
|
224
|
+
let accumulator = "";
|
|
225
|
+
|
|
226
|
+
for await (const value of generator) {
|
|
227
|
+
accumulator += value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
assert.strictEqual(
|
|
231
|
+
accumulator,
|
|
232
|
+
"<p>This is a safe description.<script>alert('This is an unsafe description.')</script>G12345255</p>",
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("htmlAsyncGenerator renders unsafe content", async () => {
|
|
237
|
+
const generator = htmlAsyncGenerator`<p>${descriptionSafe}${descriptionUnsafe}${htmlAsyncGenerator`${array1}`}${null}${255}</p>`;
|
|
238
|
+
let accumulator = "";
|
|
239
|
+
|
|
240
|
+
for await (const value of generator) {
|
|
241
|
+
accumulator += value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
assert.strictEqual(
|
|
245
|
+
accumulator,
|
|
246
|
+
"<p>This is a safe description.<script>alert('This is an unsafe description.')</script>12345255</p>",
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("htmlAsyncGenerator works with nested htmlAsyncGenerator calls in an array", async () => {
|
|
251
|
+
const generator = htmlAsyncGenerator`!${[1, 2, 3].map((i) => {
|
|
252
|
+
return htmlAsyncGenerator`${i}: <p>${readFile("test/test.md", "utf8")}</p>`;
|
|
253
|
+
})}`;
|
|
254
|
+
let accumulator = "";
|
|
255
|
+
|
|
256
|
+
for await (const value of generator) {
|
|
257
|
+
accumulator += value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
assert.strictEqual(
|
|
261
|
+
accumulator.replaceAll("\n", "").trim(),
|
|
262
|
+
"1: <p># test.md></p>2: <p># test.md></p>3: <p># test.md></p>",
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("htmlAsyncGenerator renders chunks with promises (escaped)", async () => {
|
|
267
|
+
const generator = htmlAsyncGenerator`<ul>!${[1, 2].map((i) => {
|
|
268
|
+
return htmlAsyncGenerator`${i}: ${readFile("test/test.md", "utf8")}`;
|
|
269
|
+
})}</ul>`;
|
|
270
|
+
const fileContent = readFileSync("test/test.md", "utf8").replaceAll(
|
|
271
|
+
">",
|
|
272
|
+
">",
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
let value = await generator.next();
|
|
276
|
+
assert.strictEqual(value.value, "<ul>");
|
|
277
|
+
|
|
278
|
+
value = await generator.next();
|
|
279
|
+
assert.strictEqual(value.value, `1`);
|
|
280
|
+
|
|
281
|
+
value = await generator.next();
|
|
282
|
+
assert.strictEqual(value.value, `: ${fileContent}`);
|
|
283
|
+
|
|
284
|
+
value = await generator.next();
|
|
285
|
+
assert.strictEqual(value.value, `2`);
|
|
286
|
+
|
|
287
|
+
value = await generator.next();
|
|
288
|
+
assert.strictEqual(value.value, `: ${fileContent}`);
|
|
289
|
+
|
|
290
|
+
value = await generator.next();
|
|
291
|
+
assert.strictEqual(value.value, "</ul>");
|
|
292
|
+
|
|
293
|
+
value = await generator.next();
|
|
294
|
+
assert.strictEqual(value.done, true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("htmlAsyncGenerator renders chunks with promises (raw)", async () => {
|
|
298
|
+
const generator = htmlAsyncGenerator`<ul>!${[1, 2].map((i) => {
|
|
299
|
+
return htmlAsyncGenerator`${i}: !${readFile("test/test.md", "utf8")}`;
|
|
300
|
+
})}</ul>`;
|
|
301
|
+
const fileContent = readFileSync("test/test.md", "utf8");
|
|
302
|
+
|
|
303
|
+
let value = await generator.next();
|
|
304
|
+
assert.strictEqual(value.value, "<ul>");
|
|
305
|
+
|
|
306
|
+
value = await generator.next();
|
|
307
|
+
assert.strictEqual(value.value, `1`);
|
|
308
|
+
|
|
309
|
+
value = await generator.next();
|
|
310
|
+
assert.strictEqual(value.value, `: ${fileContent}`);
|
|
311
|
+
|
|
312
|
+
value = await generator.next();
|
|
313
|
+
assert.strictEqual(value.value, `2`);
|
|
314
|
+
|
|
315
|
+
value = await generator.next();
|
|
316
|
+
assert.strictEqual(value.value, `: ${fileContent}`);
|
|
317
|
+
|
|
318
|
+
value = await generator.next();
|
|
319
|
+
assert.strictEqual(value.value, "</ul>");
|
|
320
|
+
|
|
321
|
+
value = await generator.next();
|
|
322
|
+
assert.strictEqual(value.done, true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("htmlAsyncGenerator redners in chuncks", async () => {
|
|
326
|
+
const generator = htmlAsyncGenerator`<ul>${generatorPromiseExample()}</ul>`;
|
|
327
|
+
|
|
328
|
+
let value = await generator.next();
|
|
329
|
+
assert.strictEqual(value.value, "<ul>");
|
|
330
|
+
|
|
331
|
+
value = await generator.next();
|
|
332
|
+
assert.strictEqual(value.value, "<p>");
|
|
333
|
+
|
|
334
|
+
value = await generator.next();
|
|
335
|
+
assert.strictEqual(value.value, "12");
|
|
336
|
+
|
|
337
|
+
value = await generator.next();
|
|
338
|
+
assert.strictEqual(value.value, "</ul>");
|
|
339
|
+
|
|
340
|
+
value = await generator.next();
|
|
341
|
+
assert.strictEqual(value.done, true);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("htmlAsyncGenerator redners in chuncks (raw)", async () => {
|
|
345
|
+
const generator = htmlAsyncGenerator`<ul>!${generatorPromiseExample()}</ul>`;
|
|
346
|
+
|
|
347
|
+
let value = await generator.next();
|
|
348
|
+
assert.strictEqual(value.value, "<ul>");
|
|
349
|
+
|
|
350
|
+
value = await generator.next();
|
|
351
|
+
assert.strictEqual(value.value, "<p>");
|
|
352
|
+
|
|
353
|
+
value = await generator.next();
|
|
354
|
+
assert.strictEqual(value.value, "12");
|
|
355
|
+
|
|
356
|
+
value = await generator.next();
|
|
357
|
+
assert.strictEqual(value.value, "</ul>");
|
|
358
|
+
|
|
359
|
+
value = await generator.next();
|
|
360
|
+
assert.strictEqual(value.done, true);
|
|
361
|
+
});
|
package/test/test.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# test.md>
|
package/test.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { htmlAsyncGenerator as html } from "ghtml";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import http from "node:http";
|
|
6
|
+
|
|
7
|
+
const asyncGenerator = async function* () {
|
|
8
|
+
const Hello = await new Promise((resolve) => {
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
resolve("Hello");
|
|
11
|
+
}, 1000);
|
|
12
|
+
});
|
|
13
|
+
yield `${Hello}!`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
http
|
|
17
|
+
.createServer((req, res) => {
|
|
18
|
+
const htmlContent = html`<html>
|
|
19
|
+
<p>${asyncGenerator()}</p>
|
|
20
|
+
<code>${readFile("./README.md", "utf8")}</code>
|
|
21
|
+
<code>${createReadStream("./README.md", "utf8")}</code>
|
|
22
|
+
</html>`;
|
|
23
|
+
const readableStream = Readable.from(htmlContent);
|
|
24
|
+
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" });
|
|
25
|
+
readableStream.pipe(res);
|
|
26
|
+
})
|
|
27
|
+
.listen(3000);
|