sjabloon 0.5.0 → 0.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/README.md CHANGED
@@ -68,7 +68,7 @@ Sjabloon errors keep their native `SyntaxError` or `TypeError` class and expose:
68
68
  - `end`: the exclusive template offset;
69
69
  - `blocks`: a frozen, outermost-first array of `{ type, start, end }` opener spans.
70
70
 
71
- Parser codes are `SJABLOON_EACH_SYNTAX`, `SJABLOON_BLOCKED_BINDING`, `SJABLOON_UNEXPECTED_TAG`, `SJABLOON_UNKNOWN_BLOCK`, and `SJABLOON_UNCLOSED_BLOCK`. A missing closer uses an empty span at the end of the template. Expression offsets refer to the original template, so surrounding braces, whitespace, and trim markers contribute to their absolute position.
71
+ Parser codes are `SJABLOON_EACH_SYNTAX`, `SJABLOON_BLOCKED_BINDING`, `SJABLOON_UNEXPECTED_TAG`, `SJABLOON_UNKNOWN_BLOCK`, `SJABLOON_UNCLOSED_BLOCK`, and `SJABLOON_TOO_DEEP` (block nesting past 256 levels, located at the opener that crossed the cap). A missing closer uses an empty span at the end of the template. Expression offsets refer to the original template, so surrounding braces, whitespace, and trim markers contribute to their absolute position.
72
72
 
73
73
  Unauthenticated errors thrown by registered functions, getters, methods, or value coercion hooks are host errors. Sjabloon passes them through unchanged and does not attach template diagnostic fields.
74
74
 
@@ -115,13 +115,18 @@ Here the loop variable `company` shadows the root's for a bare name, but `$.comp
115
115
 
116
116
  sjabloon works under `script-src 'self'` with no `unsafe-eval`. Templates parse into a tree of closures that call other closures; xprsn compiles the expressions the same way. The test suite runs under `node --disallow-code-generation-from-strings`, which throws on any string-to-code construct exactly like a strict CSP does.
117
117
 
118
- This is the practical difference from engines like Handlebars (without precompilation) or tempura, which generate a JavaScript function per template and therefore need `unsafe-eval` at runtime. If you can precompile templates at build time, those engines are great and fast. If templates arrive at runtime (user-edited templates, CMS content, email templates) and your CSP is strict, sjabloon fits.
118
+ That runtime CSP support costs some render speed. Handlebars and tempura generate specialized JavaScript, so their compiled renderers are faster but runtime compilation requires `unsafe-eval`. Build-time precompilation avoids that restriction when templates are known in advance. If templates arrive at runtime (user-edited templates, CMS content, email templates) and your CSP is strict, sjabloon fits. See the [comparison benchmarks](bench/comparison/) for cold-compile and hot-render comparisons.
119
119
 
120
120
  ## Safety
121
121
 
122
122
  - `{{ expr }}` escapes `& < > " '` by default; unescaped output requires the explicit `{{{ }}}` form.
123
123
  - Expressions inherit all of xprsn's guards: no `__proto__`/`constructor`/`prototype` access, null-prototype hash literals, and functions resolved only from your registry.
124
124
  - Templates read your values; they cannot assign to them.
125
+ - Registered functions are host-provided capabilities, not a sandbox boundary. Only register helpers that template authors are allowed to invoke; likewise, treat explicit raw output as trusted HTML.
126
+
127
+ ## Environments
128
+
129
+ Node.js 22 and newer are supported through the ESM and CommonJS builds. Browser use is supported through a standards-based ESM bundler in environments supporting ES2024. Direct `<script>` globals and UMD builds are not provided.
125
130
 
126
131
  ## License
127
132
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
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=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,o,u,p,p+f.length,s,l],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,_,v=()=>Object.freeze(_.slice()),y=(e,t)=>Object.freeze({type:e,start:t[2],end:t[3]}),b=(e,t)=>(Object.defineProperty(e,"blocks",{value:t,enumerable:!0}),a(e),e),x=(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,b(a,v())},S=(t,n,r,i=e.isDiagnostic)=>{throw i(t)?(t.start+=n,t.end+=n,b(t,r)):t},C=e=>x(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),w=(e,t)=>e.map(e=>e(t)).join(``),T=(e,t)=>(e=>n=>t(e(n)??``))(E(e[1],e[6],v())),E=(t,n,r)=>{let i;try{i=(0,e.compile)(t,d)}catch(e){S(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){S(e,n,r,i.isDiagnostic)}}},D=e=>{let t=O([`#elif`,`#else`,`/if`]),n=f[1],r=[];return n.startsWith(`#elif `)?r=[D(E(n.slice(6),f[6]+6,v()))]:n===`#else`?(r=O([`/if`]),f[1]===`/if`||C(f)):n!==`/if`&&C(f),n=>w(e(n)?t:r,n)},O=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(T(i,String));else if(e.includes(a.split(` `)[0]))return f=i,t;else if(a[0]!==`!`)if(a.startsWith(`#if `))_.push(y(`if`,i)),t.push(D(E(a.slice(4),i[6]+4,v()))),_.pop();else if(/^#each(?:\s|$)/.test(a)){_.push(y(`each`,i));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(a);e||x(`Bad {{`+a+`}}`,`SJABLOON_EACH_SYNTAX`,i);let n=e[3],o=e[4],s=i[6]+a.length-e[2].length;if(r.test(n)&&x(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,s,s+n.length),o&&r.test(o)){let e=i[6]+a.length-o.length;x(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,e,e+o.length)}let c=E(e[1],i[6]+6,v()),l=p.length;p.push(n),o&&p.push(o),p.push(`loop`);let u=O([`#else`,`/each`]);p.length=l;let d=[];f[1]===`#else`?(d=O([`/each`]),f[1]===`/each`||C(f)):f[1]!==`/each`&&C(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},w(u,l)}).join(``):w(d,e)})}else/^#(?:if|elif|else)(?:\s|$)/.test(a)||a[0]===`/`?C(i):a[0]===`#`?x(`Unknown {{`+a+`}}`,`SJABLOON_UNKNOWN_BLOCK`,i):t.push(T(i,n))}return e.length&&x(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t};function k(e,t){d=t,p=[`$`,`@`],m=new Set,h=new Set,g=String(e),_=[],l=c(g),u=0;let n=O([]),r=(e,t)=>{e||={};let r=Object.create(e);return r.$=t?t.root:e,t?`item`in t&&(r[`@`]=t.item):r[`@`]=e,w(n,r)};return r.names=Array.from(m),r.functions=Array.from(h),r}function A(e,t,n){return k(e,n)(t)}exports.isDiagnostic=s,exports.render=A,exports.template=k;
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=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,_,v=()=>Object.freeze(_.slice()),y=(e,t)=>(_.length<256||x(`Template too deeply nested`,`SJABLOON_TOO_DEEP`,t),Object.freeze({type:e,start:t[2],end:t[3]})),b=(e,t)=>(Object.defineProperty(e,"blocks",{value:t,enumerable:!0}),a(e),e),x=(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,b(a,v())},S=(t,n,r,i=e.isDiagnostic)=>{throw i(t)?(t.start+=n,t.end+=n,b(t,r)):t},C=e=>x(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),w=(e,t)=>e.map(e=>e(t)).join(``),T=(e,t)=>(e=>n=>t(e(n)??``))(E(e[1],e[4],v())),E=(t,n,r)=>{let i;try{i=(0,e.compile)(t,d)}catch(e){S(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){S(e,n,r,i.isDiagnostic)}}},D=e=>{let t=O([`#elif`,`#else`,`/if`]),n=f[1],r=[];return n.startsWith(`#elif `)?r=[D(E(n.slice(6),f[4]+6,v()))]:n===`#else`?(r=O([`/if`]),f[1]===`/if`||C(f)):n!==`/if`&&C(f),n=>w(e(n)?t:r,n)},O=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(T(i,String));else if(e.includes(a.split(` `)[0]))return f=i,t;else if(a[0]!==`!`)if(a.startsWith(`#if `))_.push(y(`if`,i)),t.push(D(E(a.slice(4),i[4]+4,v()))),_.pop();else if(/^#each(?:\s|$)/.test(a)){_.push(y(`each`,i));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(a);e||x(`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)&&x(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,s,s+n.length),o&&r.test(o)){let e=i[4]+a.length-o.length;x(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,i,e,e+o.length)}let c=E(e[1],i[4]+6,v()),l=p.length;p.push(n),o&&p.push(o),p.push(`loop`);let u=O([`#else`,`/each`]);p.length=l;let d=[];f[1]===`#else`?(d=O([`/each`]),f[1]===`/each`||C(f)):f[1]!==`/each`&&C(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},w(u,l)}).join(``):w(d,e)})}else/^#(?:if|elif|else)(?:\s|$)/.test(a)||a[0]===`/`?C(i):a[0]===`#`?x(`Unknown {{`+a+`}}`,`SJABLOON_UNKNOWN_BLOCK`,i):t.push(T(i,n))}return e.length&&x(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t};function k(e,t){d=t,p=[`$`,`@`],m=new Set,h=new Set,g=String(e),_=[],l=c(g),u=0;let n;try{n=O([])}catch(e){throw e instanceof RangeError&&x(`Template too deeply nested`,`SJABLOON_TOO_DEEP`),e}let r=(e,t)=>{e||={};let r=Object.create(e);return r.$=t?t.root:e,t?`item`in t&&(r[`@`]=t.item):r[`@`]=e,w(n,r)};return r.names=Array.from(m),r.functions=Array.from(h),r}function A(e,t,n){return k(e,n)(t)}exports.isDiagnostic=s,exports.render=A,exports.template=k;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{compile as e,isDiagnostic as t}from"xprsn";const n={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`},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,o,u,p,p+f.length,s,l],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,y=()=>Object.freeze(v.slice()),b=(e,t)=>Object.freeze({type:e,start:t[2],end:t[3]}),x=(e,t)=>(Object.defineProperty(e,"blocks",{value:t,enumerable:!0}),o(e),e),S=(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,x(a,y())},C=(e,n,r,i=t)=>{throw i(e)?(e.start+=n,e.end+=n,x(e,r)):e},w=e=>S(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),T=(e,t)=>e.map(e=>e(t)).join(``),E=(e,t)=>(e=>n=>t(e(n)??``))(D(e[1],e[6],y())),D=(t,n,r)=>{let i;try{i=e(t,f)}catch(e){C(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){C(e,n,r,i.isDiagnostic)}}},O=e=>{let t=k([`#elif`,`#else`,`/if`]),n=p[1],r=[];return n.startsWith(`#elif `)?r=[O(D(n.slice(6),p[6]+6,y()))]:n===`#else`?(r=k([`/if`]),p[1]===`/if`||w(p)):n!==`/if`&&w(p),n=>T(e(n)?t:r,n)},k=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(E(n,String));else if(e.includes(a.split(` `)[0]))return p=n,t;else if(a[0]!==`!`)if(a.startsWith(`#if `))v.push(b(`if`,n)),t.push(O(D(a.slice(4),n[6]+4,y()))),v.pop();else if(/^#each(?:\s|$)/.test(a)){v.push(b(`each`,n));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(a);e||S(`Bad {{`+a+`}}`,`SJABLOON_EACH_SYNTAX`,n);let r=e[3],o=e[4],s=n[6]+a.length-e[2].length;if(i.test(r)&&S(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,n,s,s+r.length),o&&i.test(o)){let e=n[6]+a.length-o.length;S(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,n,e,e+o.length)}let c=D(e[1],n[6]+6,y()),l=m.length;m.push(r),o&&m.push(o),m.push(`loop`);let u=k([`#else`,`/each`]);m.length=l;let d=[];p[1]===`#else`?(d=k([`/each`]),p[1]===`/each`||w(p)):p[1]!==`/each`&&w(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},T(u,l)}).join(``):T(d,e)})}else/^#(?:if|elif|else)(?:\s|$)/.test(a)||a[0]===`/`?w(n):a[0]===`#`?S(`Unknown {{`+a+`}}`,`SJABLOON_UNKNOWN_BLOCK`,n):t.push(E(n,r))}return e.length&&S(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t};function A(e,t){f=t,m=[`$`,`@`],h=new Set,g=new Set,_=String(e),v=[],u=l(_),d=0;let n=k([]),r=(e,t)=>{e||={};let r=Object.create(e);return r.$=t?t.root:e,t?`item`in t&&(r[`@`]=t.item):r[`@`]=e,T(n,r)};return r.names=Array.from(h),r.functions=Array.from(g),r}function j(e,t,n){return A(e,n)(t)}export{c as isDiagnostic,j as render,A as template};
1
+ import{compile as e,isDiagnostic as t}from"xprsn";const n={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`},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,y=()=>Object.freeze(v.slice()),b=(e,t)=>(v.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}),o(e),e),S=(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,x(a,y())},C=(e,n,r,i=t)=>{throw i(e)?(e.start+=n,e.end+=n,x(e,r)):e},w=e=>S(`Unexpected {{`+e[1]+`}}`,`SJABLOON_UNEXPECTED_TAG`,e),T=(e,t)=>e.map(e=>e(t)).join(``),E=(e,t)=>(e=>n=>t(e(n)??``))(D(e[1],e[4],y())),D=(t,n,r)=>{let i;try{i=e(t,f)}catch(e){C(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){C(e,n,r,i.isDiagnostic)}}},O=e=>{let t=k([`#elif`,`#else`,`/if`]),n=p[1],r=[];return n.startsWith(`#elif `)?r=[O(D(n.slice(6),p[4]+6,y()))]:n===`#else`?(r=k([`/if`]),p[1]===`/if`||w(p)):n!==`/if`&&w(p),n=>T(e(n)?t:r,n)},k=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(E(n,String));else if(e.includes(a.split(` `)[0]))return p=n,t;else if(a[0]!==`!`)if(a.startsWith(`#if `))v.push(b(`if`,n)),t.push(O(D(a.slice(4),n[4]+4,y()))),v.pop();else if(/^#each(?:\s|$)/.test(a)){v.push(b(`each`,n));let e=/^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(a);e||S(`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)&&S(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,n,s,s+r.length),o&&i.test(o)){let e=n[4]+a.length-o.length;S(`Bad {{`+a+`}}`,`SJABLOON_BLOCKED_BINDING`,n,e,e+o.length)}let c=D(e[1],n[4]+6,y()),l=m.length;m.push(r),o&&m.push(o),m.push(`loop`);let u=k([`#else`,`/each`]);m.length=l;let d=[];p[1]===`#else`?(d=k([`/each`]),p[1]===`/each`||w(p)):p[1]!==`/each`&&w(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},T(u,l)}).join(``):T(d,e)})}else/^#(?:if|elif|else)(?:\s|$)/.test(a)||a[0]===`/`?w(n):a[0]===`#`?S(`Unknown {{`+a+`}}`,`SJABLOON_UNKNOWN_BLOCK`,n):t.push(E(n,r))}return e.length&&S(`Missing {{`+e[e.length-1]+`}}`,`SJABLOON_UNCLOSED_BLOCK`),t};function A(e,t){f=t,m=[`$`,`@`],h=new Set,g=new Set,_=String(e),v=[],u=l(_),d=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)=>{e||={};let r=Object.create(e);return r.$=t?t.root:e,t?`item`in t&&(r[`@`]=t.item):r[`@`]=e,T(n,r)};return r.names=Array.from(h),r.functions=Array.from(g),r}function j(e,t,n){return A(e,n)(t)}export{c as isDiagnostic,j as render,A as template};
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "sjabloon",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Tiny, CSP-safe template engine for JavaScript, powered by xprsn expressions. No eval, no new Function.",
5
- "repository": "robinvdvleuten/sjabloon",
5
+ "repository": "getquario/sjabloon",
6
6
  "license": "MIT",
7
7
  "author": {
8
8
  "name": "Robin van der Vleuten",
@@ -21,6 +21,9 @@
21
21
  "require": "./dist/index.cjs"
22
22
  }
23
23
  },
24
+ "engines": {
25
+ "node": ">=22.0.0"
26
+ },
24
27
  "files": [
25
28
  "dist",
26
29
  "src",
@@ -33,12 +36,14 @@
33
36
  },
34
37
  {
35
38
  "path": "dist/index.cjs",
36
- "limit": "1.7 kB"
39
+ "limit": "1.72 kB"
37
40
  }
38
41
  ],
39
42
  "scripts": {
40
43
  "bench": "node --disallow-code-generation-from-strings bench/index.js",
44
+ "bench:comparison": "npm run build && npm --prefix bench/comparison run bench",
41
45
  "build": "tsdown",
46
+ "check": "run-s build size test fuzz:regression test:browser",
42
47
  "fuzz": "npm run fuzz:prepare && run-s fuzz:compile fuzz:render fuzz:structured",
43
48
  "fuzz:prepare": "node -e \"for (const x of ['compile','render','structured']) require('fs').mkdirSync('.fuzz-corpus/'+x,{recursive:true})\"",
44
49
  "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/",
@@ -50,8 +55,10 @@
50
55
  "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/",
51
56
  "prepublishOnly": "npm run build",
52
57
  "size": "size-limit",
53
- "test": "node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js && npm run typecheck",
54
- "typecheck": "tsc --noEmit --strict test/types.check.ts"
58
+ "test": "run-s test:unit test:types",
59
+ "test:unit": "node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js",
60
+ "test:browser": "npm run build && playwright install chromium && node test/browser/harness.js",
61
+ "test:types": "tsc"
55
62
  },
56
63
  "keywords": [
57
64
  "template",
@@ -68,6 +75,7 @@
68
75
  "@jazzer.js/core": "^4.0.0",
69
76
  "@size-limit/file": "^12.1.0",
70
77
  "npm-run-all": "^4.1.5",
78
+ "playwright": "^1.61.1",
71
79
  "size-limit": "^12.1.0",
72
80
  "tsdown": "^0.22.12",
73
81
  "typescript": "^7.0.2"
package/src/index.js CHANGED
@@ -40,7 +40,7 @@ let lex = s => {
40
40
  const r = b > p && s[b - 1] === '-';
41
41
  const q = r ? b - 1 : b, whole = s.slice(p, q), body = whole.trim();
42
42
  const start = p + whole.length - whole.trimStart().length, end = b + 2 + raw;
43
- const t = [raw ? 1 : 2, body, a, end, p, q, start, start + body.length, l, r];
43
+ const t = [raw ? 1 : 2, body, a, end, start];
44
44
  const prev = out.at(-1);
45
45
  if (l && prev?.[0] === 0 && prev[1]) prev[1] = prev[1].trimEnd();
46
46
  out.push(t);
@@ -55,7 +55,13 @@ let lex = s => {
55
55
  let toks, i, fns, last, bound, nms, fnms, src, blocks;
56
56
 
57
57
  let snap = () => Object.freeze(blocks.slice());
58
- let opener = (type, t) => Object.freeze({ type, start: t[2], end: t[3] });
58
+ // Block nesting is capped so a pathological template fails as a deterministic
59
+ // SyntaxError at the offending opener, far below the native stack limit.
60
+ const DEPTH = 256;
61
+ let opener = (type, t) => {
62
+ blocks.length < DEPTH || fault('Template too deeply nested', 'SJABLOON_TOO_DEEP', t);
63
+ return Object.freeze({ type, start: t[2], end: t[3] });
64
+ };
59
65
  let attach = (e, context) => {
60
66
  Object.defineProperty(e, 'blocks', { value: context, enumerable: true });
61
67
  mark(e);
@@ -81,7 +87,7 @@ let run = (nodes, v) => nodes.map(n => n(v)).join('');
81
87
 
82
88
  // A leaf interpolation node: compile `src`, render nullish as '', apply `wrap`
83
89
  // (`esc` for `{{ }}`, `String` for the raw `{{{ }}}` form).
84
- let interp = (t, wrap) => (e => v => wrap(e(v) ?? ''))(cp(t[1], t[6], snap()));
90
+ let interp = (t, wrap) => (e => v => wrap(e(v) ?? ''))(cp(t[1], t[4], snap()));
85
91
 
86
92
  // Compile one expression and collect its free variables (minus the loop
87
93
  // variables currently in scope, which belong to the template) and the registry
@@ -109,7 +115,7 @@ let branch = cond => {
109
115
  const then = parse(['#elif', '#else', '/if']);
110
116
  const tag = last[1];
111
117
  let els = [];
112
- if (tag.startsWith('#elif ')) els = [branch(cp(tag.slice(6), last[6] + 6, snap()))];
118
+ if (tag.startsWith('#elif ')) els = [branch(cp(tag.slice(6), last[4] + 6, snap()))];
113
119
  else if (tag === '#else') {
114
120
  els = parse(['/if']);
115
121
  last[1] === '/if' || unexpected(last);
@@ -132,19 +138,19 @@ let parse = stops => {
132
138
  // comment
133
139
  } else if (tag.startsWith('#if ')) {
134
140
  blocks.push(opener('if', t));
135
- nodes.push(branch(cp(tag.slice(4), t[6] + 4, snap())));
141
+ nodes.push(branch(cp(tag.slice(4), t[4] + 4, snap())));
136
142
  blocks.pop();
137
143
  } else if (/^#each(?:\s|$)/.test(tag)) {
138
144
  blocks.push(opener('each', t));
139
145
  const m = /^#each ([\s\S]+) as ((\w+)(?:\s*,\s*(\w+))?)$/.exec(tag);
140
146
  m || fault('Bad {{' + tag + '}}', 'SJABLOON_EACH_SYNTAX', t);
141
- const name = m[3], idx = m[4], at = t[6] + tag.length - m[2].length;
147
+ const name = m[3], idx = m[4], at = t[4] + tag.length - m[2].length;
142
148
  if (BLOCKED.test(name)) fault('Bad {{' + tag + '}}', 'SJABLOON_BLOCKED_BINDING', t, at, at + name.length);
143
149
  if (idx && BLOCKED.test(idx)) {
144
- const p = t[6] + tag.length - idx.length;
150
+ const p = t[4] + tag.length - idx.length;
145
151
  fault('Bad {{' + tag + '}}', 'SJABLOON_BLOCKED_BINDING', t, p, p + idx.length);
146
152
  }
147
- const list = cp(m[1], t[6] + 6, snap());
153
+ const list = cp(m[1], t[4] + 6, snap());
148
154
  // `name`, `idx`, and `loop` are engine-bound inside the body, so
149
155
  // exclude them from names there and restore outer bindings after.
150
156
  const mark = bound.length;
@@ -223,7 +229,17 @@ export function template(str, funcs) {
223
229
  blocks = [];
224
230
  toks = lex(src);
225
231
  i = 0;
226
- const nodes = parse([]);
232
+ // Deeply nested blocks overflow the recursive-descent parser; surface that
233
+ // as a SyntaxError so malformed input keeps its documented compile-time
234
+ // contract (mirroring xprsn's XPRSN_TOO_DEEP for expressions).
235
+ let nodes;
236
+ try {
237
+ nodes = parse([]);
238
+ } catch (x) {
239
+ // An empty span at the end, like an unclosed block.
240
+ if (x instanceof RangeError) fault('Template too deeply nested', 'SJABLOON_TOO_DEEP');
241
+ throw x;
242
+ }
227
243
  // Wrap the values in a root scope carrying the anchors, without mutating
228
244
  // what the caller passed: by default `$` and `@` both point at the root.
229
245
  // An embedder can override the anchors with a `{ root, item }` second arg: