sjabloon 0.8.0 → 0.9.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/README.md +83 -43
- package/lib/core.js +419 -233
- package/lib/html.d.ts +2 -2
- package/lib/html.js +9 -9
- package/lib/index.d.ts +2 -2
- package/lib/index.js +25 -16
- package/lib/text.d.ts +2 -2
- package/lib/text.js +7 -7
- package/lib/types.d.ts +36 -23
- package/package.json +108 -99
package/lib/html.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compile a template once, render it many times to an HTML string.
|
package/lib/html.js
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
* 0.6's behaviour, kept for templates that target HTML directly. Everything
|
|
4
4
|
* else in sjabloon is output-neutral; escaping lives here and nowhere else.
|
|
5
5
|
*/
|
|
6
|
-
import { make } from
|
|
6
|
+
import { litNode, make } from "./core.js";
|
|
7
7
|
|
|
8
8
|
/** @type {Record<string, string>} */
|
|
9
|
-
const ESC = {
|
|
9
|
+
const ESC = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
10
10
|
/** @param {any} s */
|
|
11
|
-
const esc = s => String(s).replace(/[&<>"']/g, c => ESC[c]);
|
|
11
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ESC[c]);
|
|
12
12
|
|
|
13
|
-
export { isDiagnostic } from
|
|
13
|
+
export { isDiagnostic, relocate } from "./core.js";
|
|
14
14
|
|
|
15
15
|
export const { template, render } = make([
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
litNode,
|
|
17
|
+
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text += esc(value ?? ""))),
|
|
18
|
+
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text += String(value ?? ""))),
|
|
19
|
+
() => ({ text: "" }),
|
|
20
|
+
(acc) => acc.text,
|
|
21
21
|
]);
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues, Token } from
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues, Token } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compile a template once, render it many times to a token stream.
|
package/lib/index.js
CHANGED
|
@@ -4,18 +4,23 @@
|
|
|
4
4
|
* so nothing here is HTML-aware and `{{{ }}}` has no meaning — `{{ }}` is
|
|
5
5
|
* already raw.
|
|
6
6
|
*/
|
|
7
|
-
import { make } from
|
|
7
|
+
import { make } from "./core.js";
|
|
8
8
|
|
|
9
|
-
export { isDiagnostic } from
|
|
9
|
+
export { isDiagnostic, relocate } from "./core.js";
|
|
10
10
|
|
|
11
11
|
export const { template, render } = make([
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
// Static text is a compile-time constant: hoist and freeze one token per
|
|
13
|
+
// text node rather than allocating a fresh object every loop iteration.
|
|
14
|
+
(text) =>
|
|
15
|
+
((token) => (scope, acc) => {
|
|
16
|
+
acc.push(token);
|
|
17
|
+
})(Object.freeze({ literal: text })),
|
|
18
|
+
(expr) => (scope, acc) => {
|
|
19
|
+
acc.push({ value: expr(scope) });
|
|
20
|
+
},
|
|
21
|
+
0,
|
|
22
|
+
() => /** @type {import('./types.js').Token[]} */ ([]),
|
|
23
|
+
(acc) => acc,
|
|
19
24
|
]);
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -25,11 +30,15 @@ export const { template, render } = make([
|
|
|
25
30
|
* @param {readonly import('./types.js').Token[]} tokens A render's output.
|
|
26
31
|
* @returns {string} The joined text.
|
|
27
32
|
*/
|
|
28
|
-
export const text = tokens => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
export const text = (tokens) => {
|
|
34
|
+
let s = "";
|
|
35
|
+
// One `?? ''` per token, so a literal never stringifies and a nullish value
|
|
36
|
+
// still renders empty. Widened here because each token carries one key or
|
|
37
|
+
// the other, which the public union deliberately does not model.
|
|
38
|
+
for (const t of /** @type {readonly { literal?: string, value?: unknown }[]} */ (tokens))
|
|
39
|
+
// Stringifying an arbitrary value is this helper's documented contract, so
|
|
40
|
+
// `no-base-to-string` is describing the feature rather than a mistake.
|
|
41
|
+
// oxlint-disable-next-line typescript/no-base-to-string
|
|
42
|
+
s += t.literal ?? String(t.value ?? "");
|
|
43
|
+
return s;
|
|
35
44
|
};
|
package/lib/text.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from "./types.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compile a template once, render it many times to a plain string.
|
package/lib/text.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* Definitionally `text(template(str)(values))` from the root entry, but built
|
|
7
7
|
* as a string accumulator so casual string users never allocate tokens.
|
|
8
8
|
*/
|
|
9
|
-
import { make } from
|
|
9
|
+
import { litNode, make } from "./core.js";
|
|
10
10
|
|
|
11
|
-
export { isDiagnostic } from
|
|
11
|
+
export { isDiagnostic, relocate } from "./core.js";
|
|
12
12
|
|
|
13
13
|
export const { template, render } = make([
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
litNode,
|
|
15
|
+
(expr) => (scope, acc, value) => ((value = expr(scope)), (acc.text += String(value ?? ""))),
|
|
16
|
+
0,
|
|
17
|
+
() => ({ text: "" }),
|
|
18
|
+
(acc) => acc.text,
|
|
19
19
|
]);
|
package/lib/types.d.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import type { XprsnErrorCode } from
|
|
1
|
+
import type { XprsnErrorCode } from "xprsn";
|
|
2
2
|
|
|
3
3
|
export type SjabloonErrorCode =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
| XprsnErrorCode
|
|
5
|
+
| "SJABLOON_EACH_SYNTAX"
|
|
6
|
+
| "SJABLOON_BLOCKED_BINDING"
|
|
7
|
+
| "SJABLOON_UNEXPECTED_TAG"
|
|
8
|
+
| "SJABLOON_UNKNOWN_BLOCK"
|
|
9
|
+
| "SJABLOON_UNCLOSED_BLOCK"
|
|
10
|
+
| "SJABLOON_TOO_DEEP"
|
|
11
|
+
| "SJABLOON_RAW_TAG";
|
|
12
12
|
|
|
13
13
|
export interface SjabloonBlock {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
readonly type: "if" | "each";
|
|
15
|
+
readonly start: number;
|
|
16
|
+
readonly end: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface SjabloonDiagnostic extends Error {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
readonly code: SjabloonErrorCode;
|
|
21
|
+
readonly start: number;
|
|
22
|
+
readonly end: number;
|
|
23
|
+
readonly blocks: readonly SjabloonBlock[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export type SjabloonValues = Record<string, any>;
|
|
@@ -41,8 +41,8 @@ export type SjabloonFunctions = Record<string, Function>;
|
|
|
41
41
|
* band that has no current row wants exactly that.
|
|
42
42
|
*/
|
|
43
43
|
export interface SjabloonScope {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
root?: any;
|
|
45
|
+
item?: any;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -53,19 +53,19 @@ export interface SjabloonScope {
|
|
|
53
53
|
* registry functions the template calls, deduplicated.
|
|
54
54
|
*/
|
|
55
55
|
export interface SjabloonRenderer<T> {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
(values?: SjabloonValues, scope?: SjabloonScope): T;
|
|
57
|
+
names: string[];
|
|
58
|
+
functions: string[];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** One static text run of the template, verbatim. */
|
|
62
62
|
export interface LiteralToken {
|
|
63
|
-
|
|
63
|
+
literal: string;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** One `{{ }}` interpolation, pre-stringify. Nullish values are preserved. */
|
|
67
67
|
export interface ValueToken {
|
|
68
|
-
|
|
68
|
+
value: unknown;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
@@ -81,3 +81,16 @@ export type Token = LiteralToken | ValueToken;
|
|
|
81
81
|
* through all of them.
|
|
82
82
|
*/
|
|
83
83
|
export function isDiagnostic(error: unknown): error is SjabloonDiagnostic;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Copy a diagnostic into an embedder's coordinates: `prefix` is prepended to
|
|
87
|
+
* the message verbatim, `offset` shifts the span, every other field — the
|
|
88
|
+
* frozen `blocks` context included — is carried over, and the copy is
|
|
89
|
+
* authenticated exactly as the original was.
|
|
90
|
+
*
|
|
91
|
+
* @throws {TypeError} When `diag` is not a sjabloon diagnostic.
|
|
92
|
+
*/
|
|
93
|
+
export function relocate(
|
|
94
|
+
diag: unknown,
|
|
95
|
+
opts?: { prefix?: string; offset?: number },
|
|
96
|
+
): SjabloonDiagnostic;
|
package/package.json
CHANGED
|
@@ -1,101 +1,110 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
2
|
+
"name": "sjabloon",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Tiny, CSP-safe template engine for JavaScript, powered by xprsn expressions. No eval, no new Function.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"csp",
|
|
7
|
+
"handlebars",
|
|
8
|
+
"render",
|
|
9
|
+
"template",
|
|
10
|
+
"template-engine"
|
|
11
|
+
],
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Robin van der Vleuten",
|
|
15
|
+
"email": "robin@webstronauts.com",
|
|
16
|
+
"url": "https://robinvdvleuten.nl"
|
|
17
|
+
},
|
|
18
|
+
"repository": "getquario/sjabloon",
|
|
19
|
+
"files": [
|
|
20
|
+
"lib"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"module": "lib/index.js",
|
|
24
|
+
"types": "lib/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./lib/index.d.ts",
|
|
28
|
+
"default": "./lib/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./text": {
|
|
31
|
+
"types": "./lib/text.d.ts",
|
|
32
|
+
"default": "./lib/text.js"
|
|
33
|
+
},
|
|
34
|
+
"./html": {
|
|
35
|
+
"types": "./lib/html.d.ts",
|
|
36
|
+
"default": "./lib/html.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"bench": "node --disallow-code-generation-from-strings bench/index.js",
|
|
42
|
+
"bench:comparison": "npm --prefix bench/comparison run bench",
|
|
43
|
+
"check": "run-s fmt:check lint fallow size test fuzz:regression test:browser",
|
|
44
|
+
"fallow": "fallow",
|
|
45
|
+
"fmt": "oxfmt",
|
|
46
|
+
"fmt:check": "oxfmt --check",
|
|
47
|
+
"fuzz": "npm run fuzz:prepare && run-s fuzz:compile fuzz:render fuzz:structured",
|
|
48
|
+
"fuzz:compile": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/compile.fuzz.js -i lib/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync .fuzz-corpus/compile fuzz/corpus/compile -- -max_total_time=60 -use_value_profile=1 -dict=fuzz/sjabloon.dict -artifact_prefix=fuzz/",
|
|
49
|
+
"fuzz:prepare": "node -e \"for (const x of ['compile','render','structured']) require('fs').mkdirSync('.fuzz-corpus/'+x,{recursive:true})\"",
|
|
50
|
+
"fuzz:regression": "run-s fuzz:regression:compile fuzz:regression:render fuzz:regression:structured",
|
|
51
|
+
"fuzz:regression:compile": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/compile.fuzz.js -i lib/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync --mode=regression fuzz/corpus/compile -- -artifact_prefix=fuzz/",
|
|
52
|
+
"fuzz:regression:render": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/render.fuzz.js -i lib/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync --mode=regression fuzz/corpus/render -- -artifact_prefix=fuzz/",
|
|
53
|
+
"fuzz:regression:structured": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/structured.fuzz.js -i lib/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync --mode=regression fuzz/corpus/structured -- -artifact_prefix=fuzz/",
|
|
54
|
+
"fuzz:render": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/render.fuzz.js -i lib/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync .fuzz-corpus/render fuzz/corpus/render -- -max_total_time=60 -use_value_profile=1 -dict=fuzz/sjabloon.dict -artifact_prefix=fuzz/",
|
|
55
|
+
"fuzz:structured": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/structured.fuzz.js -i lib/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync .fuzz-corpus/structured fuzz/corpus/structured -- -max_total_time=60 -use_value_profile=1 -dict=fuzz/sjabloon.dict -artifact_prefix=fuzz/",
|
|
56
|
+
"lint": "oxlint",
|
|
57
|
+
"size": "size-limit",
|
|
58
|
+
"test": "run-s test:unit test:types",
|
|
59
|
+
"test:browser": "playwright install chromium && node test/browser/harness.js",
|
|
60
|
+
"test:types": "tsc && attw --pack . --profile esm-only",
|
|
61
|
+
"test:unit": "c8 --100 --src lib/ node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"xprsn": "^0.10.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@arethetypeswrong/cli": "^0.18.3",
|
|
68
|
+
"@jazzer.js/bug-detectors": "^4.0.0",
|
|
69
|
+
"@jazzer.js/core": "^4.0.0",
|
|
70
|
+
"@size-limit/preset-small-lib": "^13.0.3",
|
|
71
|
+
"c8": "^12.0.0",
|
|
72
|
+
"fallow": "^3.17.0",
|
|
73
|
+
"npm-run-all": "^4.1.5",
|
|
74
|
+
"oxfmt": "^0.64.0",
|
|
75
|
+
"oxlint": "^1.78.0",
|
|
76
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
77
|
+
"playwright": "^1.61.1",
|
|
78
|
+
"size-limit": "^13.0.3",
|
|
79
|
+
"typescript": "7.0.2"
|
|
80
|
+
},
|
|
81
|
+
"size-limit": [
|
|
82
|
+
{
|
|
83
|
+
"name": "sjabloon",
|
|
84
|
+
"path": "lib/index.js",
|
|
85
|
+
"ignore": [
|
|
86
|
+
"xprsn"
|
|
87
|
+
],
|
|
88
|
+
"limit": "2.25 kB"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "sjabloon/text",
|
|
92
|
+
"path": "lib/text.js",
|
|
93
|
+
"ignore": [
|
|
94
|
+
"xprsn"
|
|
95
|
+
],
|
|
96
|
+
"limit": "2.25 kB"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "sjabloon/html",
|
|
100
|
+
"path": "lib/html.js",
|
|
101
|
+
"ignore": [
|
|
102
|
+
"xprsn"
|
|
103
|
+
],
|
|
104
|
+
"limit": "2.3 kB"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"engines": {
|
|
108
|
+
"node": ">=22.12.0"
|
|
109
|
+
}
|
|
101
110
|
}
|