@murumets-ee/tokens 0.109.0 → 0.111.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/dist/index.d.mts CHANGED
@@ -258,4 +258,3 @@ declare function createTokenResolver(deps?: TokenResolverDeps): TokenResolver;
258
258
  declare function resolveTokens(text: string, context: TokenContext, options?: ResolveOptions): string;
259
259
  //#endregion
260
260
  export { type AnyTokenProvider, BUILT_IN_FILTERS, DEFAULT_TOKEN_LOCALE, type FilterRegistry, type MissingPolicy, type ParsedToken, RESERVED_FILTER_NAMES, type ResolveOptions, type ScanLimits, type ScanResult, type ScannedToken, type TokenChainValue, type TokenContext, type TokenDef, TokenExpansionLimitError, type TokenFilter, type TokenFilterContext, type TokenKind, type TokenOutputMode, type TokenProvider, type TokenProviderRegistry, TokenResolveError, type TokenResolveResult, type TokenResolver, type TokenResolverDeps, TokenSyntaxError, type TokenValue, buildFilterRegistry, buildTokenProviderRegistry, createTokenResolver, defineTokenProvider, escapeHtml, resolveTokens, safeUrl, scanTokens };
261
- //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1 @@
1
1
  import{i as e,n as t,r as n,t as r}from"./parse-D_0FL4H7.mjs";const i={"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`};function a(e){return e.replace(/[&<>"']/g,e=>i[e]??e)}function o(e){if(!e||/^[\s\\/]*[\\/]{2,}/.test(e))return`#`;let t;try{t=new URL(e)}catch{return e.startsWith(`/`)||e.startsWith(`#`)||e.startsWith(`?`)?e:`#`}let n=t.protocol.toLowerCase();return n===`http:`||n===`https:`||n===`mailto:`?t.toString():`#`}const s=`en`,c={mode:`text`,missing:`leave`,maxDepth:4,maxExpansions:500,strict:!1};function l(e){return e.locale??`en`}const u=`[A-Za-z_]\\w*`,d=RegExp(`^${u}(?::${u})*$`),f=RegExp(`^${u}$`);function p(e){if(e instanceof Date)return Number.isNaN(e.getTime())?null:e;if(typeof e==`number`){let t=new Date(e);return Number.isNaN(t.getTime())?null:t}if(typeof e==`string`&&e.trim()!==``){let t=new Date(e);return Number.isNaN(t.getTime())?null:t}return null}function m(e){if(typeof e==`number`)return Number.isFinite(e)?e:null;if(typeof e==`string`&&e.trim()!==``){let t=Number(e);return Number.isFinite(t)?t:null}return null}const h=[`full`,`long`,`medium`,`short`];function g(e,t){return e===void 0?t:h.find(t=>t===e.toLowerCase())??t}function _(e,t){if(e===void 0)return t;let n=Number.parseInt(e,10);return!Number.isFinite(n)||n<0?t:n}const v=Object.freeze({date:(e,t,n)=>{let r=p(e);return r===null?e:new Intl.DateTimeFormat(l(n),{dateStyle:g(t[0],`medium`)}).format(r)},time:(e,t,n)=>{let r=p(e);return r===null?e:new Intl.DateTimeFormat(l(n),{timeStyle:g(t[0],`short`)}).format(r)},number:(e,t,n)=>{let r=m(e);if(r===null)return e;let i=t[0];if(i===void 0)return new Intl.NumberFormat(l(n)).format(r);let a=Math.min(_(i,0),20);return new Intl.NumberFormat(l(n),{minimumFractionDigits:a,maximumFractionDigits:a}).format(r)},currency:(e,t,n)=>{let r=t[0];if(r===void 0||r===``)return e;let i=m(e);if(i===null)return e;let a=Number.isInteger(i)?i/100:i;return new Intl.NumberFormat(l(n),{style:`currency`,currency:r}).format(a)},truncate:(e,t)=>{if(typeof e!=`string`)return e;let n=_(t[0],50),r=[...e];return r.length<=n?e:`${r.slice(0,n).join(``)}…`},upper:(e,t,n)=>typeof e==`string`?e.toLocaleUpperCase(l(n)):e,lower:(e,t,n)=>typeof e==`string`?e.toLocaleLowerCase(l(n)):e,capitalize:(e,t,n)=>{if(typeof e!=`string`)return e;let r=[...e],i=r[0];return i===void 0?e:i.toLocaleUpperCase(l(n))+r.slice(1).join(``)},trim:e=>typeof e==`string`?e.trim():e,default:(e,t)=>{let n=t[0];return n===void 0?e:e??n}}),y=Object.freeze(Object.keys(v));function b(e={}){let t=[],n=new Map(Object.entries(v));for(let[r,i]of Object.entries(e)){if(!d.test(r)){t.push(`invalid filter name '${r}' — every ':'-separated segment must start with a letter or '_' and continue with word characters, or the grammar can never reach it (e.g. 'rent:nights')`);continue}if(Object.hasOwn(v,r)){t.push(`reserved built-in filter name '${r}' — built-in names cannot be shadowed; namespace plugin filters (e.g. 'rent:nights')`);continue}n.set(r,i)}if(t.length>0)throw Error(`Token filter registry: ${t.join(`; `)}`);let r=[...n.keys()].sort();return{get:e=>n.get(e),has:e=>n.has(e),names:()=>r}}function x(e,t){let n=0;for(;n<t.length;){let e=t[n];if(e===void 0||e.quoted||!f.test(e.text))break;n++}for(let r=n;r>=1;r--){let n=t.slice(0,r).map(e=>e.text).join(`:`),i=e.get(n);if(i!==void 0)return{name:n,args:t.slice(r).map(e=>e.text),filter:i}}return{name:t[0]?.text??``,args:t.slice(1).map(e=>e.text),filter:void 0}}function S(e){return e}const C=/^\w+$/;function w(e){let t=[],n=new Map;for(let r of e){let{namespace:e}=r;if(!C.test(e)){t.push(`invalid provider namespace '${e}' — namespaces must be \\w+ to be reachable as {{ namespace:property }}`);continue}if(n.has(e)){t.push(`namespace collision on '${e}' — two providers cannot share a namespace`);continue}for(let n of Object.keys(r.tokens))C.test(n)||t.push(`invalid token name '${e}:${n}' — token names must be \\w+ to be reachable`);n.set(e,r)}for(let e of n.values())for(let[r,i]of Object.entries(e.tokens)){let{chainsTo:a}=i;a!==void 0&&!n.has(a)&&t.push(`'${e.namespace}:${r}' chains to unknown namespace '${a}' — chainsTo must name a registered provider`)}if(t.length>0)throw Error(`Token provider registry: ${t.join(`; `)}`);let r=[...n.values()];return{get:e=>n.get(e),has:e=>n.has(e),list:()=>r}}const T=w([]);function E(e,t){if(Object.hasOwn(e.tokens,t))return e.tokens[t]}function D(e,t){let n=e.resolve;return n(t)}const O={value:void 0,kind:void 0},k=`[@murumets-ee/tokens]`;function A(e){return typeof e==`object`&&!!e&&!(e instanceof Date)}function j(e){return typeof e==`string`||typeof e==`boolean`?e:typeof e==`number`?Number.isFinite(e)?e:null:e instanceof Date?Number.isNaN(e.getTime())?null:e:null}function M(e,t){return typeof e==`string`?e:typeof e==`boolean`?e?`true`:`false`:e instanceof Date?new Intl.DateTimeFormat(t,{dateStyle:`medium`}).format(e):new Intl.NumberFormat(t).format(e)}function N(e,t,n,r){try{return M(e,t)}catch(e){return r(`${k} formatting '${n}' for locale '${t}' threw: ${e instanceof Error?e.message:String(e)}; token left unresolved`),null}}function P(e,t){let n=t.vars;return n===void 0||!Object.hasOwn(n,e)?O:{value:n[e],kind:void 0}}function F(e,t){let n=e.needs;if(n===void 0)return{ok:!0,ctx:void 0};let r=t.data;if(r===void 0||!Object.hasOwn(r,n))return{ok:!1};let i=r[n];return i==null?{ok:!1}:{ok:!0,ctx:i}}function I(e,t,n,r,i){try{return{ok:!0,value:D(e,t)}}catch(e){return i(`${k} resolver '${n}' threw on '${r}': ${e instanceof Error?e.message:String(e)}; token left unresolved`),{ok:!1}}}function L(e,n,r,i,a,o){let[s,...c]=e;if(s===void 0)return O;if(c.length>i)throw new t(`Token '${a}' exceeds the maximum chain depth of ${i} — shorten the chain or raise maxDepth`);let l=r.get(s);if(l===void 0)return O;let u=F(l,n);if(!u.ok)return O;let d=u.ctx;for(let e=0;e<c.length;e++){let t=c[e];if(t===void 0)return O;let n=E(l,t);if(n===void 0)return O;let i=I(n,d,`${l.namespace}:${t}`,a,o);if(!i.ok)return O;let s=i.value;if(e===c.length-1)return A(s)?O:{value:s,kind:n.kind};if(n.chainsTo===void 0||!A(s))return O;let u=r.get(n.chainsTo);if(u===void 0)return O;l=u,d=s}return O}function R(t,n,r,i,a,o){let s=n;for(let n of t.filters){let{name:c,args:l,filter:u}=x(r,n.segments);if(u===void 0){let n=`unknown filter '${c}' in '${t.raw}' — filters must be registered by name`;if(a)throw new e(`${k} ${n}`);o(`${k} ${n}; token left unfiltered`);continue}try{s=u(s,l,i)}catch(e){o(`${k} filter '${c}' threw on '${t.raw}': ${e instanceof Error?e.message:String(e)}; value left unfiltered`)}}return s}function z(e,t,n,r,i,s,c){let l=N(e,i,s,c);return l===null?null:r===`html`?t===`url`?a(o(l)):n?l:a(l):l}function B(e,t){if(t===`error`)throw new n(e.expression);return t===`empty`?``:e.raw}function V(n,i,a,o,s,l){let u=a?.mode??c.mode,d=a?.missing??c.missing,f=a?.maxDepth??c.maxDepth,p=a?.maxExpansions??c.maxExpansions,m=a?.strict??c.strict,h=i.locale??`en`,g=Object.freeze({locale:h}),{spans:_,unterminated:v}=r(n,{maxUnterminated:p});if(_.length>p)throw new t(`Template contains ${_.length} token expansions, exceeding the limit of ${p} — split the template or raise maxExpansions`);if(m){for(let t of _)if(t.malformed!==null)throw new e(`${k} malformed token '${t.raw}' — ${t.malformed.reason}`);if(v>0)throw new e(`${k} ${v} unterminated '{{' in template`)}let y=``,b=0;for(let e of _){y+=n.slice(b,e.index),b=e.index+e.raw.length;let t=e.token;if(t===null){y+=e.raw;continue}let r=t.path.length===1?P(t.path[0]??``,i):L(t.path,i,o,f,t.raw,l),a=j(R(t,r.value,s,g,m,l)),c=a===null?null:z(a,r.kind,t.unescaped,u,h,t.raw,l);if(c===null){y+=B(t,d);continue}y+=c}return y+n.slice(b)}function H(e={}){let t=e.providers??T,n=e.filters??b(),r=e.onWarn??(e=>console.warn(e));return(e,i,a)=>V(e,i,a,t,n,r)}const U=H();function W(e,t,n){return U(e,t,n)}export{v as BUILT_IN_FILTERS,s as DEFAULT_TOKEN_LOCALE,y as RESERVED_FILTER_NAMES,t as TokenExpansionLimitError,n as TokenResolveError,e as TokenSyntaxError,b as buildFilterRegistry,w as buildTokenProviderRegistry,H as createTokenResolver,S as defineTokenProvider,a as escapeHtml,W as resolveTokens,o as safeUrl,r as scanTokens};
2
- //# sourceMappingURL=index.mjs.map
@@ -101,4 +101,3 @@ interface ScanLimits {
101
101
  declare function scanTokens(text: string, limits?: ScanLimits): ScanResult;
102
102
  //#endregion
103
103
  export { scanTokens as a, TokenSyntaxError as c, ScannedToken as i, ScanLimits as n, TokenExpansionLimitError as o, ScanResult as r, TokenResolveError as s, ParsedToken as t };
104
- //# sourceMappingURL=parse-COr_fTUr.d.mts.map
@@ -1,2 +1 @@
1
1
  var e=class extends Error{token;constructor(e){super(`Token could not be resolved: '${e}' — no value for this token in the given context`),this.name=`TokenResolveError`,this.token=e}},t=class extends Error{constructor(e){super(e),this.name=`TokenExpansionLimitError`}},n=class extends Error{constructor(e){super(e),this.name=`TokenSyntaxError`}};const r=/^\w+$/;function i(e,t){let n=[],r=[],i=``,a=null,o=0,s=e=>{(i.length>0||e)&&r.push({text:i,quoted:e}),i=``},c=e=>{s(!1),n.push({text:r.map(e=>e.quoted?e.text:e.text.trim()).join(``),raw:e,quoted:r.some(e=>e.quoted)}),r=[]};for(let n=0;n<e.length;n++){let r=e[n];if(r!==void 0){if(a!==null){if(r===`\\`){let t=e[n+1];if(t!==void 0){i+=t,n++;continue}}if(r===a){s(!0),a=null;continue}i+=r;continue}if(r===`"`||r===`'`){s(!1),a=r;continue}if(r===t){c(e.slice(o,n)),o=n+1;continue}i+=r}}return s(a!==null),c(e.slice(o)),n}function a(e){let t=i(e,`|`),n=t[0];if(n===void 0||n.text===``)return{error:`empty token expression`};let a=i(n.raw,`:`).map(e=>e.text);if(a.some(e=>!r.test(e)))return{error:`invalid token path '${n.text}'`};let o=[];for(let e=1;e<t.length;e++){let n=t[e];if(n===void 0)continue;let r=i(n.raw,`:`),a=r[0];if(a===void 0||a.text===``)return{error:`empty filter name in pipe`};o.push({segments:r,raw:n.raw.trim()})}return{token:{path:a,filters:o}}}function o(e,t,n){let r=Math.min(e.length,t+1024),i=null;for(let a=t;a<r;a++){let t=e[a];if(t===void 0)break;if(i!==null){if(t===`\\`){a++;continue}t===i&&(i=null);continue}if(t===`"`||t===`'`){i=t;continue}if(e.startsWith(n,a))return a}return-1}function s(e,n={}){let r=n.maxUnterminated??500,i=[],s=0,c=0;for(;c<e.length;){if(e[c]!==`{`||e[c+1]!==`{`){c++;continue}let n=e[c+2]===`{`,l=n?3:2,u=n?`}}}`:`}}`,d=o(e,c+l,u);if(d===-1){if(s++,s>r)throw new t(`Template contains more than ${r} unterminated '{{' openers — refusing to scan further`);c++;continue}let f=e.slice(c,d+u.length),p=e.slice(c+l,d).trim(),m=a(p);i.push(`error`in m?{index:c,raw:f,token:null,malformed:{raw:f,expression:p,reason:m.error}}:{index:c,raw:f,token:{raw:f,expression:p,unescaped:n,...m.token},malformed:null}),c=d+u.length}return{spans:i,unterminated:s}}export{n as i,t as n,e as r,s as t};
2
- //# sourceMappingURL=parse-D_0FL4H7.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/tokens",
3
- "version": "0.109.0",
3
+ "version": "0.111.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/escape.ts","../src/types.ts","../src/filters.ts","../src/providers.ts","../src/resolve.ts"],"mappings":";;;;;;AAsBA;;;;AAAsC;AAUtC;;;;AAAqC;AAArC,iBAVgB,UAAA,CAAW,GAAW;;;ACVtC;;;;iBDoBgB,OAAA,CAAQ,KAAa;;;;;;AAVrC;;;;AAAsC;AAUtC;;;KCpBY,UAAA,+BAAyC,IAAI;ADoBpB;;;;ACpBrC;;;;AAAyD;AAYzD;ADQqC,KCRzB,eAAA,GAAkB,MAAM;;KAGxB,kBAAA,GAAqB,UAAA,GAAa,eAAe;AAHzB;AAGpC;;;;AAHoC,UAUnB,YAAA;EAAA;EAEf,IAAA,GAAO,MAAA,SAAe,UAAA;;EAEtB,IAAA,GAAO,MAAA;EAFA;;;;EAOP,MAAA;AAAA;;;;;;AAAM;AAkBR;;;;AAAiC;AAGjC;;;;cAHa,oBAAA;AAMb;AAAA,KAHY,aAAA;;KAGA,eAAA;AAAA,UAEK,cAAA;EAAA;EAEf,IAAA,GAAO,eAAA;;EAEP,OAAA,GAAU,aAAa;EAFvB;EAIA,QAAA;EAFA;EAIA,aAAA;EAFA;;;;AASM;;EAAN,MAAA;AAAA;;;;UCtEe,kBAAA;;;;ADFjB;;ECQE,MAAM;AAAA;ADRiD;AAYzD;;;;AAAoC;AAZqB,KC4B7C,WAAA,IAAe,KAAA,EAAO,UAAA,EAAY,IAAA,YAAgB,GAAA,EAAK,kBAAA,KAAuB,UAAA;;cA0J7E,gBAAA,EAAkB,QAAA,CAAS,MAAA,SAAe,WAAA;;cAc1C,qBAAA;AD9Kb;AAAA,UCqLiB,cAAA;EACf,GAAA,CAAI,IAAA,WAAe,WAAW;EAC9B,GAAA,CAAI,IAAA;EDrLG;ECuLP,KAAA;AAAA;;;;;;;;;ADhLM;AAkBR;;;;iBC8KgB,mBAAA,CACd,WAAA,GAAa,QAAA,CAAS,MAAA,SAAe,WAAA,KACpC,cAAA;;;;KC3NS,SAAA;AAAA,UAEK,QAAA;;EAEf,KAAA;EACA,WAAA;EFCU;EECV,IAAA,GAAO,SAAA;;EAEP,QAAA;EFHkC;EEKlC,OAAA;EFF4B;;;AAA+B;AAO7D;;;EEGE,OAAA,GAAU,GAAA,EAAK,GAAA,KAAQ,kBAAA;AAAA;AAAA,UAGR,aAAA;EFFF;EEIb,SAAA;EFNA;;;;EEWA,KAAA;EACA,MAAA,EAAQ,MAAA,SAAe,QAAA,CAAS,GAAA;AAAA;AFL1B;AAkBR;;;;AAlBQ,KEaI,gBAAA,GAAmB,aAAa;AFQ5C;AAAA,iBELgB,mBAAA,eAAA,CACd,QAAA,EAAU,aAAA,CAAc,GAAA,IACvB,aAAA,CAAc,GAAA;;UAQA,qBAAA;EACf,GAAA,CAAI,SAAA,WAAoB,gBAAA;EACxB,GAAA,CAAI,SAAA;EFJqB;EEMzB,IAAA,aAAiB,gBAAgB;AAAA;AFNR;AAE3B;;;;;;;;;;;;AAeQ;;;;AAjBmB,iBE0BX,0BAAA,CACd,SAAA,WAAoB,gBAAA,KACnB,qBAAqB;;;AFpExB;AAAA,UG4BiB,iBAAA;;EAEf,SAAA,GAAY,qBAAA;EH9B+C;EGgC3D,OAAA,GAAU,cAAc;EHzBG;;;;;;EGgC3B,MAAA,IAAU,OAAA;AAAA;;KAIA,aAAA,IACV,IAAA,UACA,OAAA,EAAS,YAAA,EACT,OAAA,GAAU,cAAc;;;;;AH9BlB;AAkBR;;;;AAAiC;AAGjC;;;;AAAyB;AAGzB;;iBGoXgB,mBAAA,CAAoB,IAAA,GAAM,iBAAA,GAAyB,aAAa;;AHpXrD;AAE3B;;;;;;iBGsYgB,aAAA,CACd,IAAA,UACA,OAAA,EAAS,YAAA,EACT,OAAA,GAAU,cAAc"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/escape.ts","../src/types.ts","../src/filters.ts","../src/providers.ts","../src/resolve.ts"],"sourcesContent":["/**\n * HTML/URL output safety primitives.\n *\n * Ported verbatim (behavior-identical) from `@murumets-ee/mail`'s\n * `src/template.ts`, which is the canonical, already-audited implementation.\n * Phase 07 removes mail's copies and points it here — until then the two are\n * intentional duplicates, not divergent forks.\n */\n\nconst ESCAPED_CHARS: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n}\n\n/**\n * Escape `&`, `<`, `>`, `\"`, `'` for safe interpolation into HTML text or\n * quoted attribute values. Does NOT neuter dangerous URL schemes — pair with\n * {@link safeUrl} when interpolating into `href`/`src`.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(/[&<>\"']/g, (ch) => ESCAPED_CHARS[ch] ?? ch)\n}\n\n/**\n * Allow only http(s)/mailto URLs through to attribute values; everything else\n * (including `javascript:`, `data:`, `vbscript:`, protocol-relative\n * `//attacker.com`, and malformed URLs) is rewritten to `#` so an `href`\n * becomes a no-op rather than executing script or navigating off-domain.\n */\nexport function safeUrl(input: string): string {\n if (!input) return '#'\n // Protocol-relative URLs (`//host/...`, `\\\\host\\...`) inherit the page's\n // scheme and navigate off-domain — treat them as a phishing footgun and\n // reject before the URL constructor accepts them as same-origin relative.\n if (/^[\\s\\\\/]*[\\\\/]{2,}/.test(input)) return '#'\n let parsed: URL\n try {\n parsed = new URL(input)\n } catch {\n // Same-origin relative URLs (`/path`, `#anchor`, `?q=1`) are fine —\n // they have no scheme to abuse and stay within the rendering origin.\n if (input.startsWith('/') || input.startsWith('#') || input.startsWith('?')) return input\n return '#'\n }\n const proto = parsed.protocol.toLowerCase()\n if (proto === 'http:' || proto === 'https:' || proto === 'mailto:') {\n return parsed.toString()\n }\n return '#'\n}\n","/**\n * Core value + context types for `@murumets-ee/tokens`.\n *\n * See `plan/tokens-and-templates/03-core-primitive-api.md` §3.\n */\n\n/**\n * A renderable token value. Anything a provider, a filter, or a caller-supplied\n * `vars` entry can produce and the resolver knows how to stringify safely.\n *\n * Deliberately scalar-only: objects are never rendered (see {@link TokenChainValue}).\n */\nexport type TokenValue = string | number | boolean | Date | null | undefined\n\n/**\n * The object a `chainsTo` token hands to the next provider as ITS context —\n * e.g. `{{ ticket:assignee:name }}` resolves `assignee` to a user-ish object\n * which the `user` provider then resolves `name` against.\n *\n * A `TokenChainValue` is NEVER rendered directly: `{{ ticket:assignee }}` (no\n * trailing property) is treated as unresolved and follows the missing policy,\n * so a stray object can't leak `[object Object]` — or its whole JSON shape —\n * into an email.\n */\nexport type TokenChainValue = Record<string, unknown>\n\n/** What a token resolver may return: a renderable value, or a chain context. */\nexport type TokenResolveResult = TokenValue | TokenChainValue\n\n/**\n * Everything a single `resolveTokens` call is allowed to see. The resolver\n * reads NOTHING else — no ambient session, no DB, no globals. The call site\n * (which has already done its own authorization) decides what is visible.\n */\nexport interface TokenContext {\n /** Flat vars for bare `{{ var }}` tokens (back-compat with mail/ticketing). */\n vars?: Record<string, TokenValue>\n /** Resolved context objects keyed by a provider's `needs` key, for `{{ ns:prop }}` tokens. */\n data?: Record<string, unknown>\n /**\n * Locale for filter formatting (Intl) + locale-aware default stringification.\n * Omitted → {@link DEFAULT_TOKEN_LOCALE}, never the host's default locale.\n */\n locale?: string\n}\n\n/**\n * The locale used for `Intl` formatting when the call site supplied no\n * {@link TokenContext.locale}.\n *\n * Fixed on purpose. Passing `undefined` to `Intl` makes it fall back to the\n * HOST's default locale (Node derives it from `LANG`/`LC_ALL`), so the same\n * template + the same data would render differently per machine — locale drift\n * by another route, which phase 03 §4 rules out. A deterministic constant keeps\n * an invoice or a booking confirmation byte-identical across environments.\n *\n * Note this pins the LOCALE only: date/time values are still formatted in the\n * host's default TIME ZONE, because `TokenContext` has no `timeZone` field yet.\n * A caller that needs timezone-correct output should pre-format the `Date` into\n * a string before passing it as a token value.\n */\nexport const DEFAULT_TOKEN_LOCALE = 'en'\n\n/** What to do when a token cannot be resolved. See phase 03 §5. */\nexport type MissingPolicy = 'leave' | 'empty' | 'error'\n\n/** Output mode. `'html'` escapes values; `'text'` emits them verbatim. */\nexport type TokenOutputMode = 'html' | 'text'\n\nexport interface ResolveOptions {\n /** `'html'` → escape values (and honor `{{{ }}}` raw); `'text'` → no escaping. Default `'text'`. */\n mode?: TokenOutputMode\n /** What to do when a token resolves to undefined / unknown namespace. Default `'leave'`. */\n missing?: MissingPolicy\n /** Max chain depth (e.g. `ticket:assignee:manager:name` = 3). Default 4. */\n maxDepth?: number\n /** Max total token expansions per call (DoS guard). Default 500. */\n maxExpansions?: number\n /**\n * Strict / lint mode: malformed `{{ }}` and unknown filter names throw\n * {@link TokenSyntaxError} instead of being left alone / warn-and-passed-through.\n * Intended for the authoring + preview surfaces, never for a live send.\n * Default `false`.\n */\n strict?: boolean\n}\n\n/** Defaults applied when a caller omits {@link ResolveOptions} fields. */\nexport const RESOLVE_DEFAULTS = {\n mode: 'text',\n missing: 'leave',\n maxDepth: 4,\n maxExpansions: 500,\n strict: false,\n} as const satisfies Required<ResolveOptions>\n","/**\n * Curated pipe filters (phase 05).\n *\n * Filters are **vetted transforms looked up by name in an allowlist** — never\n * inline code, never user-supplied functions, never I/O (D002). Every built-in\n * is `Intl`-backed and pure: same input + same locale ⇒ same output — and with\n * no locale at all, {@link DEFAULT_TOKEN_LOCALE} rather than the host's, so the\n * output does not drift between machines.\n */\n\nimport type { ExpressionSegment } from './parse.js'\nimport { DEFAULT_TOKEN_LOCALE, type TokenValue } from './types.js'\n\n/** Everything a filter is allowed to know about the surrounding call. */\nexport interface TokenFilterContext {\n /**\n * BCP-47 locale from `TokenContext.locale`. The resolver always populates\n * this, substituting {@link DEFAULT_TOKEN_LOCALE} when the call site omitted\n * one — it is optional only for a hand-built context.\n */\n locale?: string\n}\n\n/**\n * The locale a built-in formats with.\n *\n * Never `ctx.locale` raw: `undefined` would make `Intl` pick the HOST default\n * locale, so the same value would render differently per machine. The fallback\n * is a fixed constant instead (see {@link DEFAULT_TOKEN_LOCALE}).\n */\nfunction localeOf(ctx: TokenFilterContext): string {\n return ctx.locale ?? DEFAULT_TOKEN_LOCALE\n}\n\n/**\n * A filter transforms a resolved value. Contract: **pure, no I/O, no throw** —\n * return the value unchanged on input it doesn't understand. (A filter that\n * throws anyway is caught by the resolver, warn-logged, and its input is used\n * unfiltered, so a bad filter can never crash a send.)\n */\nexport type TokenFilter = (value: TokenValue, args: string[], ctx: TokenFilterContext) => TokenValue\n\n/**\n * One identifier-shaped name segment: a letter or `_` lead, then word chars.\n *\n * ONE source of truth for both the registration validator ({@link FILTER_NAME})\n * and the parser walk ({@link NAME_PART} in {@link resolveFilterCall}) — they\n * must not drift. When they did, a digit-leading name (`'911'`, `'rent:9nights'`)\n * passed the looser `\\w+` registration check but could never be reached by the\n * walk, so the filter registered fine and was then silently dead.\n */\nconst NAME_PART_SOURCE = '[A-Za-z_]\\\\w*'\n/** Filter names are `:`-joined name parts (e.g. `rent:nights`). */\nconst FILTER_NAME = new RegExp(`^${NAME_PART_SOURCE}(?::${NAME_PART_SOURCE})*$`)\n/** A name part eligible to be joined into a namespaced filter name. */\nconst NAME_PART = new RegExp(`^${NAME_PART_SOURCE}$`)\n\n// ---------------------------------------------------------------------------\n// Coercion helpers (shared by the built-ins)\n// ---------------------------------------------------------------------------\n\nfunction toDate(value: TokenValue): Date | null {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value\n if (typeof value === 'number') {\n const fromEpoch = new Date(value)\n return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch\n }\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed\n }\n return null\n}\n\nfunction toNumber(value: TokenValue): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (typeof value === 'string' && value.trim() !== '') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n }\n return null\n}\n\ntype IntlStyle = 'full' | 'long' | 'medium' | 'short'\nconst INTL_STYLES: readonly IntlStyle[] = ['full', 'long', 'medium', 'short']\n\nfunction toStyle(arg: string | undefined, fallback: IntlStyle): IntlStyle {\n if (arg === undefined) return fallback\n const found = INTL_STYLES.find((style) => style === arg.toLowerCase())\n return found ?? fallback\n}\n\nfunction toCount(arg: string | undefined, fallback: number): number {\n if (arg === undefined) return fallback\n const parsed = Number.parseInt(arg, 10)\n if (!Number.isFinite(parsed) || parsed < 0) return fallback\n return parsed\n}\n\n// ---------------------------------------------------------------------------\n// Built-in filters — phase 05 §3\n// ---------------------------------------------------------------------------\n\n/** `{{ ticket:createdAt | date:\"medium\" }}` — `Intl.DateTimeFormat` date part. */\nconst dateFilter: TokenFilter = (value, args, ctx) => {\n const date = toDate(value)\n if (date === null) return value\n return new Intl.DateTimeFormat(localeOf(ctx), {\n dateStyle: toStyle(args[0], 'medium'),\n }).format(date)\n}\n\n/** `{{ x | time:\"short\" }}` — `Intl.DateTimeFormat` time part. */\nconst timeFilter: TokenFilter = (value, args, ctx) => {\n const date = toDate(value)\n if (date === null) return value\n return new Intl.DateTimeFormat(localeOf(ctx), {\n timeStyle: toStyle(args[0], 'short'),\n }).format(date)\n}\n\n/** `{{ x | number }}` / `{{ x | number:2 }}` — `Intl.NumberFormat`, optional fixed fraction digits. */\nconst numberFilter: TokenFilter = (value, args, ctx) => {\n const num = toNumber(value)\n if (num === null) return value\n const digitsArg = args[0]\n if (digitsArg === undefined) return new Intl.NumberFormat(localeOf(ctx)).format(num)\n const digits = Math.min(toCount(digitsArg, 0), 20)\n return new Intl.NumberFormat(localeOf(ctx), {\n minimumFractionDigits: digits,\n maximumFractionDigits: digits,\n }).format(num)\n}\n\n/**\n * `{{ order:totalCents | currency:\"EUR\" }}`.\n *\n * **Cents convention:** an INTEGER input is interpreted as minor units (cents)\n * and divided by 100 — matching the toolkit's `*Cents` field convention. Pass a\n * non-integer (e.g. `12.5`) to format major units directly.\n *\n * With no currency code, or an unknown one, the value is returned unchanged\n * (an unknown code makes `Intl` throw; the resolver catches + warns).\n */\nconst currencyFilter: TokenFilter = (value, args, ctx) => {\n const code = args[0]\n if (code === undefined || code === '') return value\n const num = toNumber(value)\n if (num === null) return value\n const amount = Number.isInteger(num) ? num / 100 : num\n return new Intl.NumberFormat(localeOf(ctx), { style: 'currency', currency: code }).format(amount)\n}\n\n/** `{{ ticket:subject | truncate:60 }}` — code-point-safe slice + ellipsis. */\nconst truncateFilter: TokenFilter = (value, args) => {\n if (typeof value !== 'string') return value\n const limit = toCount(args[0], 50)\n // Split by code point so a truncation can never cut a surrogate pair in half.\n const chars = [...value]\n if (chars.length <= limit) return value\n return `${chars.slice(0, limit).join('')}…`\n}\n\n/** `{{ x | upper }}` — locale-aware uppercase. */\nconst upperFilter: TokenFilter = (value, _args, ctx) =>\n typeof value === 'string' ? value.toLocaleUpperCase(localeOf(ctx)) : value\n\n/** `{{ x | lower }}` — locale-aware lowercase. */\nconst lowerFilter: TokenFilter = (value, _args, ctx) =>\n typeof value === 'string' ? value.toLocaleLowerCase(localeOf(ctx)) : value\n\n/** `{{ x | capitalize }}` — locale-aware uppercase of the first code point only. */\nconst capitalizeFilter: TokenFilter = (value, _args, ctx) => {\n if (typeof value !== 'string') return value\n const chars = [...value]\n const first = chars[0]\n if (first === undefined) return value\n return first.toLocaleUpperCase(localeOf(ctx)) + chars.slice(1).join('')\n}\n\n/** `{{ x | trim }}`. */\nconst trimFilter: TokenFilter = (value) => (typeof value === 'string' ? value.trim() : value)\n\n/**\n * `{{ x | default:\"Friend\" }}` — literal fallback for a null/undefined value.\n * The literal is NEVER re-scanned for tokens (HubSpot's no-recursion rule, D002).\n */\nconst defaultFilter: TokenFilter = (value, args) => {\n const literal = args[0]\n if (literal === undefined) return value\n return value ?? literal\n}\n\n/** The reserved, always-registered built-in filter set. */\nexport const BUILT_IN_FILTERS: Readonly<Record<string, TokenFilter>> = Object.freeze({\n date: dateFilter,\n time: timeFilter,\n number: numberFilter,\n currency: currencyFilter,\n truncate: truncateFilter,\n upper: upperFilter,\n lower: lowerFilter,\n capitalize: capitalizeFilter,\n trim: trimFilter,\n default: defaultFilter,\n})\n\n/** Names a plugin may not use — the built-ins cannot be shadowed. */\nexport const RESERVED_FILTER_NAMES: readonly string[] = Object.freeze(Object.keys(BUILT_IN_FILTERS))\n\n// ---------------------------------------------------------------------------\n// Registry\n// ---------------------------------------------------------------------------\n\n/** Allowlist of filters a resolver may invoke, by name. */\nexport interface FilterRegistry {\n get(name: string): TokenFilter | undefined\n has(name: string): boolean\n /** Every registered name, sorted — powers the discovery UI (phase 06). */\n names(): readonly string[]\n}\n\n/**\n * Build the filter allowlist: the built-ins plus any contributed filters.\n *\n * Validates at construction (deny-by-default, boot-time — same shape as the\n * toolkit's other contribution-collision checks):\n * - a contributed name may not shadow a built-in;\n * - a contributed name must be `:`-joined {@link NAME_PART_SOURCE} segments, so\n * it is actually reachable from the grammar — {@link resolveFilterCall} only\n * ever extends a name through segments of that shape, so a name outside it\n * (a digit-leading `'911'`, a space) would register and then be dead.\n *\n * All violations are aggregated into ONE error.\n */\nexport function buildFilterRegistry(\n contributed: Readonly<Record<string, TokenFilter>> = {},\n): FilterRegistry {\n const violations: string[] = []\n const filters = new Map<string, TokenFilter>(Object.entries(BUILT_IN_FILTERS))\n\n for (const [name, filter] of Object.entries(contributed)) {\n if (!FILTER_NAME.test(name)) {\n violations.push(\n `invalid filter name '${name}' — every ':'-separated segment must start with a letter or '_' and continue with word characters, or the grammar can never reach it (e.g. 'rent:nights')`,\n )\n continue\n }\n if (Object.hasOwn(BUILT_IN_FILTERS, name)) {\n violations.push(\n `reserved built-in filter name '${name}' — built-in names cannot be shadowed; namespace plugin filters (e.g. 'rent:nights')`,\n )\n continue\n }\n filters.set(name, filter)\n }\n\n if (violations.length > 0) {\n throw new Error(`Token filter registry: ${violations.join('; ')}`)\n }\n\n const names = [...filters.keys()].sort()\n return {\n get: (name) => filters.get(name),\n has: (name) => filters.has(name),\n names: () => names,\n }\n}\n\n/**\n * Pick the filter name out of a pipe step's colon-separated segments.\n *\n * A plugin filter may be namespaced (`rent:nights`), which collides with the\n * `name:arg` shape — so the name is resolved by longest-prefix match against\n * the registry, considering only leading UNQUOTED, identifier-shaped segments.\n * `truncate:60` therefore can never be mistaken for a two-part name (`60` is\n * not identifier-shaped) and `default:\"date\"` can never be either (quoted).\n */\nexport function resolveFilterCall(\n registry: FilterRegistry,\n segments: readonly ExpressionSegment[],\n): { name: string; args: string[]; filter: TokenFilter | undefined } {\n let joinable = 0\n while (joinable < segments.length) {\n const segment = segments[joinable]\n if (segment === undefined || segment.quoted || !NAME_PART.test(segment.text)) break\n joinable++\n }\n\n for (let length = joinable; length >= 1; length--) {\n const name = segments\n .slice(0, length)\n .map((segment) => segment.text)\n .join(':')\n const filter = registry.get(name)\n if (filter !== undefined) {\n return { name, args: segments.slice(length).map((segment) => segment.text), filter }\n }\n }\n\n return {\n name: segments[0]?.text ?? '',\n args: segments.slice(1).map((segment) => segment.text),\n filter: undefined,\n }\n}\n","/**\n * Declarative token providers + the registry built from them (phase 04).\n *\n * A provider co-locates the *describe* metadata (label/kind/example → discovery\n * UI) and the *resolve* function per token, so the two can never drift.\n *\n * ## The security invariant\n *\n * `resolve` is **pure over the `ctx` it is handed**. It never reads ambient\n * state — no session, no request, no DB, no network. A `ticket` provider\n * receives the ticket object the *already-authorized* call site supplied; it\n * can never fetch \"some other ticket\". That makes cross-user / cross-tenant\n * leakage structurally impossible at the token layer.\n */\n\nimport type { TokenResolveResult } from './types.js'\n\n/** How a token's value should be treated on output. `'url'` triggers `safeUrl`. */\nexport type TokenKind = 'text' | 'url' | 'number' | 'date'\n\nexport interface TokenDef<Ctx = unknown> {\n /** Human name → discovery UI (phase 06). */\n label: string\n description?: string\n /** Drives default formatting + `safeUrl` for `'url'`. */\n kind?: TokenKind\n /** Another provider's namespace — makes `{{ ns:prop:sub }}` chaining possible. */\n chainsTo?: string\n /** Sample value for live preview (phase 06/13). */\n example?: string\n /**\n * Resolve this token from the caller-supplied context.\n *\n * MUST be pure over `ctx`: no ambient/global reads, no I/O, no throw. When\n * `chainsTo` is set, return the object handed to the chained provider as ITS\n * context; otherwise return a renderable {@link TokenValue}.\n */\n resolve: (ctx: Ctx) => TokenResolveResult\n}\n\nexport interface TokenProvider<Ctx = unknown> {\n /** `'ticket' | 'site' | 'customer' | 'currentUser'` — unique across the app. */\n namespace: string\n /**\n * Key into the call's `TokenContext.data` that this provider needs.\n * Absent → GLOBAL provider (resolvable with no context object).\n */\n needs?: string\n tokens: Record<string, TokenDef<Ctx>>\n}\n\n/**\n * A provider with its context type erased — `TokenProvider<never>` is the\n * universal supertype every `TokenProvider<Ctx>` is assignable to, so the\n * registry can hold providers with different context types without `any`.\n */\nexport type AnyTokenProvider = TokenProvider<never>\n\n/** Identity helper that pins the context type for a provider's resolvers. */\nexport function defineTokenProvider<Ctx = unknown>(\n provider: TokenProvider<Ctx>,\n): TokenProvider<Ctx> {\n return provider\n}\n\n/** Namespaces + token property names are `\\w+` so the grammar can reach them. */\nconst IDENTIFIER = /^\\w+$/\n\n/** The validated allowlist of providers a resolver may consult. */\nexport interface TokenProviderRegistry {\n get(namespace: string): AnyTokenProvider | undefined\n has(namespace: string): boolean\n /** Every registered provider, in registration order — powers discovery (phase 06). */\n list(): readonly AnyTokenProvider[]\n}\n\n/**\n * Build + validate the provider registry (phase 04 §5). Deny-by-default: a\n * malformed contribution graph fails at BOOT, never silently at send time.\n *\n * Validates:\n * - no duplicate `namespace` (two plugins cannot own the same namespace);\n * - `namespace` / token property names are `\\w+` (otherwise unreachable);\n * - every `chainsTo` names a namespace present in the same set.\n *\n * All violations are aggregated into ONE error so a misconfigured app is fixed\n * in a single pass.\n *\n * This is a pure function on purpose: `@murumets-ee/tokens` is a leaf and never\n * imports `@murumets-ee/core`. The framework's merge step is what gathers\n * `contributes.tokens` and calls this — the registry is injected into the\n * resolver, not imported by it.\n */\nexport function buildTokenProviderRegistry(\n providers: readonly AnyTokenProvider[],\n): TokenProviderRegistry {\n const violations: string[] = []\n const byNamespace = new Map<string, AnyTokenProvider>()\n\n for (const provider of providers) {\n const { namespace } = provider\n if (!IDENTIFIER.test(namespace)) {\n violations.push(\n `invalid provider namespace '${namespace}' — namespaces must be \\\\w+ to be reachable as {{ namespace:property }}`,\n )\n continue\n }\n if (byNamespace.has(namespace)) {\n violations.push(\n `namespace collision on '${namespace}' — two providers cannot share a namespace`,\n )\n continue\n }\n for (const property of Object.keys(provider.tokens)) {\n if (!IDENTIFIER.test(property)) {\n violations.push(\n `invalid token name '${namespace}:${property}' — token names must be \\\\w+ to be reachable`,\n )\n }\n }\n byNamespace.set(namespace, provider)\n }\n\n for (const provider of byNamespace.values()) {\n for (const [property, def] of Object.entries(provider.tokens)) {\n const { chainsTo } = def\n if (chainsTo !== undefined && !byNamespace.has(chainsTo)) {\n violations.push(\n `'${provider.namespace}:${property}' chains to unknown namespace '${chainsTo}' — chainsTo must name a registered provider`,\n )\n }\n }\n }\n\n if (violations.length > 0) {\n throw new Error(`Token provider registry: ${violations.join('; ')}`)\n }\n\n const list = [...byNamespace.values()]\n return {\n get: (namespace) => byNamespace.get(namespace),\n has: (namespace) => byNamespace.has(namespace),\n list: () => list,\n }\n}\n\n/** A provider registry with nothing in it — the default for a resolver with no wiring. */\nexport const EMPTY_PROVIDER_REGISTRY: TokenProviderRegistry = buildTokenProviderRegistry([])\n\n/** Look up an own token definition, never an inherited `Object.prototype` member. */\nexport function getTokenDef(\n provider: AnyTokenProvider,\n property: string,\n): TokenDef<never> | undefined {\n if (!Object.hasOwn(provider.tokens, property)) return undefined\n return provider.tokens[property]\n}\n\n/** The erased shape of a resolver once its declared context type is forgotten. */\ntype ErasedResolver = (ctx: unknown) => TokenResolveResult\n\n/**\n * Invoke a token's resolver with the caller-supplied context.\n *\n * The registry is heterogeneous — every provider declares its own `Ctx` and is\n * stored as `TokenProvider<never>` — so the declared parameter type has to be\n * widened back to `unknown` at this single call site. `needs` is what makes\n * that sound at runtime: a provider only ever receives `data[needs]`, the\n * object the authorized call site itself put there.\n */\nexport function callTokenResolver(def: TokenDef<never>, ctx: unknown): TokenResolveResult {\n const resolve = def.resolve as ErasedResolver\n return resolve(ctx)\n}\n","/**\n * The resolution core (phase 03).\n *\n * ## Invariants\n *\n * 1. **Single pass, never recursive over output.** Only the INPUT template is\n * scanned for `{{ … }}`. A resolved value that happens to contain `{{ x }}`\n * is emitted literally — it is never expanded. This structurally prevents\n * injection-via-data and unbounded expansion (D002).\n * 2. **No eval, ever.** The grammar is a regex + a hand parser; filters are an\n * allowlist looked up by name.\n * 3. **Bounded.** `maxExpansions` caps tokens per call, `maxDepth` caps chain\n * hops per token — so total work is bounded by their product.\n * 4. **Zero ambient reads.** Everything the resolver can see arrives in\n * `TokenContext`; providers resolve purely over what the call site passed.\n * 5. **Contained failures.** A throwing filter, a throwing provider resolver, or\n * an `Intl` throw while stringifying (a malformed `TokenContext.locale`) is\n * caught, warned through `onWarn`, and degrades to the call's `missing`\n * policy — a template author cannot crash a send with a typo unless the call\n * site opted into `missing: 'error'` / `strict` (phase 03 §6).\n * 6. **Deterministic formatting.** With no `context.locale`, `Intl` is given\n * `DEFAULT_TOKEN_LOCALE`, never the host's default locale.\n */\n\nimport { TokenExpansionLimitError, TokenResolveError, TokenSyntaxError } from './errors.js'\nimport { escapeHtml, safeUrl } from './escape.js'\nimport {\n buildFilterRegistry,\n type FilterRegistry,\n resolveFilterCall,\n type TokenFilterContext,\n} from './filters.js'\nimport { type ParsedToken, scanTokens } from './parse.js'\nimport {\n type AnyTokenProvider,\n callTokenResolver,\n EMPTY_PROVIDER_REGISTRY,\n getTokenDef,\n type TokenDef,\n type TokenKind,\n type TokenProviderRegistry,\n} from './providers.js'\nimport {\n DEFAULT_TOKEN_LOCALE,\n type MissingPolicy,\n RESOLVE_DEFAULTS,\n type ResolveOptions,\n type TokenChainValue,\n type TokenContext,\n type TokenOutputMode,\n type TokenResolveResult,\n type TokenValue,\n} from './types.js'\n\n/** Registries + sinks a resolver is built from. All optional; all injected, never imported. */\nexport interface TokenResolverDeps {\n /** Provider allowlist for `{{ ns:prop }}` tokens. Default: empty (flat `{{ var }}` only). */\n providers?: TokenProviderRegistry\n /** Filter allowlist. Default: the built-ins only. */\n filters?: FilterRegistry\n /**\n * Sink for non-fatal diagnostics (unknown filter, throwing filter, throwing\n * provider resolver, an `Intl` throw on a malformed `TokenContext.locale`).\n * Default: `console.warn`. Injected rather than imported so the package stays\n * dependency-free.\n */\n onWarn?: (message: string) => void\n}\n\n/** A resolver bound to a provider + filter registry. */\nexport type TokenResolver = (\n text: string,\n context: TokenContext,\n options?: ResolveOptions,\n) => string\n\n/** Everything resolved for one `{{ … }}` span before output shaping. */\ninterface ResolvedToken {\n value: TokenValue\n kind: TokenKind | undefined\n}\n\nconst UNRESOLVED: ResolvedToken = { value: undefined, kind: undefined }\n\nconst WARN_PREFIX = '[@murumets-ee/tokens]'\n\n/** `true` for an object that can be a chained provider's context (never renderable). */\nfunction isChainValue(value: unknown): value is TokenChainValue {\n return typeof value === 'object' && value !== null && !(value instanceof Date)\n}\n\n/** A value that survived resolution + filtering and can be rendered. */\ntype RenderableValue = string | number | boolean | Date\n\n/**\n * Narrow a resolved value to something renderable, or `null` when there is\n * nothing safe to render (nullish, `NaN`/`Infinity`, an invalid `Date`) — those\n * follow the missing policy instead of leaking `NaN` / `Invalid Date`.\n */\nfunction toRenderable(value: TokenValue): RenderableValue | null {\n if (typeof value === 'string' || typeof value === 'boolean') return value\n if (typeof value === 'number') return Number.isFinite(value) ? value : null\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value\n // Deny-by-default: nullish, objects, arrays and functions are NOT renderable.\n // The types already forbid them; this is the runtime backstop for an untyped\n // (JS) caller or a misbehaving plugin filter, so a whole object can never be\n // stringified into an email.\n return null\n}\n\n/**\n * Default stringification for an unfiltered value — `Intl` with the call's\n * locale, never `toString()` (phase 03 §4), so output can't drift with the\n * server's system locale. The caller-omitted case resolves to\n * `DEFAULT_TOKEN_LOCALE` upstream, so `Intl` never picks the HOST default here.\n *\n * Note that this means a bare `{{ ns:count }}` number is *localized* (grouping\n * separators, `Intl`'s default 3-fraction-digit rounding). Providers that need\n * verbatim output — identifiers, reference numbers — should resolve to a\n * `string`; callers that need a specific shape should pipe `| number:2`.\n *\n * MAY THROW: `Intl` rejects a malformed BCP-47 tag with a `RangeError`. Always\n * call through {@link stringifySafely}, never directly.\n */\nfunction stringifyValue(value: RenderableValue, locale: string): string {\n if (typeof value === 'string') return value\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (value instanceof Date) {\n return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(value)\n }\n return new Intl.NumberFormat(locale).format(value)\n}\n\n/**\n * Stringify a resolved value, catching a throw.\n *\n * {@link stringifyValue} hands `TokenContext.locale` straight to `Intl`, which\n * rejects a malformed BCP-47 tag (`'not-a-locale!!'`) with a raw `RangeError`.\n * A bad locale on the context object must no more take a send down than a\n * throwing filter or a throwing provider resolver does (phase 03 §6): warn and\n * report the token as unresolved so the call's `missing` policy decides,\n * exactly as {@link callResolverSafely} does.\n *\n * Note this is the LAST of the three contained-failure paths: a locale bad\n * enough to break `Intl` also breaks every locale-aware built-in, so\n * `{{ n | number }}` first trips {@link applyFilters}' catch (value left\n * unfiltered) and then lands here.\n */\nfunction stringifySafely(\n value: RenderableValue,\n locale: string,\n raw: string,\n warn: (message: string) => void,\n): string | null {\n try {\n return stringifyValue(value, locale)\n } catch (error) {\n warn(\n `${WARN_PREFIX} formatting '${raw}' for locale '${locale}' threw: ${\n error instanceof Error ? error.message : String(error)\n }; token left unresolved`,\n )\n return null\n }\n}\n\n/** Resolve `{{ var }}` against the caller's flat vars map. */\nfunction resolveFlatVar(name: string, context: TokenContext): ResolvedToken {\n const vars = context.vars\n // `Object.hasOwn` — never reach an inherited `Object.prototype` member\n // (`{{ constructor }}` must be unresolved, not a function).\n if (vars === undefined || !Object.hasOwn(vars, name)) return UNRESOLVED\n return { value: vars[name], kind: undefined }\n}\n\n/** The context object a provider's resolvers receive, or `undefined` when unavailable. */\nfunction providerContext(\n provider: AnyTokenProvider,\n context: TokenContext,\n): { ok: true; ctx: unknown } | { ok: false } {\n const needs = provider.needs\n // No `needs` → GLOBAL provider: resolvable with no context object at all.\n if (needs === undefined) return { ok: true, ctx: undefined }\n const data = context.data\n if (data === undefined || !Object.hasOwn(data, needs)) return { ok: false }\n const ctx = data[needs]\n if (ctx === null || ctx === undefined) return { ok: false }\n return { ok: true, ctx }\n}\n\n/**\n * Invoke one token's resolver, catching a throw.\n *\n * A resolver is contracted not to throw ({@link TokenDef.resolve}); one that\n * does — a data-shape mismatch between the declared `Ctx` and what the call\n * site actually passed, a typo in a plugin provider — must not take a send down\n * with it (phase 03 §6). Warn and report the token as unresolved so the call's\n * `missing` policy decides, exactly as {@link applyFilters} does for a throwing\n * filter.\n */\nfunction callResolverSafely(\n def: TokenDef<never>,\n ctx: unknown,\n token: string,\n raw: string,\n warn: (message: string) => void,\n): { ok: true; value: TokenResolveResult } | { ok: false } {\n try {\n return { ok: true, value: callTokenResolver(def, ctx) }\n } catch (error) {\n warn(\n `${WARN_PREFIX} resolver '${token}' threw on '${raw}': ${\n error instanceof Error ? error.message : String(error)\n }; token left unresolved`,\n )\n return { ok: false }\n }\n}\n\n/**\n * Resolve `{{ ns:prop }}` / `{{ ns:prop:sub }}` through the provider graph.\n *\n * Chaining walks providers, never text — `maxDepth` bounds the walk. An object\n * value is only ever used as the next provider's context; it is never rendered.\n */\nfunction resolveProviderPath(\n path: readonly string[],\n context: TokenContext,\n providers: TokenProviderRegistry,\n maxDepth: number,\n raw: string,\n warn: (message: string) => void,\n): ResolvedToken {\n const [namespace, ...properties] = path\n if (namespace === undefined) return UNRESOLVED\n\n if (properties.length > maxDepth) {\n throw new TokenExpansionLimitError(\n `Token '${raw}' exceeds the maximum chain depth of ${maxDepth} — shorten the chain or raise maxDepth`,\n )\n }\n\n let provider = providers.get(namespace)\n if (provider === undefined) return UNRESOLVED\n\n const initial = providerContext(provider, context)\n if (!initial.ok) return UNRESOLVED\n let ctx = initial.ctx\n\n for (let i = 0; i < properties.length; i++) {\n const property = properties[i]\n if (property === undefined) return UNRESOLVED\n\n const def = getTokenDef(provider, property)\n if (def === undefined) return UNRESOLVED\n\n const resolved = callResolverSafely(def, ctx, `${provider.namespace}:${property}`, raw, warn)\n if (!resolved.ok) return UNRESOLVED\n const value = resolved.value\n const isLast = i === properties.length - 1\n\n if (isLast) {\n // An object is a chain context, not a value — never render it.\n return isChainValue(value) ? UNRESOLVED : { value, kind: def.kind }\n }\n\n if (def.chainsTo === undefined || !isChainValue(value)) return UNRESOLVED\n const next = providers.get(def.chainsTo)\n if (next === undefined) return UNRESOLVED\n provider = next\n ctx = value\n }\n\n return UNRESOLVED\n}\n\n/** Apply a token's pipe filters left→right. Never throws unless `strict`. */\nfunction applyFilters(\n token: ParsedToken,\n initial: TokenValue,\n filters: FilterRegistry,\n filterContext: TokenFilterContext,\n strict: boolean,\n warn: (message: string) => void,\n): TokenValue {\n let value = initial\n for (const call of token.filters) {\n const { name, args, filter } = resolveFilterCall(filters, call.segments)\n if (filter === undefined) {\n const message = `unknown filter '${name}' in '${token.raw}' — filters must be registered by name`\n if (strict) throw new TokenSyntaxError(`${WARN_PREFIX} ${message}`)\n warn(`${WARN_PREFIX} ${message}; token left unfiltered`)\n continue\n }\n try {\n value = filter(value, args, filterContext)\n } catch (error) {\n // A filter is contracted not to throw; one that does must not take a send\n // down with it. Warn and keep the value it was handed.\n warn(\n `${WARN_PREFIX} filter '${name}' threw on '${token.raw}': ${\n error instanceof Error ? error.message : String(error)\n }; value left unfiltered`,\n )\n }\n }\n return value\n}\n\n/**\n * Shape a resolved value for the active output mode, or `null` when it could\n * not be stringified at all (see {@link stringifySafely}) — that follows the\n * missing policy instead of crashing the call.\n */\nfunction renderValue(\n value: RenderableValue,\n kind: TokenKind | undefined,\n unescaped: boolean,\n mode: TokenOutputMode,\n locale: string,\n raw: string,\n warn: (message: string) => void,\n): string | null {\n const text = stringifySafely(value, locale, raw, warn)\n if (text === null) return null\n if (mode !== 'html') return text\n // A `url`-kind value is sanitised even inside `{{{ }}}`: there is no\n // legitimate reason for a URL to carry raw HTML, and `safeUrl` is a value\n // sanitiser (scheme allowlist), not an escaper — the two compose.\n if (kind === 'url') return escapeHtml(safeUrl(text))\n return unescaped ? text : escapeHtml(text)\n}\n\n/** What to emit when a token could not be resolved (phase 03 §5). */\nfunction renderMissing(token: ParsedToken, missing: MissingPolicy): string {\n if (missing === 'error') throw new TokenResolveError(token.expression)\n if (missing === 'empty') return ''\n return token.raw\n}\n\nfunction resolveWithRegistries(\n text: string,\n context: TokenContext,\n options: ResolveOptions | undefined,\n providers: TokenProviderRegistry,\n filters: FilterRegistry,\n warn: (message: string) => void,\n): string {\n const mode = options?.mode ?? RESOLVE_DEFAULTS.mode\n const missing = options?.missing ?? RESOLVE_DEFAULTS.missing\n const maxDepth = options?.maxDepth ?? RESOLVE_DEFAULTS.maxDepth\n const maxExpansions = options?.maxExpansions ?? RESOLVE_DEFAULTS.maxExpansions\n const strict = options?.strict ?? RESOLVE_DEFAULTS.strict\n // A fixed fallback, never `undefined`: handing `undefined` to `Intl` makes it\n // pick the HOST's default locale, so the same template + data would render\n // differently per machine.\n const locale = context.locale ?? DEFAULT_TOKEN_LOCALE\n // Frozen: one context object is shared by every filter in this call, so a\n // misbehaving plugin filter must not be able to mutate what the next one\n // sees. Filters are trusted code, not a security boundary — this is cheap\n // defense in depth.\n const filterContext: TokenFilterContext = Object.freeze({ locale })\n\n const { spans, unterminated } = scanTokens(text, { maxUnterminated: maxExpansions })\n\n if (spans.length > maxExpansions) {\n throw new TokenExpansionLimitError(\n `Template contains ${spans.length} token expansions, exceeding the limit of ${maxExpansions} — split the template or raise maxExpansions`,\n )\n }\n\n if (strict) {\n for (const span of spans) {\n if (span.malformed !== null) {\n throw new TokenSyntaxError(\n `${WARN_PREFIX} malformed token '${span.raw}' — ${span.malformed.reason}`,\n )\n }\n }\n if (unterminated > 0) {\n throw new TokenSyntaxError(`${WARN_PREFIX} ${unterminated} unterminated '{{' in template`)\n }\n }\n\n let output = ''\n let cursor = 0\n\n for (const span of spans) {\n output += text.slice(cursor, span.index)\n cursor = span.index + span.raw.length\n\n const token = span.token\n if (token === null) {\n // Malformed span — left untouched verbatim (phase 03 §2).\n output += span.raw\n continue\n }\n\n const resolved =\n token.path.length === 1\n ? resolveFlatVar(token.path[0] ?? '', context)\n : resolveProviderPath(token.path, context, providers, maxDepth, token.raw, warn)\n\n const filtered = applyFilters(token, resolved.value, filters, filterContext, strict, warn)\n const renderable = toRenderable(filtered)\n\n const rendered =\n renderable === null\n ? null\n : renderValue(renderable, resolved.kind, token.unescaped, mode, locale, token.raw, warn)\n\n if (rendered === null) {\n output += renderMissing(token, missing)\n continue\n }\n\n output += rendered\n }\n\n return output + text.slice(cursor)\n}\n\n/**\n * Build a resolver bound to a provider + filter registry.\n *\n * The registries are **injected, never imported** — that is what keeps\n * `@murumets-ee/tokens` a leaf package with no dependency on\n * `@murumets-ee/core`. The framework's merge step gathers plugin contributions,\n * builds the registries (`buildTokenProviderRegistry` / `buildFilterRegistry`),\n * and hands the bound resolver to consumers.\n *\n * ```ts\n * const resolve = createTokenResolver({\n * providers: buildTokenProviderRegistry(getContributions('tokens')),\n * filters: buildFilterRegistry(mergedTokenFilters),\n * })\n * resolve('Hi {{ ticket:requesterName }}', { data: { ticket } }, { missing: 'empty' })\n * ```\n */\nexport function createTokenResolver(deps: TokenResolverDeps = {}): TokenResolver {\n const providers = deps.providers ?? EMPTY_PROVIDER_REGISTRY\n const filters = deps.filters ?? buildFilterRegistry()\n const warn = deps.onWarn ?? ((message: string) => console.warn(message))\n\n return (text, context, options) =>\n resolveWithRegistries(text, context, options, providers, filters, warn)\n}\n\n/** The default resolver: built-in filters, no providers (flat `{{ var }}` tokens only). */\nconst defaultResolver = createTokenResolver()\n\n/**\n * Resolve `{{ … }}` tokens in `text` against `context`.\n *\n * This is the unbound convenience entry point — it knows the built-in filters\n * but has no providers, so it covers the flat `{{ var }}` case (mail/ticketing\n * back-compat). For `{{ namespace:property }}` tokens, build a resolver with\n * {@link createTokenResolver} and inject the app's provider registry.\n */\nexport function resolveTokens(\n text: string,\n context: TokenContext,\n options?: ResolveOptions,\n): string {\n return defaultResolver(text, context, options)\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"parse-COr_fTUr.d.mts","names":[],"sources":["../src/errors.ts","../src/parse.ts"],"mappings":";;AASA;;;;;;;cAAa,iBAAA,SAA0B,KAAK;EAIjB;EAAA,SAFhB,KAAA;cAEG,KAAA;AAAA;;cAQD,wBAAA,SAAiC,KAAK;cACrC,OAAA;AAAA;;cAOD,gBAAA,SAAyB,KAAK;cAC7B,OAAA;AAAA;;;;AArBd;;;;;;;;;UCciB,iBAAA;EDFJ;ECIX,IAAA;;EAEA,GAAA;EDN4C;ECQ5C,MAAA;AAAA;;UAIe,gBAAA;EDJJ;ECMX,QAAA,EAAU,iBAAiB;;EAE3B,GAAA;AAAA;;UAIe,WAAA;EDXY;ECa3B,GAAA;;EAEA,UAAA;;EAEA,SAAA;EAxBgC;EA0BhC,IAAA;EA1BgC;EA4BhC,OAAA,EAAS,gBAAgB;AAAA;;UAIV,cAAA;EACf,GAAA;EACA,UAAA;EACA,MAAA;AAAA;;UAIe,YAAA;EACf,KAAA;EACA,GAAA;EACA,KAAA,EAAO,WAAA;EACP,SAAA,EAAW,cAAc;AAAA;AAAA,UAGV,UAAA;;EAEf,KAAA,EAAO,YAAY;EA5BnB;EA8BA,YAAA;AAAA;AAAA,UA+Je,UAAA;EAtKf;;;AAAyB;AAG3B;;EA0KE,eAAe;AAAA;;;;;AAtKH;AA+Jd;;;;AAOiB;iBAaD,UAAA,CAAW,IAAA,UAAc,MAAA,GAAQ,UAAA,GAAkB,UAAU"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"parse-D_0FL4H7.mjs","names":[],"sources":["../src/errors.ts","../src/parse.ts"],"sourcesContent":["/**\n * Error classes for `@murumets-ee/tokens` (phase 03 §6).\n *\n * Resolution never throws on *content* except under `missing: 'error'` or the\n * bound guards — a template author cannot crash a send with a typo unless the\n * call site opted into strict mode.\n */\n\n/** `missing: 'error'` hit a token that could not be resolved. */\nexport class TokenResolveError extends Error {\n /** The raw token expression, e.g. `ticket:subject | upper`. */\n readonly token: string\n\n constructor(token: string) {\n super(`Token could not be resolved: '${token}' — no value for this token in the given context`)\n this.name = 'TokenResolveError'\n this.token = token\n }\n}\n\n/** `maxExpansions` or `maxDepth` exceeded — a pathological or abusive template. */\nexport class TokenExpansionLimitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'TokenExpansionLimitError'\n }\n}\n\n/** Malformed `{{ }}` or an unknown filter — only thrown in strict / lint mode. */\nexport class TokenSyntaxError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'TokenSyntaxError'\n }\n}\n","/**\n * Token grammar parsing (phase 03 §2).\n *\n * Substitution only — there is no expression language here and there never\n * will be (D002). The whole grammar is: a bounded, quote-aware scan for\n * `{{ … }}` / `{{{ … }}}` spans, plus a small hand parser that splits a span\n * into a colon-separated token path and pipe-separated filter calls.\n * **No eval, ever.**\n */\n\nimport { TokenExpansionLimitError } from './errors.js'\n\n/**\n * Longest `{{ … }}` span the scanner will consider. A real token — even one\n * with a long `default:\"…\"` literal — is far shorter than this; the cap keeps\n * a template full of stray `{{` from turning the scan quadratic.\n */\nconst MAX_SPAN_LENGTH = 1024\n\n/** Token path segments are `\\w+` — matches the mail/ticketing `{{var}}` contract we stay compatible with. */\nconst PATH_SEGMENT = /^\\w+$/\n\n/** A path segment or filter argument. */\nexport interface ExpressionSegment {\n /** The value with quotes stripped and unquoted whitespace trimmed. */\n text: string\n /** The exact source slice, quotes included — split further from THIS, never from `text`. */\n raw: string\n /** Whether any part of the segment came from a quoted literal. */\n quoted: boolean\n}\n\n/** One `| name:arg1:arg2` step of a token's filter pipe. */\nexport interface ParsedFilterCall {\n /** Colon-separated segments, quotes stripped. The name may span several of them (`rent:nights`). */\n segments: ExpressionSegment[]\n /** Raw source of this pipe step, for error/warn messages. */\n raw: string\n}\n\n/** A syntactically valid `{{ … }}` span. */\nexport interface ParsedToken {\n /** Full raw match including braces, e.g. `{{ ticket:subject | upper }}`. */\n raw: string\n /** Expression inside the braces, trimmed — e.g. `ticket:subject | upper`. */\n expression: string\n /** `true` for `{{{ … }}}` (unescaped output). */\n unescaped: boolean\n /** Colon-separated token path: `['ticket', 'subject']`, or `['name']` for a flat var. */\n path: string[]\n /** Pipe-separated filter calls, applied left→right. */\n filters: ParsedFilterCall[]\n}\n\n/** A `{{ … }}` span that is not valid token syntax (empty or a non-identifier path). */\nexport interface MalformedToken {\n raw: string\n expression: string\n reason: string\n}\n\n/** One scanned span: either a parsed token or a malformed one, plus where it sat. */\nexport interface ScannedToken {\n index: number\n raw: string\n token: ParsedToken | null\n malformed: MalformedToken | null\n}\n\nexport interface ScanResult {\n /** Every `{{ … }}` span found, in source order. */\n spans: ScannedToken[]\n /** `{{` openers that never closed — strict mode reports these; scanning ignores them. */\n unterminated: number\n}\n\n/**\n * Split on a top-level separator, honoring `'…'` / `\"…\"` quoting (so\n * `default:\"a|b\"` and `default:\"12:30\"` keep their literals) and `\\` escapes\n * inside quotes.\n *\n * Whitespace outside quotes is trimmed; whitespace inside a quoted literal is\n * preserved. Each segment also carries its `raw` source slice — a nested split\n * (pipe step → its colon-separated parts) MUST run on `raw`, because `text`\n * has already lost the quotes that protect the separator.\n */\n// NOTE: the quote/backslash-escape state machine here (entering/exiting a\n// quote, treating `\\` as consuming the next char) is intentionally mirrored\n// in `findCloser` below — both independently enforce the no-recursion rule\n// (see `findCloser`'s doc comment). If you change one, change the other, or\n// the two can silently desync brace-detection from value-extraction\n// semantics. Not extracted into a shared helper: they consume the char\n// differently (this one builds an output buffer, `findCloser` only skips),\n// and the security-critical nature of this code means a refactor here\n// deserves fresh, deliberate test coverage rather than a drive-by change.\nexport function splitTopLevel(input: string, separator: string): ExpressionSegment[] {\n const segments: ExpressionSegment[] = []\n /** Pieces of the segment currently being built (quoted runs kept verbatim). */\n let pieces: Array<{ text: string; quoted: boolean }> = []\n let buffer = ''\n let quote: string | null = null\n let segmentStart = 0\n\n const flushBuffer = (quoted: boolean): void => {\n if (buffer.length > 0 || quoted) pieces.push({ text: buffer, quoted })\n buffer = ''\n }\n const flushSegment = (raw: string): void => {\n flushBuffer(false)\n segments.push({\n text: pieces.map((p) => (p.quoted ? p.text : p.text.trim())).join(''),\n raw,\n quoted: pieces.some((p) => p.quoted),\n })\n pieces = []\n }\n\n for (let i = 0; i < input.length; i++) {\n const ch = input[i]\n if (ch === undefined) continue\n\n if (quote !== null) {\n if (ch === '\\\\') {\n const next = input[i + 1]\n if (next !== undefined) {\n buffer += next\n i++\n continue\n }\n }\n if (ch === quote) {\n flushBuffer(true)\n quote = null\n continue\n }\n buffer += ch\n continue\n }\n\n if (ch === '\"' || ch === \"'\") {\n flushBuffer(false)\n quote = ch\n continue\n }\n if (ch === separator) {\n flushSegment(input.slice(segmentStart, i))\n segmentStart = i + 1\n continue\n }\n buffer += ch\n }\n\n // An unterminated quote is treated as if it had been closed at end-of-input:\n // the value still lands in the segment, so a typo degrades to a literal\n // rather than swallowing the rest of the template.\n flushBuffer(quote !== null)\n flushSegment(input.slice(segmentStart))\n\n return segments\n}\n\n/**\n * Parse the inside of a `{{ … }}` span. Returns an `error` when the span is not\n * valid token syntax — callers leave malformed spans untouched unless running\n * in strict mode.\n */\nexport function parseExpression(\n expression: string,\n): { token: Omit<ParsedToken, 'raw' | 'expression' | 'unescaped'> } | { error: string } {\n const pipeSegments = splitTopLevel(expression, '|')\n const head = pipeSegments[0]\n if (head === undefined || head.text === '') {\n return { error: 'empty token expression' }\n }\n\n const path = splitTopLevel(head.raw, ':').map((segment) => segment.text)\n if (path.some((segment) => !PATH_SEGMENT.test(segment))) {\n return { error: `invalid token path '${head.text}'` }\n }\n\n const filters: ParsedFilterCall[] = []\n for (let i = 1; i < pipeSegments.length; i++) {\n const step = pipeSegments[i]\n if (step === undefined) continue\n const segments = splitTopLevel(step.raw, ':')\n const name = segments[0]\n if (name === undefined || name.text === '') {\n return { error: 'empty filter name in pipe' }\n }\n filters.push({ segments, raw: step.raw.trim() })\n }\n\n return { token: { path, filters } }\n}\n\n/**\n * Find the index of `closer` at or after `from`, skipping over quoted literals.\n *\n * Quote-awareness is what enforces the no-recursion-in-defaults rule: the inner\n * braces of `{{ x | default:\"{{ y }}\" }}` belong to the literal, so `{{ y }}`\n * is never seen as a token of its own and can never be expanded (D002, R01).\n *\n * The quote/escape tracking below is intentionally mirrored in\n * {@link splitTopLevel} — see the note on its state machine. Keep both in sync.\n *\n * Returns `-1` when no closer is found within {@link MAX_SPAN_LENGTH}.\n */\nfunction findCloser(text: string, from: number, closer: string): number {\n const limit = Math.min(text.length, from + MAX_SPAN_LENGTH)\n let quote: string | null = null\n\n for (let i = from; i < limit; i++) {\n const ch = text[i]\n if (ch === undefined) break\n if (quote !== null) {\n if (ch === '\\\\') {\n i++\n continue\n }\n if (ch === quote) quote = null\n continue\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch\n continue\n }\n if (text.startsWith(closer, i)) return i\n }\n\n return -1\n}\n\nexport interface ScanLimits {\n /**\n * Max unterminated `{{` openers tolerated before the template is rejected.\n * Every unterminated opener costs a bounded (≤ {@link MAX_SPAN_LENGTH})\n * forward scan, so capping them keeps a `{{{{{{…` template from burning CPU.\n * Default 500 — the same order as `maxExpansions`.\n */\n maxUnterminated?: number\n}\n\n/**\n * Scan every `{{ … }}` / `{{{ … }}}` span in `text`, in source order.\n *\n * Pure and non-recursive: this only ever looks at the INPUT template. Resolved\n * values are never fed back through it — the core anti-injection invariant\n * (phase 03 §3/§7).\n *\n * @throws {TokenExpansionLimitError} when the template has more than\n * `maxUnterminated` unterminated `{{` openers.\n */\nexport function scanTokens(text: string, limits: ScanLimits = {}): ScanResult {\n const maxUnterminated = limits.maxUnterminated ?? 500\n const spans: ScannedToken[] = []\n let unterminated = 0\n let i = 0\n\n while (i < text.length) {\n if (text[i] !== '{' || text[i + 1] !== '{') {\n i++\n continue\n }\n\n const unescaped = text[i + 2] === '{'\n const openLength = unescaped ? 3 : 2\n const closer = unescaped ? '}}}' : '}}'\n const end = findCloser(text, i + openLength, closer)\n\n if (end === -1) {\n // Unterminated opener: not a token. Advance one char so a `{{{` that has\n // only `}}` can still be read as a `{{` span, and leave the text alone.\n unterminated++\n if (unterminated > maxUnterminated) {\n throw new TokenExpansionLimitError(\n `Template contains more than ${maxUnterminated} unterminated '{{' openers — refusing to scan further`,\n )\n }\n i++\n continue\n }\n\n const raw = text.slice(i, end + closer.length)\n const expression = text.slice(i + openLength, end).trim()\n const parsed = parseExpression(expression)\n\n spans.push(\n 'error' in parsed\n ? { index: i, raw, token: null, malformed: { raw, expression, reason: parsed.error } }\n : {\n index: i,\n raw,\n token: { raw, expression, unescaped, ...parsed.token },\n malformed: null,\n },\n )\n\n i = end + closer.length\n }\n\n return { spans, unterminated }\n}\n"],"mappings":"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"}