sjabloon 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # sjabloon
2
2
 
3
- A tiny, CSP-safe template engine for JavaScript. **~0.8KB min+gzip (~2KB with [xprsn](https://www.npmjs.com/package/xprsn)), one dependency.**
3
+ A tiny, CSP-safe template engine for JavaScript. **~1.2KB min+gzip (~2.5KB with [xprsn](https://www.npmjs.com/package/xprsn)), one dependency.**
4
4
 
5
5
  [![NPM version](https://img.shields.io/npm/v/sjabloon.svg)](https://www.npmjs.com/package/sjabloon)
6
6
  [![Build Status](https://github.com/robinvdvleuten/sjabloon/actions/workflows/test.yml/badge.svg)](https://github.com/robinvdvleuten/sjabloon/actions/workflows/test.yml)
@@ -37,7 +37,15 @@ render(
37
37
 
38
38
  ### `template(str, functions?)`
39
39
 
40
- Compiles the template and returns a renderer `(values?) => string`. Malformed tags, unclosed blocks, and invalid expressions throw a `SyntaxError` at compile time.
40
+ Compiles the template and returns a renderer `(values?, scope?) => string`. Malformed tags, unclosed blocks, and invalid expressions throw a `SyntaxError` at compile time.
41
+
42
+ The anchors `$` (root) and `@` (current `{{#each}}` item) work as [described below](#syntax) with no extra arguments — at the root, before any loop, both point at `values`. If you're embedding sjabloon under an engine with its own scope model, pass `{ root, item }` as the second argument to seed the two root anchors from distinct objects: `$` becomes `root` and `@` becomes `item`. Omit `item` and `@` stays unbound at the root, so reading `@.x` throws where there is no current item. Either way, `{{#each}}` still re-points `@` to the current item inside its body.
43
+
44
+ ```js
45
+ const tpl = template('{{ $.report }} — {{ @.row }}');
46
+ tpl(base, { root: reportRoot, item: currentRow }); // $ = reportRoot, @ = currentRow
47
+ tpl(base, { root: reportRoot }); // no item → @.x throws
48
+ ```
41
49
 
42
50
  The renderer also carries `names` (every variable the template reads from your values, loop variables excluded) and `functions` (the registry functions it calls, methods excluded), both deduplicated. Check a stored template against your data model and its allowed functions before you render it, or fetch only the fields it needs.
43
51
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- var t=require("xprsn");function r(t,r){(null==r||r>t.length)&&(r=t.length);for(var e=0,n=Array(r);e<r;e++)n[e]=t[e];return n}function e(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(n)return(n=n.call(t)).next.bind(n);if(Array.isArray(t)||(n=function(t,e){if(t){if("string"==typeof t)return r(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?r(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var a=0;return function(){return a>=t.length?{done:!0}:{done:!1,value:t[a++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var n,a,u,i,o,l,c,f={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},s=function(t){return String(t).replace(/[&<>"']/g,function(t){return f[t]})},h=/\{\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}\}|\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}/,g=function(t){throw SyntaxError(t)},p=function(t,r){return t.map(function(t){return t(r)}).join("")},v=function(t,r){return e=d(t),function(t){var n;return r(null!=(n=e(t))?n:"")};var e},d=function(r){for(var n,a=t.compile(r,u),i=e(a.names);!(n=i()).done;){var f=n.value;o.has(f)||l.add(f)}for(var s,h=e(a.functions);!(s=h()).done;)c.add(s.value);return a},m=function(t){var r=y(["#elif","#else","/if"]),e=i.startsWith("#elif ")?[m(d(i.slice(6)))]:"#else"===i?y(["/if"]):[];return function(n){return p(t(n)?r:e,n)}},y=function(t){for(var r,e,u=[],l=function(){if(null!=e.text)u.push((x=e.text,function(){return x}));else if(null!=e.raw)u.push(v(e.raw,String));else{if(t.includes(e.tag.split(" ")[0]))return i=e.tag,{v:u};if("!"===e.tag[0]);else if(e.tag.startsWith("#if "))u.push(m(d(e.tag.slice(4))));else if(e.tag.startsWith("#each ")){var r=/^#each ([\s\S]+) as (\w+)(?:\s*,\s*(\w+))?$/.exec(e.tag)||g("Bad {{"+e.tag+"}}"),n=d(r[1]),a=r[2],l=r[3],c=function(){var t=[].slice.call(arguments).filter(function(t){return t&&!o.has(t)});return t.forEach(function(t){return o.add(t)}),function(){return t.forEach(function(t){return o.delete(t)})}}(a,l,"loop"),f=y(["#else","/each"]);c();var h="#else"===i?y(["/each"]):[];u.push(function(t){var r,e=(r=n(t),Array.isArray(r)?r.map(function(t,r){return[t,r]}):r&&"object"==typeof r?Object.keys(r).map(function(t){return[r[t],t]}):[]);return e.length?e.map(function(r,n){var u=r[0],i=r[1],o=Object.create(t);return o[a]=u,l&&(o[l]=i),o["@"]=u,o.loop={index:n+1,index0:n,first:!n,last:n===e.length-1,length:e.length},p(f,o)}).join(""):p(h,t)})}else"#"===e.tag[0]||"/"===e.tag[0]?g("Unexpected {{"+e.tag+"}}"):u.push(v(e.tag,s))}var x};e=n[a++];)if(r=l())return r.v;return t.length&&g("Missing {{"+t[t.length-1]+"}}"),u};function x(t,r){u=r,o=new Set(["$","@"]),l=new Set,c=new Set,n=[];for(var e=String(t).split(h),i=0;i<e.length&&(e[i]&&n.push({text:e[i]}),!(i+1>=e.length));i+=7){var f=null!=e[i+2],s=f?{raw:e[i+2]}:{tag:e[i+5]};s.l="-"===e[i+(f?1:4)],s.r="-"===e[i+(f?3:6)],n.push(s)}n.forEach(function(t,r){var e,a;t.l&&null!=(e=n[r-1])&&e.text&&(n[r-1].text=n[r-1].text.replace(/\s+$/,"")),t.r&&null!=(a=n[r+1])&&a.text&&(n[r+1].text=n[r+1].text.replace(/^\s+/,""))}),a=0;var g=y([]),v=function(t){t=t||{};var r=Object.create(t);return r.$=r["@"]=t,p(g,r)};return v.names=Array.from(l),v.functions=Array.from(c),v}exports.render=function(t,r,e){return x(t,e)(r)},exports.template=x;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("xprsn");const t={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`},n=e=>String(e).replace(/[&<>"']/g,e=>t[e]),r=/^(?:__proto__|constructor|prototype)$/,i=e=>Array.isArray(e)?e.map((e,t)=>[e,t]):e&&typeof e==`object`?Object.keys(e).map(t=>[e[t],t]):[];let a=e=>{let t=[];for(let n=0,r=1;n<e.length;){let i=e.indexOf(`{{`,n);if(i<0){t.push({text:e.slice(n)});break}i>n&&t.push({text:e.slice(n,i)});let a=e.charCodeAt(i+2)===123,o=i+2+a,s=e.charCodeAt(o)===45,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.charCodeAt(o)===45,s&&o++),c=e.indexOf(`}}`,o)),c<0){t.push({text:e.slice(i)});break}let l=c>o&&e.charCodeAt(c-1)===45,u=e.slice(o,l?c-1:c).trim(),d=a?{raw:u}:{tag:u};d.l=s,d.r=l,t.push(d),n=c+2+a}return t},o,s,c,l,u,d,f,p=e=>{throw SyntaxError(e)},m=(e,t)=>e.map(e=>e(t)).join(``),h=(e,t)=>(e=>n=>t(e(n)??``))(_(e)),g=(...e)=>{let t=e.filter(e=>e&&!u.has(e));return t.forEach(e=>u.add(e)),()=>t.forEach(e=>u.delete(e))},_=t=>{let n=(0,e.compile)(t,c);for(let e of n.names)u.has(e)||d.add(e);for(let e of n.functions)f.add(e);return n},v=e=>{let t=y([`#elif`,`#else`,`/if`]),n=l.startsWith(`#elif `)?[v(_(l.slice(6)))]:l===`#else`?y([`/if`]):[];return r=>m(e(r)?t:n,r)},y=e=>{let t=[];for(let a;a=o[s++];)if(a.text!=null)t.push((e=>()=>e)(a.text));else if(a.raw!=null)t.push(h(a.raw,String));else if(e.includes(a.tag.split(` `)[0]))return l=a.tag,t;else if(a.tag[0]!==`!`)if(a.tag.startsWith(`#if `))t.push(v(_(a.tag.slice(4))));else if(a.tag.startsWith(`#each `)){let e=/^#each ([\s\S]+) as (\w+)(?:\s*,\s*(\w+))?$/.exec(a.tag)||p(`Bad {{`+a.tag+`}}`),n=_(e[1]),o=e[2],s=e[3];(r.test(o)||s&&r.test(s))&&p(`Bad {{`+a.tag+`}}`);let c=g(o,s,`loop`),u=y([`#else`,`/each`]);c();let d=l===`#else`?y([`/each`]):[];t.push(e=>{let t=i(n(e));return t.length?t.map(([n,r],i)=>{let a=Object.create(e);return a[o]=n,s&&(a[s]=r),a[`@`]=n,a.loop={index:i+1,index0:i,first:!i,last:i===t.length-1,length:t.length},m(u,a)}).join(``):m(d,e)})}else a.tag[0]===`#`||a.tag[0]===`/`?p(`Unexpected {{`+a.tag+`}}`):t.push(h(a.tag,n));return e.length&&p(`Missing {{`+e[e.length-1]+`}}`),t};function b(e,t){c=t,u=new Set([`$`,`@`]),d=new Set,f=new Set,o=a(String(e)),o.forEach((e,t)=>{e.l&&o[t-1]?.text&&(o[t-1].text=o[t-1].text.trimEnd()),e.r&&o[t+1]?.text&&(o[t+1].text=o[t+1].text.trimStart())}),s=0;let n=y([]),r=(e,t)=>{e||={};let r=Object.create(e);return r.$=t?t.root:e,t?`item`in t&&(r[`@`]=t.item):r[`@`]=e,m(n,r)};return r.names=Array.from(d),r.functions=Array.from(f),r}function x(e,t,n){return b(e,n)(t)}exports.render=x,exports.template=b;
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{compile as e}from"xprsn";const t={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`},n=e=>String(e).replace(/[&<>"']/g,e=>t[e]),r=/^(?:__proto__|constructor|prototype)$/,i=e=>Array.isArray(e)?e.map((e,t)=>[e,t]):e&&typeof e==`object`?Object.keys(e).map(t=>[e[t],t]):[];let a=e=>{let t=[];for(let n=0,r=1;n<e.length;){let i=e.indexOf(`{{`,n);if(i<0){t.push({text:e.slice(n)});break}i>n&&t.push({text:e.slice(n,i)});let a=e.charCodeAt(i+2)===123,o=i+2+a,s=e.charCodeAt(o)===45,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.charCodeAt(o)===45,s&&o++),c=e.indexOf(`}}`,o)),c<0){t.push({text:e.slice(i)});break}let l=c>o&&e.charCodeAt(c-1)===45,u=e.slice(o,l?c-1:c).trim(),d=a?{raw:u}:{tag:u};d.l=s,d.r=l,t.push(d),n=c+2+a}return t},o,s,c,l,u,d,f,p=e=>{throw SyntaxError(e)},m=(e,t)=>e.map(e=>e(t)).join(``),h=(e,t)=>(e=>n=>t(e(n)??``))(_(e)),g=(...e)=>{let t=e.filter(e=>e&&!u.has(e));return t.forEach(e=>u.add(e)),()=>t.forEach(e=>u.delete(e))},_=t=>{let n=e(t,c);for(let e of n.names)u.has(e)||d.add(e);for(let e of n.functions)f.add(e);return n},v=e=>{let t=y([`#elif`,`#else`,`/if`]),n=l.startsWith(`#elif `)?[v(_(l.slice(6)))]:l===`#else`?y([`/if`]):[];return r=>m(e(r)?t:n,r)},y=e=>{let t=[];for(let a;a=o[s++];)if(a.text!=null)t.push((e=>()=>e)(a.text));else if(a.raw!=null)t.push(h(a.raw,String));else if(e.includes(a.tag.split(` `)[0]))return l=a.tag,t;else if(a.tag[0]!==`!`)if(a.tag.startsWith(`#if `))t.push(v(_(a.tag.slice(4))));else if(a.tag.startsWith(`#each `)){let e=/^#each ([\s\S]+) as (\w+)(?:\s*,\s*(\w+))?$/.exec(a.tag)||p(`Bad {{`+a.tag+`}}`),n=_(e[1]),o=e[2],s=e[3];(r.test(o)||s&&r.test(s))&&p(`Bad {{`+a.tag+`}}`);let c=g(o,s,`loop`),u=y([`#else`,`/each`]);c();let d=l===`#else`?y([`/each`]):[];t.push(e=>{let t=i(n(e));return t.length?t.map(([n,r],i)=>{let a=Object.create(e);return a[o]=n,s&&(a[s]=r),a[`@`]=n,a.loop={index:i+1,index0:i,first:!i,last:i===t.length-1,length:t.length},m(u,a)}).join(``):m(d,e)})}else a.tag[0]===`#`||a.tag[0]===`/`?p(`Unexpected {{`+a.tag+`}}`):t.push(h(a.tag,n));return e.length&&p(`Missing {{`+e[e.length-1]+`}}`),t};function b(e,t){c=t,u=new Set([`$`,`@`]),d=new Set,f=new Set,o=a(String(e)),o.forEach((e,t)=>{e.l&&o[t-1]?.text&&(o[t-1].text=o[t-1].text.trimEnd()),e.r&&o[t+1]?.text&&(o[t+1].text=o[t+1].text.trimStart())}),s=0;let n=y([]),r=(e,t)=>{e||={};let r=Object.create(e);return r.$=t?t.root:e,t?`item`in t&&(r[`@`]=t.item):r[`@`]=e,m(n,r)};return r.names=Array.from(d),r.functions=Array.from(f),r}function x(e,t,n){return b(e,n)(t)}export{x as render,b as template};
package/index.d.ts CHANGED
@@ -11,13 +11,21 @@
11
11
  * reach the root (`$.company`) or the current item (`@.total`) explicitly,
12
12
  * past any shadowing. Neither counts as a `name`.
13
13
  *
14
+ * An embedder with its own scope model can override the anchors per render by
15
+ * passing `{ root, item }` as the renderer's second argument: `$` becomes
16
+ * `root` and `@` becomes `item` (two distinct objects). Omit `item` to leave
17
+ * `@` unbound, so reading `@.x` throws through xprsn's guard.
18
+ *
14
19
  * @param {string} str The template, e.g. `'Hello {{ user.name }}!'`.
15
20
  * @param {Record<string, Function>} [funcs] Functions callable inside expressions.
16
- * @returns {{(values?: Record<string, any>): string, names: string[], functions: string[]}} Renderer for the compiled template.
21
+ * @returns {{(values?: Record<string, any>, scope?: { root?: any, item?: any }): string, names: string[], functions: string[]}} Renderer for the compiled template.
17
22
  * @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
18
23
  */
19
24
  export function template(str: string, funcs?: Record<string, Function>): {
20
- (values?: Record<string, any>): string;
25
+ (values?: Record<string, any>, scope?: {
26
+ root?: any;
27
+ item?: any;
28
+ }): string;
21
29
  names: string[];
22
30
  functions: string[];
23
31
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sjabloon",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Tiny, CSP-safe template engine for JavaScript, powered by xprsn expressions. No eval, no new Function.",
5
5
  "repository": "robinvdvleuten/sjabloon",
6
6
  "license": "MIT",
@@ -11,16 +11,14 @@
11
11
  },
12
12
  "type": "module",
13
13
  "source": "src/index.js",
14
- "main": "./dist/index.cjs",
15
- "module": "./dist/index.module.js",
16
- "unpkg": "./dist/index.umd.js",
17
- "types": "./index.d.ts",
14
+ "main": "dist/index.cjs",
15
+ "module": "dist/index.js",
16
+ "types": "index.d.ts",
18
17
  "exports": {
19
18
  ".": {
20
19
  "types": "./index.d.ts",
21
- "import": "./dist/index.module.js",
22
- "require": "./dist/index.cjs",
23
- "default": "./dist/index.module.js"
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs"
24
22
  }
25
23
  },
26
24
  "files": [
@@ -28,11 +26,31 @@
28
26
  "src",
29
27
  "index.d.ts"
30
28
  ],
29
+ "size-limit": [
30
+ {
31
+ "path": "dist/index.js",
32
+ "limit": "1.25 kB"
33
+ },
34
+ {
35
+ "path": "dist/index.cjs",
36
+ "limit": "1.25 kB"
37
+ }
38
+ ],
31
39
  "scripts": {
32
- "build": "microbundle --no-sourcemap",
40
+ "bench": "node bench/index.js",
41
+ "build": "tsdown",
42
+ "fuzz": "npm run fuzz:prepare && run-s fuzz:compile fuzz:render fuzz:structured",
43
+ "fuzz:prepare": "node -e \"for (const x of ['compile','render','structured']) require('fs').mkdirSync('.fuzz-corpus/'+x,{recursive:true})\"",
44
+ "fuzz:compile": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/compile.fuzz.js -i src/ --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/",
45
+ "fuzz:render": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/render.fuzz.js -i src/ --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/",
46
+ "fuzz:structured": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/structured.fuzz.js -i src/ --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/",
47
+ "fuzz:regression": "run-s fuzz:regression:compile fuzz:regression:render fuzz:regression:structured",
48
+ "fuzz:regression:compile": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/compile.fuzz.js -i src/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync --mode=regression fuzz/corpus/compile -- -artifact_prefix=fuzz/",
49
+ "fuzz:regression:render": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/render.fuzz.js -i src/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync --mode=regression fuzz/corpus/render -- -artifact_prefix=fuzz/",
50
+ "fuzz:regression:structured": "NODE_OPTIONS=--disallow-code-generation-from-strings jazzer fuzz/structured.fuzz.js -i src/ --customHooks fuzz/hooks.js --disableBugDetectors='command-injection|path-traversal|ssrf' --sync --mode=regression fuzz/corpus/structured -- -artifact_prefix=fuzz/",
33
51
  "prepublishOnly": "npm run build",
34
- "test": "node --disallow-code-generation-from-strings node_modules/tape/bin/tape test/*.test.js",
35
- "bench": "node bench/index.js"
52
+ "size": "size-limit",
53
+ "test": "node --disallow-code-generation-from-strings node_modules/tape/bin/tape test/*.test.js"
36
54
  },
37
55
  "keywords": [
38
56
  "template",
@@ -42,10 +60,15 @@
42
60
  "handlebars"
43
61
  ],
44
62
  "dependencies": {
45
- "xprsn": "^0.3.0"
63
+ "xprsn": "^0.4.1"
46
64
  },
47
65
  "devDependencies": {
48
- "microbundle": "^0.15.1",
49
- "tape": "^5.9.0"
66
+ "@jazzer.js/bug-detectors": "^4.0.0",
67
+ "@jazzer.js/core": "^4.0.0",
68
+ "@size-limit/file": "^12.1.0",
69
+ "npm-run-all": "^4.1.5",
70
+ "size-limit": "^12.1.0",
71
+ "tape": "^5.9.0",
72
+ "tsdown": "^0.22.12"
50
73
  }
51
74
  }
package/src/index.js CHANGED
@@ -7,16 +7,39 @@ import { compile } from 'xprsn';
7
7
 
8
8
  const ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
9
9
  const esc = s => String(s).replace(/[&<>"']/g, c => ESC[c]);
10
+ const BLOCKED = /^(?:__proto__|constructor|prototype)$/;
10
11
 
11
12
  // What `#each` walks: [value, key] pairs — array indexes or own object keys.
12
13
  const pairs = lv => Array.isArray(lv) ? lv.map((x, j) => [x, j])
13
14
  : lv && typeof lv === 'object' ? Object.keys(lv).map(k => [lv[k], k])
14
15
  : [];
15
16
 
16
- // Split into [text, rawL, raw, rawR, tagL, tag, tagR, ...] strides of 7.
17
- // The dash captures hug the braces, so `{{ -price }}` stays a unary minus
18
- // while `{{- price -}}` trims the whitespace touching the tag.
19
- const TAGS = /\{\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}\}|\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}/;
17
+ // Linear scan into text/tag/raw tokens. Dashes hug braces (`{{- x -}}` trims;
18
+ // `{{ -x }}` stays unary minus). Prefer {{{ }}} over {{ }}. `triple` latches
19
+ // off once }}} is gone so {{{...}}×N does not rescan to EOF (stays O(n)).
20
+ let lex = s => {
21
+ const out = [];
22
+ for (let i = 0, triple = 1; i < s.length; ) {
23
+ const a = s.indexOf('{{', i);
24
+ if (a < 0) { out.push({ text: s.slice(i) }); break; }
25
+ if (a > i) out.push({ text: s.slice(i, a) });
26
+ let raw = s.charCodeAt(a + 2) === 123, p = a + 2 + raw, l = s.charCodeAt(p) === 45, b = -1;
27
+ if (l) p++;
28
+ if (raw && triple) { b = s.indexOf('}}}', p); if (b < 0) triple = 0; }
29
+ if (b < 0) {
30
+ if (raw) { raw = !1; p = a + 2; l = s.charCodeAt(p) === 45; if (l) p++; }
31
+ b = s.indexOf('}}', p);
32
+ }
33
+ if (b < 0) { out.push({ text: s.slice(a) }); break; }
34
+ const r = b > p && s.charCodeAt(b - 1) === 45;
35
+ const body = s.slice(p, r ? b - 1 : b).trim(), t = raw ? { raw: body } : { tag: body };
36
+ t.l = l;
37
+ t.r = r;
38
+ out.push(t);
39
+ i = b + 2 + raw;
40
+ }
41
+ return out;
42
+ };
20
43
 
21
44
  // Shared parser state; parsing is synchronous so this is safe.
22
45
  // `nms` collects free variables, `fnms` the registry functions called.
@@ -75,6 +98,7 @@ let parse = stops => {
75
98
  } else if (t.tag.startsWith('#each ')) {
76
99
  const m = /^#each ([\s\S]+) as (\w+)(?:\s*,\s*(\w+))?$/.exec(t.tag) || err('Bad {{' + t.tag + '}}');
77
100
  const list = cp(m[1]), name = m[2], idx = m[3];
101
+ if (BLOCKED.test(name) || idx && BLOCKED.test(idx)) err('Bad {{' + t.tag + '}}');
78
102
  // `name`, `idx`, and `loop` are engine-bound inside the body, so
79
103
  // exclude them from names there and restore outer bindings after.
80
104
  const restore = scope(name, idx, 'loop');
@@ -120,9 +144,14 @@ let parse = stops => {
120
144
  * reach the root (`$.company`) or the current item (`@.total`) explicitly,
121
145
  * past any shadowing. Neither counts as a `name`.
122
146
  *
147
+ * An embedder with its own scope model can override the anchors per render by
148
+ * passing `{ root, item }` as the renderer's second argument: `$` becomes
149
+ * `root` and `@` becomes `item` (two distinct objects). Omit `item` to leave
150
+ * `@` unbound, so reading `@.x` throws through xprsn's guard.
151
+ *
123
152
  * @param {string} str The template, e.g. `'Hello {{ user.name }}!'`.
124
153
  * @param {Record<string, Function>} [funcs] Functions callable inside expressions.
125
- * @returns {{(values?: Record<string, any>): string, names: string[], functions: string[]}} Renderer for the compiled template.
154
+ * @returns {{(values?: Record<string, any>, scope?: { root?: any, item?: any }): string, names: string[], functions: string[]}} Renderer for the compiled template.
126
155
  * @throws {SyntaxError} On malformed tags, unclosed blocks, or bad expressions.
127
156
  */
128
157
  export function template(str, funcs) {
@@ -132,30 +161,26 @@ export function template(str, funcs) {
132
161
  bound = new Set(['$', '@']);
133
162
  nms = new Set();
134
163
  fnms = new Set();
135
- toks = [];
136
- const parts = String(str).split(TAGS);
137
- for (let j = 0; j < parts.length; j += 7) {
138
- if (parts[j]) toks.push({ text: parts[j] });
139
- if (j + 1 >= parts.length) break;
140
- const raw = parts[j + 2] != null;
141
- const t = raw ? { raw: parts[j + 2] } : { tag: parts[j + 5] };
142
- t.l = parts[j + (raw ? 1 : 4)] === '-';
143
- t.r = parts[j + (raw ? 3 : 6)] === '-';
144
- toks.push(t);
145
- }
164
+ toks = lex(String(str));
146
165
  // `{{-` / `-}}` eat the whitespace touching that side of the tag.
147
166
  toks.forEach((t, k) => {
148
- if (t.l && toks[k - 1]?.text) toks[k - 1].text = toks[k - 1].text.replace(/\s+$/, '');
149
- if (t.r && toks[k + 1]?.text) toks[k + 1].text = toks[k + 1].text.replace(/^\s+/, '');
167
+ if (t.l && toks[k - 1]?.text) toks[k - 1].text = toks[k - 1].text.trimEnd();
168
+ if (t.r && toks[k + 1]?.text) toks[k + 1].text = toks[k + 1].text.trimStart();
150
169
  });
151
170
  i = 0;
152
171
  const nodes = parse([]);
153
172
  // Wrap the values in a root scope carrying the anchors, without mutating
154
- // what the caller passed: `$` and `@` both point at the root here.
155
- const f = v => {
173
+ // what the caller passed: by default `$` and `@` both point at the root.
174
+ // An embedder can override the anchors with a `{ root, item }` second arg:
175
+ // `$` = root, `@` = item (distinct objects). Omitting `item` leaves `@`
176
+ // unbound, so `@.x` throws through xprsn's guard — a group-header band that
177
+ // has no current row wants exactly that.
178
+ const f = (v, o) => {
156
179
  v = v || {};
157
180
  const r = Object.create(v);
158
- r['$'] = r['@'] = v;
181
+ r['$'] = o ? o.root : v;
182
+ if (!o) r['@'] = v;
183
+ else if ('item' in o) r['@'] = o.item;
159
184
  return run(nodes, r);
160
185
  };
161
186
  // Array.from, not a spread: the bundler's transpile turns `[...set]` into
@@ -1 +0,0 @@
1
- import{compile as t}from"xprsn";function r(t,r){(null==r||r>t.length)&&(r=t.length);for(var n=0,e=Array(r);n<r;n++)e[n]=t[n];return e}function n(t,n){var e="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(e)return(e=e.call(t)).next.bind(e);if(Array.isArray(t)||(e=function(t,n){if(t){if("string"==typeof t)return r(t,n);var e={}.toString.call(t).slice(8,-1);return"Object"===e&&t.constructor&&(e=t.constructor.name),"Map"===e||"Set"===e?Array.from(t):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?r(t,n):void 0}}(t))||n&&t&&"number"==typeof t.length){e&&(t=e);var a=0;return function(){return a>=t.length?{done:!0}:{done:!1,value:t[a++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var e,a,u,i,o,l,f,c={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},s=function(t){return String(t).replace(/[&<>"']/g,function(t){return c[t]})},h=/\{\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}\}|\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}/,g=function(t){throw SyntaxError(t)},p=function(t,r){return t.map(function(t){return t(r)}).join("")},v=function(t,r){return n=d(t),function(t){var e;return r(null!=(e=n(t))?e:"")};var n},d=function(r){for(var e,a=t(r,u),i=n(a.names);!(e=i()).done;){var c=e.value;o.has(c)||l.add(c)}for(var s,h=n(a.functions);!(s=h()).done;)f.add(s.value);return a},m=function(t){var r=y(["#elif","#else","/if"]),n=i.startsWith("#elif ")?[m(d(i.slice(6)))]:"#else"===i?y(["/if"]):[];return function(e){return p(t(e)?r:n,e)}},y=function(t){for(var r,n,u=[],l=function(){if(null!=n.text)u.push((x=n.text,function(){return x}));else if(null!=n.raw)u.push(v(n.raw,String));else{if(t.includes(n.tag.split(" ")[0]))return i=n.tag,{v:u};if("!"===n.tag[0]);else if(n.tag.startsWith("#if "))u.push(m(d(n.tag.slice(4))));else if(n.tag.startsWith("#each ")){var r=/^#each ([\s\S]+) as (\w+)(?:\s*,\s*(\w+))?$/.exec(n.tag)||g("Bad {{"+n.tag+"}}"),e=d(r[1]),a=r[2],l=r[3],f=function(){var t=[].slice.call(arguments).filter(function(t){return t&&!o.has(t)});return t.forEach(function(t){return o.add(t)}),function(){return t.forEach(function(t){return o.delete(t)})}}(a,l,"loop"),c=y(["#else","/each"]);f();var h="#else"===i?y(["/each"]):[];u.push(function(t){var r,n=(r=e(t),Array.isArray(r)?r.map(function(t,r){return[t,r]}):r&&"object"==typeof r?Object.keys(r).map(function(t){return[r[t],t]}):[]);return n.length?n.map(function(r,e){var u=r[0],i=r[1],o=Object.create(t);return o[a]=u,l&&(o[l]=i),o["@"]=u,o.loop={index:e+1,index0:e,first:!e,last:e===n.length-1,length:n.length},p(c,o)}).join(""):p(h,t)})}else"#"===n.tag[0]||"/"===n.tag[0]?g("Unexpected {{"+n.tag+"}}"):u.push(v(n.tag,s))}var x};n=e[a++];)if(r=l())return r.v;return t.length&&g("Missing {{"+t[t.length-1]+"}}"),u};function x(t,r){u=r,o=new Set(["$","@"]),l=new Set,f=new Set,e=[];for(var n=String(t).split(h),i=0;i<n.length&&(n[i]&&e.push({text:n[i]}),!(i+1>=n.length));i+=7){var c=null!=n[i+2],s=c?{raw:n[i+2]}:{tag:n[i+5]};s.l="-"===n[i+(c?1:4)],s.r="-"===n[i+(c?3:6)],e.push(s)}e.forEach(function(t,r){var n,a;t.l&&null!=(n=e[r-1])&&n.text&&(e[r-1].text=e[r-1].text.replace(/\s+$/,"")),t.r&&null!=(a=e[r+1])&&a.text&&(e[r+1].text=e[r+1].text.replace(/^\s+/,""))}),a=0;var g=y([]),v=function(t){t=t||{};var r=Object.create(t);return r.$=r["@"]=t,p(g,r)};return v.names=Array.from(l),v.functions=Array.from(f),v}function S(t,r,n){return x(t,n)(r)}export{S as render,x as template};
package/dist/index.umd.js DELETED
@@ -1 +0,0 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("xprsn")):"function"==typeof define&&define.amd?define(["exports","xprsn"],e):e((t||self).sjabloon={},t.xprsn)}(this,function(t,e){function n(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function r(t,e){var r="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(r)return(r=r.call(t)).next.bind(r);if(Array.isArray(t)||(r=function(t,e){if(t){if("string"==typeof t)return n(t,e);var r={}.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?n(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){r&&(t=r);var a=0;return function(){return a>=t.length?{done:!0}:{done:!1,value:t[a++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o,i,u,l,f,s,c={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},p=function(t){return String(t).replace(/[&<>"']/g,function(t){return c[t]})},h=/\{\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}\}|\{\{(-)?\s*([\s\S]*?)\s*(-)?\}\}/,g=function(t){throw SyntaxError(t)},d=function(t,e){return t.map(function(t){return t(e)}).join("")},v=function(t,e){return n=m(t),function(t){var r;return e(null!=(r=n(t))?r:"")};var n},m=function(t){for(var n,a=e.compile(t,i),o=r(a.names);!(n=o()).done;){var u=n.value;l.has(u)||f.add(u)}for(var c,p=r(a.functions);!(c=p()).done;)s.add(c.value);return a},y=function(t){var e=x(["#elif","#else","/if"]),n=u.startsWith("#elif ")?[y(m(u.slice(6)))]:"#else"===u?x(["/if"]):[];return function(r){return d(t(r)?e:n,r)}},x=function(t){for(var e,n,r=[],i=function(){if(null!=n.text)r.push((h=n.text,function(){return h}));else if(null!=n.raw)r.push(v(n.raw,String));else{if(t.includes(n.tag.split(" ")[0]))return u=n.tag,{v:r};if("!"===n.tag[0]);else if(n.tag.startsWith("#if "))r.push(y(m(n.tag.slice(4))));else if(n.tag.startsWith("#each ")){var e=/^#each ([\s\S]+) as (\w+)(?:\s*,\s*(\w+))?$/.exec(n.tag)||g("Bad {{"+n.tag+"}}"),a=m(e[1]),o=e[2],i=e[3],f=function(){var t=[].slice.call(arguments).filter(function(t){return t&&!l.has(t)});return t.forEach(function(t){return l.add(t)}),function(){return t.forEach(function(t){return l.delete(t)})}}(o,i,"loop"),s=x(["#else","/each"]);f();var c="#else"===u?x(["/each"]):[];r.push(function(t){var e,n=(e=a(t),Array.isArray(e)?e.map(function(t,e){return[t,e]}):e&&"object"==typeof e?Object.keys(e).map(function(t){return[e[t],t]}):[]);return n.length?n.map(function(e,r){var a=e[0],u=e[1],l=Object.create(t);return l[o]=a,i&&(l[i]=u),l["@"]=a,l.loop={index:r+1,index0:r,first:!r,last:r===n.length-1,length:n.length},d(s,l)}).join(""):d(c,t)})}else"#"===n.tag[0]||"/"===n.tag[0]?g("Unexpected {{"+n.tag+"}}"):r.push(v(n.tag,p))}var h};n=a[o++];)if(e=i())return e.v;return t.length&&g("Missing {{"+t[t.length-1]+"}}"),r};function b(t,e){i=e,l=new Set(["$","@"]),f=new Set,s=new Set,a=[];for(var n=String(t).split(h),r=0;r<n.length&&(n[r]&&a.push({text:n[r]}),!(r+1>=n.length));r+=7){var u=null!=n[r+2],c=u?{raw:n[r+2]}:{tag:n[r+5]};c.l="-"===n[r+(u?1:4)],c.r="-"===n[r+(u?3:6)],a.push(c)}a.forEach(function(t,e){var n,r;t.l&&null!=(n=a[e-1])&&n.text&&(a[e-1].text=a[e-1].text.replace(/\s+$/,"")),t.r&&null!=(r=a[e+1])&&r.text&&(a[e+1].text=a[e+1].text.replace(/^\s+/,""))}),o=0;var p=x([]),g=function(t){t=t||{};var e=Object.create(t);return e.$=e["@"]=t,d(p,e)};return g.names=Array.from(f),g.functions=Array.from(s),g}t.render=function(t,e,n){return b(t,n)(e)},t.template=b});