sjabloon 0.6.0 → 0.8.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 +61 -23
- package/lib/core.js +446 -0
- package/lib/html.d.ts +17 -0
- package/lib/html.js +21 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +35 -0
- package/lib/text.d.ts +17 -0
- package/lib/text.js +19 -0
- package/lib/types.d.ts +83 -0
- package/package.json +51 -33
- package/dist/index.cjs +0 -1
- package/dist/index.js +0 -1
- package/index.d.ts +0 -82
- package/src/index.js +0 -291
package/lib/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token edition, and the engine proper: a template renders to a stream of
|
|
3
|
+
* literal and value tokens. Escaping belongs to whoever consumes the stream,
|
|
4
|
+
* so nothing here is HTML-aware and `{{{ }}}` has no meaning — `{{ }}` is
|
|
5
|
+
* already raw.
|
|
6
|
+
*/
|
|
7
|
+
import { make } from './core.js';
|
|
8
|
+
|
|
9
|
+
export { isDiagnostic } from './core.js';
|
|
10
|
+
|
|
11
|
+
export const { template, render } = make([
|
|
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
|
+
s => (t => (v, o) => { o.push(t); })(Object.freeze({ literal: s })),
|
|
15
|
+
e => (v, o) => { o.push({ value: e(v) }); },
|
|
16
|
+
0,
|
|
17
|
+
() => /** @type {import('./types.js').Token[]} */ ([]),
|
|
18
|
+
o => o,
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Join a token stream into the string `sjabloon/text` would have produced:
|
|
23
|
+
* literals verbatim, values as `String(value ?? '')`.
|
|
24
|
+
*
|
|
25
|
+
* @param {readonly import('./types.js').Token[]} tokens A render's output.
|
|
26
|
+
* @returns {string} The joined text.
|
|
27
|
+
*/
|
|
28
|
+
export const text = tokens => {
|
|
29
|
+
let s = '';
|
|
30
|
+
// One `?? ''` per token, so a literal never stringifies and a nullish value
|
|
31
|
+
// still renders empty. Widened here because each token carries one key or
|
|
32
|
+
// the other, which the public union deliberately does not model.
|
|
33
|
+
for (const t of /** @type {readonly { literal?: string, value?: unknown }[]} */ (tokens)) s += t.literal ?? String(t.value ?? '');
|
|
34
|
+
return s;
|
|
35
|
+
};
|
package/lib/text.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
import type { SjabloonFunctions, SjabloonRenderer, SjabloonValues } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compile a template once, render it many times to a plain string.
|
|
6
|
+
*
|
|
7
|
+
* `{{ expr }}` interpolates unescaped — escaping belongs at the output edge —
|
|
8
|
+
* so `{{{ expr }}}` has no meaning here and is a compile-time
|
|
9
|
+
* `SJABLOON_RAW_TAG` error.
|
|
10
|
+
*
|
|
11
|
+
* @see SjabloonRenderer for `names`/`functions`, SjabloonScope for `$` and `@`.
|
|
12
|
+
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
13
|
+
*/
|
|
14
|
+
export function template(str: string, funcs?: SjabloonFunctions): SjabloonRenderer<string>;
|
|
15
|
+
|
|
16
|
+
/** Compile and render in one go. Shorthand for `template(str, funcs)(values)`. */
|
|
17
|
+
export function render(str: string, values?: SjabloonValues, funcs?: SjabloonFunctions): string;
|
package/lib/text.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain-text edition: `{{ }}` interpolates unescaped and renders to a
|
|
3
|
+
* string. Escaping belongs at the output edge, so there is no raw form —
|
|
4
|
+
* `{{ }}` is already raw and `{{{ }}}` is a compile-time error.
|
|
5
|
+
*
|
|
6
|
+
* Definitionally `text(template(str)(values))` from the root entry, but built
|
|
7
|
+
* as a string accumulator so casual string users never allocate tokens.
|
|
8
|
+
*/
|
|
9
|
+
import { make } from './core.js';
|
|
10
|
+
|
|
11
|
+
export { isDiagnostic } from './core.js';
|
|
12
|
+
|
|
13
|
+
export const { template, render } = make([
|
|
14
|
+
s => (v, o) => { o.s += s; },
|
|
15
|
+
e => (v, o, x) => (x = e(v), o.s += String(x ?? '')),
|
|
16
|
+
0,
|
|
17
|
+
() => ({ s: '' }),
|
|
18
|
+
o => o.s,
|
|
19
|
+
]);
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { XprsnErrorCode } from 'xprsn';
|
|
2
|
+
|
|
3
|
+
export type SjabloonErrorCode =
|
|
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
|
+
|
|
13
|
+
export interface SjabloonBlock {
|
|
14
|
+
readonly type: 'if' | 'each';
|
|
15
|
+
readonly start: number;
|
|
16
|
+
readonly end: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SjabloonDiagnostic extends Error {
|
|
20
|
+
readonly code: SjabloonErrorCode;
|
|
21
|
+
readonly start: number;
|
|
22
|
+
readonly end: number;
|
|
23
|
+
readonly blocks: readonly SjabloonBlock[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SjabloonValues = Record<string, any>;
|
|
27
|
+
export type SjabloonFunctions = Record<string, Function>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Per-render override of the scope anchors, for embedders with their own scope
|
|
31
|
+
* model.
|
|
32
|
+
*
|
|
33
|
+
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
34
|
+
* current `#each` item (the root outside any loop). They let a nested loop
|
|
35
|
+
* reach the root (`$.company`) or the current item (`@.total`) explicitly,
|
|
36
|
+
* past any shadowing. Neither counts as a `name`.
|
|
37
|
+
*
|
|
38
|
+
* Passing this object as the renderer's second argument makes `$` become
|
|
39
|
+
* `root` and `@` become `item` (two distinct objects). Omit `item` to leave
|
|
40
|
+
* `@` unbound, so reading `@.x` throws through xprsn's guard — a group-header
|
|
41
|
+
* band that has no current row wants exactly that.
|
|
42
|
+
*/
|
|
43
|
+
export interface SjabloonScope {
|
|
44
|
+
root?: any;
|
|
45
|
+
item?: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A compiled template: render it many times.
|
|
50
|
+
*
|
|
51
|
+
* `names` are the variables the template reads from your values, deduplicated;
|
|
52
|
+
* loop variables the template introduces are not included. `functions` are the
|
|
53
|
+
* registry functions the template calls, deduplicated.
|
|
54
|
+
*/
|
|
55
|
+
export interface SjabloonRenderer<T> {
|
|
56
|
+
(values?: SjabloonValues, scope?: SjabloonScope): T;
|
|
57
|
+
names: string[];
|
|
58
|
+
functions: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One static text run of the template, verbatim. */
|
|
62
|
+
export interface LiteralToken {
|
|
63
|
+
literal: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One `{{ }}` interpolation, pre-stringify. Nullish values are preserved. */
|
|
67
|
+
export interface ValueToken {
|
|
68
|
+
value: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A render's output in the token edition, in render order: loop bodies append
|
|
73
|
+
* once per iteration, untaken branches append nothing, and block expressions
|
|
74
|
+
* (`#if` conditions, `#each` collections) never appear.
|
|
75
|
+
*/
|
|
76
|
+
export type Token = LiteralToken | ValueToken;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check whether an error was produced or translated by sjabloon. Every entry
|
|
80
|
+
* shares one core, so a diagnostic thrown through any of them authenticates
|
|
81
|
+
* through all of them.
|
|
82
|
+
*/
|
|
83
|
+
export function isDiagnostic(error: unknown): error is SjabloonDiagnostic;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sjabloon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Tiny, CSP-safe template engine for JavaScript, powered by xprsn expressions. No eval, no new Function.",
|
|
5
5
|
"repository": "getquario/sjabloon",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -10,55 +10,73 @@
|
|
|
10
10
|
"url": "https://robinvdvleuten.nl"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"module": "dist/index.js",
|
|
16
|
-
"types": "index.d.ts",
|
|
13
|
+
"module": "lib/index.js",
|
|
14
|
+
"types": "lib/index.d.ts",
|
|
17
15
|
"exports": {
|
|
18
16
|
".": {
|
|
19
|
-
"types": "./index.d.ts",
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
"types": "./lib/index.d.ts",
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./text": {
|
|
21
|
+
"types": "./lib/text.d.ts",
|
|
22
|
+
"default": "./lib/text.js"
|
|
23
|
+
},
|
|
24
|
+
"./html": {
|
|
25
|
+
"types": "./lib/html.d.ts",
|
|
26
|
+
"default": "./lib/html.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
23
29
|
},
|
|
24
30
|
"engines": {
|
|
25
|
-
"node": ">=22.
|
|
31
|
+
"node": ">=22.12.0"
|
|
26
32
|
},
|
|
27
33
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"src",
|
|
30
|
-
"index.d.ts"
|
|
34
|
+
"lib"
|
|
31
35
|
],
|
|
32
36
|
"size-limit": [
|
|
33
37
|
{
|
|
34
|
-
"
|
|
35
|
-
"
|
|
38
|
+
"name": "sjabloon",
|
|
39
|
+
"path": "lib/index.js",
|
|
40
|
+
"ignore": [
|
|
41
|
+
"xprsn"
|
|
42
|
+
],
|
|
43
|
+
"limit": "1.95 kB"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "sjabloon/text",
|
|
47
|
+
"path": "lib/text.js",
|
|
48
|
+
"ignore": [
|
|
49
|
+
"xprsn"
|
|
50
|
+
],
|
|
51
|
+
"limit": "1.9 kB"
|
|
36
52
|
},
|
|
37
53
|
{
|
|
38
|
-
"
|
|
39
|
-
"
|
|
54
|
+
"name": "sjabloon/html",
|
|
55
|
+
"path": "lib/html.js",
|
|
56
|
+
"ignore": [
|
|
57
|
+
"xprsn"
|
|
58
|
+
],
|
|
59
|
+
"limit": "1.97 kB"
|
|
40
60
|
}
|
|
41
61
|
],
|
|
42
62
|
"scripts": {
|
|
43
63
|
"bench": "node --disallow-code-generation-from-strings bench/index.js",
|
|
44
|
-
"bench:comparison": "npm
|
|
45
|
-
"
|
|
46
|
-
"check": "run-s build size test fuzz:regression test:browser",
|
|
64
|
+
"bench:comparison": "npm --prefix bench/comparison run bench",
|
|
65
|
+
"check": "run-s size test fuzz:regression test:browser",
|
|
47
66
|
"fuzz": "npm run fuzz:prepare && run-s fuzz:compile fuzz:render fuzz:structured",
|
|
48
67
|
"fuzz:prepare": "node -e \"for (const x of ['compile','render','structured']) require('fs').mkdirSync('.fuzz-corpus/'+x,{recursive:true})\"",
|
|
49
|
-
"fuzz:compile": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/compile.fuzz.js -i
|
|
50
|
-
"fuzz:render": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/render.fuzz.js -i
|
|
51
|
-
"fuzz:structured": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/structured.fuzz.js -i
|
|
68
|
+
"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/",
|
|
69
|
+
"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/",
|
|
70
|
+
"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/",
|
|
52
71
|
"fuzz:regression": "run-s fuzz:regression:compile fuzz:regression:render fuzz:regression:structured",
|
|
53
|
-
"fuzz:regression:compile": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/compile.fuzz.js -i
|
|
54
|
-
"fuzz:regression:render": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/render.fuzz.js -i
|
|
55
|
-
"fuzz:regression:structured": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/structured.fuzz.js -i
|
|
56
|
-
"prepublishOnly": "npm run build",
|
|
72
|
+
"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/",
|
|
73
|
+
"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/",
|
|
74
|
+
"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/",
|
|
57
75
|
"size": "size-limit",
|
|
58
76
|
"test": "run-s test:unit test:types",
|
|
59
77
|
"test:unit": "node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js",
|
|
60
|
-
"test:browser": "
|
|
61
|
-
"test:types": "tsc"
|
|
78
|
+
"test:browser": "playwright install chromium && node test/browser/harness.js",
|
|
79
|
+
"test:types": "tsc && attw --pack . --profile esm-only"
|
|
62
80
|
},
|
|
63
81
|
"keywords": [
|
|
64
82
|
"template",
|
|
@@ -68,16 +86,16 @@
|
|
|
68
86
|
"handlebars"
|
|
69
87
|
],
|
|
70
88
|
"dependencies": {
|
|
71
|
-
"xprsn": "^0.
|
|
89
|
+
"xprsn": "^0.9.0"
|
|
72
90
|
},
|
|
73
91
|
"devDependencies": {
|
|
92
|
+
"@arethetypeswrong/cli": "^0.18.3",
|
|
74
93
|
"@jazzer.js/bug-detectors": "^4.0.0",
|
|
75
94
|
"@jazzer.js/core": "^4.0.0",
|
|
76
|
-
"@size-limit/
|
|
95
|
+
"@size-limit/preset-small-lib": "^12.1.0",
|
|
77
96
|
"npm-run-all": "^4.1.5",
|
|
78
97
|
"playwright": "^1.61.1",
|
|
79
98
|
"size-limit": "^12.1.0",
|
|
80
|
-
"
|
|
81
|
-
"typescript": "^7.0.2"
|
|
99
|
+
"typescript": "7.0.2"
|
|
82
100
|
}
|
|
83
101
|
}
|
package/dist/index.cjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("xprsn");const t={"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`},n=e=>String(e).replace(/[&<>"']/g,e=>t[e]),r=/^(?:__proto__|constructor|prototype)$/,i=new WeakSet,a=i.add.bind(i),o=i.has.bind(i),s=e=>o(e);let c=e=>{let t=[];for(let n=0,r=1;n<e.length;){let i=e.indexOf(`{{`,n);if(i<0){t.push([0,e.slice(n)]);break}i>n&&t.push([0,e.slice(n,i)]);let a=e[i+2]===`{`,o=i+2+a,s=e[o]===`-`,c=-1;if(s&&o++,a&&r&&(c=e.indexOf(`}}}`,o),c<0&&(r=0)),c<0&&(a&&(a=!1,o=i+2,s=e[o]===`-`,s&&o++),c=e.indexOf(`}}`,o)),c<0){t.push([0,e.slice(i)]);break}let l=c>o&&e[c-1]===`-`,u=l?c-1:c,d=e.slice(o,u),f=d.trim(),p=o+d.length-d.trimStart().length,m=c+2+a,h=[a?1:2,f,i,m,p],g=t.at(-1);if(s&&g?.[0]===0&&g[1]&&(g[1]=g[1].trimEnd()),t.push(h),n=m,l)for(;/\s/.test(e[n]);)n++}return t},l,u,d,f,p,m,h,g,_;const v=Symbol();let y=()=>Object.freeze(_.slice()),b=(e,t)=>(_.length<256||S(`Template too deeply nested`,`SJABLOON_TOO_DEEP`,t),Object.freeze({type:e,start:t[2],end:t[3]})),x=(e,t)=>(Object.defineProperty(e,"blocks",{value:t,enumerable:!0}),a(e),e),S=(e,t,n,r=n?.[2]??g.length,i=n?.[3]??g.length)=>{let a=SyntaxError(e);throw a.code=t,a.start=r,a.end=i,x(a,y())},C=(t,n,r,i=e.isDiagnostic)=>{throw i(t)?(t.start+=n,t.end+=n,x(t,r)):t},w=e=>S(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),T=(e,t)=>e.map(e=>e(t)).join(``),E=(e,t)=>(e=>(n,r)=>(r=e(n),n[v]?.push(r),t(r??``)))(D(e[1],e[4],y())),D=(t,n,r)=>{let i;try{i=(0,e.compile)(t,d)}catch(e){C(e,n,r)}for(let e of i.names)p.includes(e)||m.add(e);for(let e of i.functions)h.add(e);return e=>{try{return i(e)}catch(e){C(e,n,r,i.isDiagnostic)}}},O=e=>{let t=k([`#elif`,`#else`,`/if`]),n=f[1],r=[];return n.startsWith(`#elif `)?r=[O(D(n.slice(6),f[4]+6,y()))]:n===`#else`?(r=k([`/if`]),f[1]===`/if`||w(f)):n!==`/if`&&w(f),n=>T(e(n)?t:r,n)},k=e=>{let t=[];for(let i;i=l[u++];){let a=i[1];if(!i[0])t.push((e=>()=>e)(a));else if(i[0]===1)t.push(E(i,String));else if(e.includes(a.split(` `)[0]))return f=i,t;else if(a[0]!==`!`)if(a.startsWith(`#if `))_.push(b(`if`,i)),t.push(O(D(a.slice(4),i[4]+4,y()))),_.pop();else if(/^#each(?:\s|$)/.test(a)){_.push(b(`each`,i));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(a);e||S(`Bad {{`+a+`}}`,`SJABLOON_EACH_SYNTAX`,i);let n=e[3],o=e[4],s=i[4]+a.length-e[2].length;if(r.test(n)&&S(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,s,s+n.length),o&&r.test(o)){let e=i[4]+a.length-o.length;S(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,e,e+o.length)}let c=D(e[1],i[4]+6,y()),l=p.length;p.push(n),o&&p.push(o),p.push(`loop`);let u=k([`#else`,`/each`]);p.length=l;let d=[];f[1]===`#else`?(d=k([`/each`]),f[1]===`/each`||w(f)):f[1]!==`/each`&&w(f),_.pop(),t.push(e=>{let t=c(e),r=Array.isArray(t),i=r?t.slice():t&&typeof t==`object`?Object.keys(t).map(e=>[t[e],e]):[];return i.length?i.map((t,a)=>{let s=r?t:t[0],c=r?a:t[1],l=Object.create(e);return l[n]=s,o&&(l[o]=c),l[`@`]=s,l.loop={index:a+1,index0:a,first:!a,last:a===i.length-1,length:i.length},T(u,l)}).join(``):T(d,e)})}else/^#(?:if|elif|else)(?:\s|$)/.test(a)||a[0]===`/`?w(i):a[0]===`#`?S(`Unknown {{`+a+`}}`,`SJABLOON_UNKNOWN_BLOCK`,i):t.push(E(i,n))}return e.length&&S(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t};function A(e,t){d=t,p=[`$`,`@`],m=new Set,h=new Set,g=String(e),_=[],l=c(g),u=0;let n;try{n=k([])}catch(e){throw e instanceof RangeError&&S(`Template too deeply nested`,`SJABLOON_TOO_DEEP`),e}let r=(e,t,r)=>{e||={};let i=Object.create(e);return i.$=t?t.root:e,t?`item`in t&&(i[`@`]=t.item):i[`@`]=e,i[v]=r,T(n,i)},i=(e,t)=>r(e,t);return i.withRaw=(e,t,n=[])=>({text:r(e,t,n),raws:n}),i.names=Array.from(m),i.functions=Array.from(h),i}function j(e,t,n){return A(e,n)(t)}exports.isDiagnostic=s,exports.render=j,exports.template=A;
|
package/dist/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{compile as e,isDiagnostic as t}from"xprsn";const n={"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`},r=e=>String(e).replace(/[&<>"']/g,e=>n[e]),i=/^(?:__proto__|constructor|prototype)$/,a=new WeakSet,o=a.add.bind(a),s=a.has.bind(a),c=e=>s(e);let l=e=>{let t=[];for(let n=0,r=1;n<e.length;){let i=e.indexOf(`{{`,n);if(i<0){t.push([0,e.slice(n)]);break}i>n&&t.push([0,e.slice(n,i)]);let a=e[i+2]===`{`,o=i+2+a,s=e[o]===`-`,c=-1;if(s&&o++,a&&r&&(c=e.indexOf(`}}}`,o),c<0&&(r=0)),c<0&&(a&&(a=!1,o=i+2,s=e[o]===`-`,s&&o++),c=e.indexOf(`}}`,o)),c<0){t.push([0,e.slice(i)]);break}let l=c>o&&e[c-1]===`-`,u=l?c-1:c,d=e.slice(o,u),f=d.trim(),p=o+d.length-d.trimStart().length,m=c+2+a,h=[a?1:2,f,i,m,p],g=t.at(-1);if(s&&g?.[0]===0&&g[1]&&(g[1]=g[1].trimEnd()),t.push(h),n=m,l)for(;/\s/.test(e[n]);)n++}return t},u,d,f,p,m,h,g,_,v;const y=Symbol();let b=()=>Object.freeze(v.slice()),x=(e,t)=>(v.length<256||C(`Template too deeply nested`,`SJABLOON_TOO_DEEP`,t),Object.freeze({type:e,start:t[2],end:t[3]})),S=(e,t)=>(Object.defineProperty(e,"blocks",{value:t,enumerable:!0}),o(e),e),C=(e,t,n,r=n?.[2]??_.length,i=n?.[3]??_.length)=>{let a=SyntaxError(e);throw a.code=t,a.start=r,a.end=i,S(a,b())},w=(e,n,r,i=t)=>{throw i(e)?(e.start+=n,e.end+=n,S(e,r)):e},T=e=>C(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),E=(e,t)=>e.map(e=>e(t)).join(``),D=(e,t)=>(e=>(n,r)=>(r=e(n),n[y]?.push(r),t(r??``)))(O(e[1],e[4],b())),O=(t,n,r)=>{let i;try{i=e(t,f)}catch(e){w(e,n,r)}for(let e of i.names)m.includes(e)||h.add(e);for(let e of i.functions)g.add(e);return e=>{try{return i(e)}catch(e){w(e,n,r,i.isDiagnostic)}}},k=e=>{let t=A([`#elif`,`#else`,`/if`]),n=p[1],r=[];return n.startsWith(`#elif `)?r=[k(O(n.slice(6),p[4]+6,b()))]:n===`#else`?(r=A([`/if`]),p[1]===`/if`||T(p)):n!==`/if`&&T(p),n=>E(e(n)?t:r,n)},A=e=>{let t=[];for(let n;n=u[d++];){let a=n[1];if(!n[0])t.push((e=>()=>e)(a));else if(n[0]===1)t.push(D(n,String));else if(e.includes(a.split(` `)[0]))return p=n,t;else if(a[0]!==`!`)if(a.startsWith(`#if `))v.push(x(`if`,n)),t.push(k(O(a.slice(4),n[4]+4,b()))),v.pop();else if(/^#each(?:\s|$)/.test(a)){v.push(x(`each`,n));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(a);e||C(`Bad {{`+a+`}}`,`SJABLOON_EACH_SYNTAX`,n);let r=e[3],o=e[4],s=n[4]+a.length-e[2].length;if(i.test(r)&&C(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,n,s,s+r.length),o&&i.test(o)){let e=n[4]+a.length-o.length;C(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,n,e,e+o.length)}let c=O(e[1],n[4]+6,b()),l=m.length;m.push(r),o&&m.push(o),m.push(`loop`);let u=A([`#else`,`/each`]);m.length=l;let d=[];p[1]===`#else`?(d=A([`/each`]),p[1]===`/each`||T(p)):p[1]!==`/each`&&T(p),v.pop(),t.push(e=>{let t=c(e),n=Array.isArray(t),i=n?t.slice():t&&typeof t==`object`?Object.keys(t).map(e=>[t[e],e]):[];return i.length?i.map((t,a)=>{let s=n?t:t[0],c=n?a:t[1],l=Object.create(e);return l[r]=s,o&&(l[o]=c),l[`@`]=s,l.loop={index:a+1,index0:a,first:!a,last:a===i.length-1,length:i.length},E(u,l)}).join(``):E(d,e)})}else/^#(?:if|elif|else)(?:\s|$)/.test(a)||a[0]===`/`?T(n):a[0]===`#`?C(`Unknown {{`+a+`}}`,`SJABLOON_UNKNOWN_BLOCK`,n):t.push(D(n,r))}return e.length&&C(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t};function j(e,t){f=t,m=[`$`,`@`],h=new Set,g=new Set,_=String(e),v=[],u=l(_),d=0;let n;try{n=A([])}catch(e){throw e instanceof RangeError&&C(`Template too deeply nested`,`SJABLOON_TOO_DEEP`),e}let r=(e,t,r)=>{e||={};let i=Object.create(e);return i.$=t?t.root:e,t?`item`in t&&(i[`@`]=t.item):i[`@`]=e,i[y]=r,E(n,i)},i=(e,t)=>r(e,t);return i.withRaw=(e,t,n=[])=>({text:r(e,t,n),raws:n}),i.names=Array.from(h),i.functions=Array.from(g),i}function M(e,t,n){return j(e,n)(t)}export{c as isDiagnostic,M as render,j as template};
|
package/index.d.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { XprsnErrorCode } from 'xprsn';
|
|
2
|
-
|
|
3
|
-
export type SjabloonErrorCode =
|
|
4
|
-
| XprsnErrorCode
|
|
5
|
-
| 'SJABLOON_EACH_SYNTAX'
|
|
6
|
-
| 'SJABLOON_BLOCKED_BINDING'
|
|
7
|
-
| 'SJABLOON_UNEXPECTED_TAG'
|
|
8
|
-
| 'SJABLOON_UNKNOWN_BLOCK'
|
|
9
|
-
| 'SJABLOON_UNCLOSED_BLOCK';
|
|
10
|
-
|
|
11
|
-
export interface SjabloonBlock {
|
|
12
|
-
readonly type: 'if' | 'each';
|
|
13
|
-
readonly start: number;
|
|
14
|
-
readonly end: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface SjabloonDiagnostic extends Error {
|
|
18
|
-
readonly code: SjabloonErrorCode;
|
|
19
|
-
readonly start: number;
|
|
20
|
-
readonly end: number;
|
|
21
|
-
readonly blocks: readonly SjabloonBlock[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check whether an error was produced or translated by this sjabloon module instance.
|
|
26
|
-
*/
|
|
27
|
-
export function isDiagnostic(error: unknown): error is SjabloonDiagnostic;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Compile a template once, render it many times.
|
|
31
|
-
*
|
|
32
|
-
* The returned renderer exposes `names`: the variables the template reads
|
|
33
|
-
* from your values, deduplicated. Loop variables the template introduces are
|
|
34
|
-
* not included. It also exposes `functions`: the registry functions the
|
|
35
|
-
* template calls, deduplicated.
|
|
36
|
-
*
|
|
37
|
-
* Two anchors are always in scope: `$` is the root values, and `@` is the
|
|
38
|
-
* current `#each` item (the root outside any loop). They let a nested loop
|
|
39
|
-
* reach the root (`$.company`) or the current item (`@.total`) explicitly,
|
|
40
|
-
* past any shadowing. Neither counts as a `name`.
|
|
41
|
-
*
|
|
42
|
-
* An embedder with its own scope model can override the anchors per render by
|
|
43
|
-
* passing `{ root, item }` as the renderer's second argument: `$` becomes
|
|
44
|
-
* `root` and `@` becomes `item` (two distinct objects). Omit `item` to leave
|
|
45
|
-
* `@` unbound, so reading `@.x` throws through xprsn's guard.
|
|
46
|
-
*
|
|
47
|
-
* The renderer also exposes `withRaw(values, scope)`: one render, both channels.
|
|
48
|
-
* It returns `{ text, raws }` — the rendered string plus each interpolation's
|
|
49
|
-
* pre-escape, pre-stringify value (`{{ }}` and `{{{ }}}` alike, nullish
|
|
50
|
-
* included), in render order: loop bodies push once per iteration, untaken
|
|
51
|
-
* branches push nothing. Block expressions (`#if` conditions, `#each`
|
|
52
|
-
* collections) are never captured.
|
|
53
|
-
*
|
|
54
|
-
* @param {string} str The template, e.g. `'Hello {{ user.name }}!'`.
|
|
55
|
-
* @param {Record<string, Function>} [funcs] Functions callable inside expressions.
|
|
56
|
-
* @returns {{(values?: Record<string, any>, scope?: { root?: any, item?: any }): string, withRaw: (values?: Record<string, any>, scope?: { root?: any, item?: any }) => { text: string, raws: unknown[] }, names: string[], functions: string[]}} Renderer for the compiled template.
|
|
57
|
-
* @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
|
|
58
|
-
*/
|
|
59
|
-
export function template(str: string, funcs?: Record<string, Function>): {
|
|
60
|
-
(values?: Record<string, any>, scope?: {
|
|
61
|
-
root?: any;
|
|
62
|
-
item?: any;
|
|
63
|
-
}): string;
|
|
64
|
-
withRaw(values?: Record<string, any>, scope?: {
|
|
65
|
-
root?: any;
|
|
66
|
-
item?: any;
|
|
67
|
-
}): {
|
|
68
|
-
text: string;
|
|
69
|
-
raws: unknown[];
|
|
70
|
-
};
|
|
71
|
-
names: string[];
|
|
72
|
-
functions: string[];
|
|
73
|
-
};
|
|
74
|
-
/**
|
|
75
|
-
* Compile and render a template in one go.
|
|
76
|
-
*
|
|
77
|
-
* @param {string} str The template to render.
|
|
78
|
-
* @param {Record<string, any>} [values] Variables available to the template.
|
|
79
|
-
* @param {Record<string, Function>} [funcs] Functions callable inside expressions.
|
|
80
|
-
* @returns {string} The rendered output.
|
|
81
|
-
*/
|
|
82
|
-
export function render(str: string, values?: Record<string, any>, funcs?: Record<string, Function>): string;
|