fewrd 1.0.0 → 1.0.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
@@ -50,10 +50,7 @@ Rendering the tree is yours. fewrd stops at data.
50
50
  npm install fewrd
51
51
  ```
52
52
 
53
- ESM only, zero dependencies, types included. Runs in current Node and in any current browser or bundler.
54
-
55
- > [!IMPORTANT]
56
- > This README describes 1.0.0, a rewrite. Until it is on npm, `npm install fewrd` still gets 0.3.0, the older book-and-recipes API; the [changelog](CHANGELOG.md) says what changed and how to upgrade. Meanwhile, build this repository (`pnpm install && pnpm build`) and depend on the checkout.
53
+ ESM only, zero dependencies, types included. Runs in current Node and in any current browser or bundler. Coming from 0.3.0, the book-and-recipes API: the [changelog](CHANGELOG.md) says what changed and how to upgrade.
57
54
 
58
55
  ## Quick start
59
56
 
@@ -561,6 +558,7 @@ pnpm install
561
558
  pnpm typecheck # tsc --strict, no emit
562
559
  pnpm test # node --test, TypeScript type-stripped: no build
563
560
  pnpm dev # the playground, on http://localhost:5577
561
+ pnpm bench <conf-dir> [texts] # times find and dom apart over a fewrd-play conf folder
564
562
  ```
565
563
 
566
564
  That loop has no build step. `pnpm build`, run at publish and before running `fewrd-play` from a checkout, bundles the two public entries with vite into `dist/` and writes their types with `tsc`; `dist/` is never committed. The gate for any change is `pnpm typecheck` and `pnpm test`. It is developed on Node 25 and pnpm 10; the loop needs a Node that runs TypeScript files directly. There is no CI: the gate is run locally. One file runs on its own with `node --test test/fold.test.ts`.
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- const P=["^","$","*"],ye=["doc","text"],ke=["value",...P],F={conf:["version","patterns","tags"],tag:["rx","search","resolve","weak","fate"],search:["from","back","forward"],atom:["tag","rx","as","optional"]},xe=/^[A-Za-z_][A-Za-z0-9_]*$/,Ee=/%\{([A-Za-z_][A-Za-z0-9_]*)\}/g,z=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),G=e=>typeof e=="string"&&e!=="",Oe=(e,t)=>e?`${e}.${t}`:t;function V(e,t,n,s){for(const o of Object.keys(e))t.includes(o)||s.push({path:Oe(n,o),message:`unknown key "${o}"`})}function ae(e,t,n=[]){return e.replace(Ee,(s,o)=>{const a=[...n,o];if(n.includes(o))throw new Error(`circular pattern ${a.join(" \u2192 ")}`);const c=t.get(o);if(c===void 0)throw new Error(`unknown pattern %{${o}}`);return`(?:${ae(c,t,a)})`})}function ie(e,t,n,s){if(e===void 0)return void s.push({path:t,message:"is required"});const o=typeof e=="string"&&e[0]==="/"?e.lastIndexOf("/"):-1;if(typeof e!="string"||o<1)return void s.push({path:t,message:"must be a pattern string like /source/flags"});try{return new RegExp(ae(e.slice(1,o),n.patterns),e.slice(o+1))}catch(a){s.push({path:t,message:a instanceof Error?a.message:String(a)})}}function ce(e,t,n,s){if(!G(e))return void s.push({path:t,message:"must be a non-empty string (a tag name)"});!P.includes(e)&&!n.declared.has(e)&&s.push({path:t,message:`unknown tag "${e}"`})}function Me(e,t,n,s){if(!z(e))return void s.push({path:t,message:"must be an object (an atom)"});const o=s.length;V(e,F.atom,t,s),"tag"in e=="rx"in e&&s.push({path:t,message:"must have exactly one of tag and rx"}),e.as!==void 0&&(G(e.as)?ke.includes(e.as)&&s.push({path:`${t}.as`,message:`"${e.as}" is reserved and cannot name a role`}):s.push({path:`${t}.as`,message:"must be a non-empty string (a role name)"})),e.optional!==void 0&&typeof e.optional!="boolean"&&s.push({path:`${t}.optional`,message:"must be a boolean"});let a;if("rx"in e?a=ie(e.rx,`${t}.rx`,n,s):"tag"in e&&(Array.isArray(e.tag)?(e.tag.length||s.push({path:`${t}.tag`,message:"must not be an empty list"}),e.tag.forEach((g,f)=>ce(g,`${t}.tag[${f}]`,n,s))):ce(e.tag,`${t}.tag`,n,s)),s.length>o)return;const c={...e.as!==void 0&&{as:e.as},...e.optional===!0&&{optional:!0}};return a?{rx:a,...c}:{tag:e.tag,...c}}function fe(e,t,n,s){if(!Array.isArray(e)||!e.length)return void s.push({path:t,message:"must be a non-empty array of atoms"});const o=s.length;let a=!1;const c=e.map((f,u)=>{const w=`${t}[${u}]`,y=Me(f,w,n,s),m=z(f)&&"rx"in f;return m&&a&&s.push({path:w,message:"two regex atoms in a row (optional atoms between do not separate them): merge them into one pattern"}),a=m||a&&z(f)&&f.optional===!0,y}),g=e[e.length-1];return z(g)&&g.optional===!0&&s.push({path:`${t}[${e.length-1}]`,message:"the outermost atom must not be optional"}),s.length>o?void 0:c}function Se(e,t,n,s){if(!z(e))return void s.push({path:t,message:"must be an object (a search)"});const o=s.length;V(e,F.search,t,s),G(e.from)?(P.includes(e.from)||!n.declared.has(e.from))&&s.push({path:`${t}.from`,message:`unknown tag "${e.from}"`}):s.push({path:`${t}.from`,message:"must be a non-empty string (a tag name)"}),"back"in e=="forward"in e&&s.push({path:t,message:"must have exactly one of back and forward"});const a="back"in e?fe(e.back,`${t}.back`,n,s):void 0,c="forward"in e?fe(e.forward,`${t}.forward`,n,s):void 0;if(!(s.length>o))return{from:e.from,...a&&{back:a},...c&&{forward:c}}}function je(e,t,n,s){if(!z(e))return void s.push({path:t,message:"must be an object (a tag)"});const o=s.length;V(e,F.tag,t,s),"rx"in e=="search"in e&&s.push({path:t,message:"must have exactly one of rx and search"});const a="rx"in e?ie(e.rx,`${t}.rx`,n,s):void 0;let c;"search"in e&&(!Array.isArray(e.search)||!e.search.length?s.push({path:`${t}.search`,message:"must be a non-empty array of searches"}):c=e.search.map((f,u)=>Se(f,`${t}.search[${u}]`,n,s)).filter(f=>f!==void 0));let g;if(e.resolve!==void 0&&(typeof e.resolve!="string"?s.push({path:`${t}.resolve`,message:"must be a string (a resolver name)"}):Object.hasOwn(n.resolvers,e.resolve)?g=n.resolvers[e.resolve]:s.push({path:`${t}.resolve`,message:`unknown resolver "${e.resolve}"`})),e.weak!==void 0&&typeof e.weak!="boolean"&&s.push({path:`${t}.weak`,message:"must be a boolean"}),e.fate!==void 0&&e.fate!=="separator"&&e.fate!=="connector"&&e.fate!=="bracket"&&s.push({path:`${t}.fate`,message:'must be "separator", "connector" or "bracket"'}),!(s.length>o))return{...a&&{rx:a},...c&&{search:c},...g&&{resolve:g},...e.weak===!0&&{weak:!0},...e.fate!==void 0&&{fate:e.fate}}}function Ae(e,t={}){const n=[];if(!z(e))return{conf:{version:"",tags:{}},errors:[{path:"",message:"must be an object (a conf)"}]};V(e,F.conf,"",n);let s="";G(e.version)?s=e.version:n.push({path:"version",message:e.version===void 0?"is required":"must be a non-empty string"});const o=new Map;if(e.patterns!==void 0&&!z(e.patterns))n.push({path:"patterns",message:"must be an object of name \u2192 pattern source"});else if(e.patterns)for(const[f,u]of Object.entries(e.patterns))xe.test(f)?typeof u!="string"?n.push({path:`patterns.${f}`,message:"must be a string (pattern source)"}):o.set(f,u):n.push({path:`patterns.${f}`,message:"a pattern name is letters, digits and _, not starting with a digit"});const a=z(e.patterns)?{patterns:e.patterns}:{};if(!z(e.tags))return n.push({path:"tags",message:e.tags===void 0?"is required":"must be an object of tags"}),{conf:{version:s,...a,tags:{}},errors:n};const c={patterns:o,resolvers:t.resolvers??{},declared:new Set(Object.keys(e.tags))},g={};for(const[f,u]of Object.entries(e.tags)){const w=[];(P.includes(f)||ye.includes(f))&&w.push({path:`tags.${f}`,message:`"${f}" is reserved and cannot be declared as a tag`});const y=je(u,`tags.${f}`,c,w);for(const m of w)n.push({...m,tag:f});!w.length&&y&&(g[f]=y)}return{conf:{version:s,...a,tags:g},errors:n}}function ee(e){const t={starts:new Map,ends:new Map,all:[...e]};for(const n of t.all)for(const[s,o]of[[t.starts,n[1][0]],[t.ends,n[1][1]]]){const a=s.get(o);a?a.push(n):s.set(o,[n])}return t}const C=(e,t)=>e[0]-t[0]||e[1]-t[1],le=(e,t)=>e<t?-1:e>t?1:0,ue=[];class I{#e;#s;constructor(t,n){this.#e=t,this.#s=n}static empty(t){return new I(new Map([["^",[[0,0]]],["$",[[t,t]]]]),t)}static from(t){const n=Object.entries(t).flatMap(([o,a])=>a.map(c=>[o,[c[0],c[1]]])),s=t.$?.[0]?.[1]??Math.max(0,...n.map(([,o])=>o[1]));return I.empty(s).with(n)}toJSON(){const t={};for(const n of[...this.#e.keys()].sort(le))t[n]=this.#e.get(n).map(s=>[s[0],s[1]]);return t}with(t){const n=new Map;for(const[o,a]of t){const c=n.get(o);c?c.push(a):n.set(o,[a])}let s;for(const[o,a]of n){const c=this.#e.get(o)??ue,g=new Set(c.map(u=>`${u[0]},${u[1]}`)),f=[...c];for(const u of a)!g.has(`${u[0]},${u[1]}`)&&g.add(`${u[0]},${u[1]}`)&&f.push(u);f.length!==c.length&&(s??=new Map(this.#e)).set(o,f.sort(C))}return s?new I(s,this.#s):this}size(){let t=0;for(const n of this.#e.values())t+=n.length;return t}#t(t){if(t==="*")return this.#e.values();const n=this.#e.get(t);return n?[n]:[]}has(t,n,s){for(const o of this.#t(t))if(o.some(a=>a[0]===n&&a[1]===s))return!0;return!1}after(t,n){let s;for(const o of this.#t(t)){let a=0,c=o.length;for(;a<c;){const g=a+c>>1;o[g][0]<n?a=g+1:c=g}a<o.length&&(!s||C(o[a],s)<0)&&(s=o[a])}return s}before(t,n){let s;for(const o of this.#t(t))for(let a=o.length-1;a>=0;a--)if(!(o[a][1]>n)){(!s||C(o[a],s)>0)&&(s=o[a]);break}return s}spans(t){return this.#e.get(t)??ue}all(){const t=[];for(const[n,s]of this.#e)for(const o of s)t.push([n,o]);return t.sort((n,s)=>C(n[1],s[1])||le(n[0],s[0]))}edges(){return ee(this.all())}}function H(e,t){const[n,s]=e,[o,a]=t;return n===o&&s===a?"equals":s<o?"before":a<n?"after":s===o?"meets":a===n?"met-by":n<o?s<a?"overlaps":s===a?"finished-by":"contains":n===o?s<a?"starts":"started-by":s<a?"during":s===a?"finishes":"overlapped-by"}const J=e=>e.replace(/[gyd]/g,""),he=new WeakMap;function Q(e,t){let n=he.get(e);return n||he.set(e,n={}),n[t]??=t==="scan"?new RegExp(e.source,`${J(e.flags)}g`):t==="back"?new RegExp(`(?:${e.source})$`,J(e.flags)):t==="forward"?new RegExp(e.source,`${J(e.flags)}y`):new RegExp(`^(?:${e.source})$`,J(e.flags))}function ge(e,t,n,s,o,a){const c=s.forward!==void 0,g=s.forward??s.back??[],f=(y,m,x)=>m===void 0?y:{...y,[m]:x},u=(y,m,x)=>m===void 0?y:[...y,{text:x,as:m}],w=(y,m,x,M,l,h,$)=>{if(y===g.length){if(!$)return a(x,M,l,h);if(c){const k=Q($.rx,"forward");k.lastIndex=m;const A=k.exec(e);A&&a(x,m+A[0].length,f(l,$.as,A[0]),u(h,$.as,A[0]))}else{const k=Q($.rx,"back").exec(e.slice(0,m));k&&a(k.index,M,f(l,$.as,k[0]),u(h,$.as,k[0]))}return}const d=g[y];if(d.optional&&w(y+1,m,x,M,l,h,$),"rx"in d){$||w(y+1,m,x,M,l,h,{rx:d.rx,...d.as!==void 0&&{as:d.as}});return}const R=typeof d.tag=="string"?[d.tag]:d.tag,j=k=>R.includes("*")||R.includes(k),E=(k,A,X)=>{const[T,Z]=k[1];w(y+1,c?Z:T,c?x:T,c?Z:M,f(A,d.as,n(k)),[...X,{row:k,...d.as!==void 0&&{as:d.as}}])};if(!$){for(const k of(c?t.starts:t.ends).get(m)??[])j(k[0])&&E(k,l,h);return}const W=Q($.rx,"whole");for(const k of t.all){if(!j(k[0])||(c?k[1][0]<m:k[1][1]>m))continue;const A=c?e.slice(m,k[1][0]):e.slice(k[1][1],m);W.test(A)&&E(k,f(l,$.as,A),u(h,$.as,A))}};w(0,c?o[1][1]:o[1][0],o[1][0],o[1][1],{},[{row:o}])}const Re=/[‐-―−]/g,Ne=/[‘-‟]/g,ze=e=>"\u201C\u201D\u201E\u201F".includes(e)?'"':"'";function de(e){let t="";const n=[];let s=!1,o=0;for(const a of e){if(/\s/.test(a))s||(n.push(o),t+=" "),s=!0;else{const c=a.normalize("NFKC").replace(Re,"-").replace(Ne,ze);for(let g=0;g<c.length;g++)n.push(o);t+=c,s=!1}o+=a.length}return n.push(e.length),{text:t,at:n}}const qe=/[\p{L}\p{N}]/u,U=e=>e!==void 0&&qe.test(e),Ie=(e,t)=>U(e[t])&&U(e[t-1]),We=(e,t)=>U(e[t-1])&&U(e[t]),Y=(e,t,n)=>`${e}\0${t}\0${n}`;function De(e,t,n){const s=[];for(const[o,a]of Object.entries(t.tags)){if(!a.rx)continue;const c=Q(a.rx,"scan");c.lastIndex=0;for(let g=c.exec(e);g;g=c.exec(e)){const f=g.index,u=f+g[0].length;if(c.lastIndex=f+(e.codePointAt(f)>65535?2:1),!(u===f||Ie(e,f)||We(e,u))){if(a.resolve){const w=a.resolve({value:g[0]});if(w===null)continue;n.set(Y(o,f,u),w)}s.push([o,[f,u]])}}}return s}function Te(e,t){const{text:n,at:s}=de(e),o=new Map;let a=I.empty(n.length).with(De(n,t,o));const c=Object.keys(t.tags).sort(),g=([f,[u,w]])=>o.get(Y(f,u,w))??n.slice(u,w);for(;;){const f=a.edges(),u=[];for(const y of c){const m=t.tags[y];for(const x of m.search??[])for(const M of a.spans(x.from))ge(n,f,g,x,[x.from,M],(l,h,$)=>{if(m.resolve){if(o.has(Y(y,l,h)))return;const d=m.resolve({...$,value:n.slice(l,h)});if(d===null)return;o.set(Y(y,l,h),d)}u.push([y,[l,h]])})}const w=a.with(u);if(w.size()===a.size())break;a=w}return I.empty(e.length).with([...a.all()].map(([f,[u,w]])=>[f,[s[u],s[w]]]))}const O=([e,[t,n]])=>`${e}\0${t}\0${n}`,Ze=(e,t)=>e[1][0]-t[1][0]||e[1][1]-t[1][1]||(e[0]<t[0]?-1:e[0]>t[0]?1:0),pe=(e,t)=>{const n=H(e,t);return n==="overlaps"||n==="overlapped-by"},_e=(e,t)=>{const n=H(e,t);return n==="starts"||n==="during"||n==="finishes"},Ke=e=>e[0]==="^"||e[0]==="$";function Le(e,t,n){const{text:s,at:o}=de(e),a=s.length,c=new Map,g=new Map;o.forEach((r,i)=>{c.has(r)||c.set(r,i),g.set(r,i)});const f=new Map(Object.keys(n.tags).map((r,i)=>[r,i])),u=[],w=[];for(const[r,[i,p]]of t.all())if(r==="^")u.push(["^",[0,0]]);else if(r==="$")u.push(["$",[a,a]]);else{const v=f.get(r);if(v===void 0)throw new Error(`the chart has rows of "${r}", which the conf does not declare`);const b=c.get(i),S=g.get(p);if(b===void 0||S===void 0)throw new Error(`${r} (${i},${p}) is not on a boundary of the text: is this the chart of another text?`);const N=[r,[b,S]];u.push(N),w.push({row:N,weak:n.tags[r].weak===!0,rank:v})}u.sort(Ze);const y=r=>`${r[0]} (${o[r[1][0]]},${o[r[1][1]]})`,m=new Map,x=new Map,M=new Set,l=new Set;function h(r){const i=O(r),p=m.get(i);if(p)return p;if(M.has(i))return[];M.add(i);const[v,[b,S]]=r,N=n.tags[v],se=u.filter(D=>b<=D[1][0]&&D[1][1]<=S&&O(D)!==i),ve=ee(se),_=[];for(const D of N.search??[])for(const ne of se)ne[0]===D.from&&ge(s,ve,$,D,ne,(we,$e,be,oe)=>{if(we!==b||$e!==S)return;if(!N.resolve)return void _.push({steps:oe});const re=N.resolve({...be,value:s.slice(b,S)});re===null?l.add(i):_.push({steps:oe,value:re})});if(M.delete(i),!_.length)throw new Error(l.has(i)?`resolver of "${v}" refused (${o[b]},${o[S]}) at dom time`:`no derivation of ${y(r)}: is this the chart of this conf?`);return m.set(i,_),_}function $(r){const i=n.tags[r[0]];if(!i?.resolve)return s.slice(r[1][0],r[1][1]);const p=O(r);let v=x.get(p);if(v===void 0){if(i.search)v=h(r)[0].value;else{const b=i.resolve({value:s.slice(r[1][0],r[1][1])});if(b===null)throw new Error(`resolver of "${r[0]}" refused (${o[r[1][0]]},${o[r[1][1]]}) at dom time`);v=b}x.set(p,v)}return v}w.sort((r,i)=>Number(r.weak)-Number(i.weak)||i.row[1][1]-i.row[1][0]-(r.row[1][1]-r.row[1][0])||r.rank-i.rank||r.row[1][0]-i.row[1][0]);let d=[],R=new Set,j=new Map,E=new Map,W=new Map;function k(){const r={chosen:[...d],keys:new Set(R),also:new Map([...j].map(([i,p])=>[i,[...p]])),used:new Map(E),twinOf:new Map(W)};return()=>{({chosen:d,keys:R,also:j,used:E,twinOf:W}=r)}}const A=r=>d.find(i=>i[0]!==r[0]&&H(r[1],i[1])==="equals");function X(r,i){const p=j.get(O(i));return p?p.includes(r[0])||p.push(r[0]):j.set(O(i),[r[0]]),W.set(O(r),O(i)),!0}function T(r,i){if(i!==void 0&&r[0]===i)return Z(r,i,!1);if(R.has(O(r)))return!0;const p=A(r);if(p)return X(r,p);if(d.some(b=>pe(r[1],b[1])))return!1;if(!n.tags[r[0]].search)return d.push(r),R.add(O(r)),!0;const v=k();return d.push(r),R.add(O(r)),Z(r,r[0],!0)?!0:(v(),!1)}function Z(r,i,p){for(const v of h(r)){const b=k();if(v.steps.every(S=>!("row"in S)||Ke(S.row)||T(S.row,i)))return p&&E.set(O(r),v),!0;b()}return!1}for(const{row:r}of w)R.has(O(r))||d.some(i=>pe(r[1],i[1]))||d.some(i=>i[0]===r[0]&&_e(r[1],i[1]))||T(r);const K={tag:"doc",start:0,end:e.length,attrs:{},text:e,children:[]},L=new Map,q=[{node:K,lo:0,hi:a}];for(const r of[...d].sort((i,p)=>i[1][0]-p[1][0]||p[1][1]-i[1][1])){const[i,[p,v]]=r;for(;q[q.length-1].lo>p||q[q.length-1].hi<v;)q.pop();const b=n.tags[i],S=b.resolve?b.search?E.get(O(r)).value:$(r):void 0,N={tag:i,start:o[p],end:o[v],...S!==void 0&&{value:S},attrs:{},...j.has(O(r))&&{also:j.get(O(r))},...b.fate!==void 0&&{fate:b.fate},children:[]};q[q.length-1].node.children.push(N),q.push({node:N,lo:p,hi:v}),L.set(O(r),N)}for(const r of d){const i=E.get(O(r));if(!i)continue;const p=L.get(O(r));for(const v of i.steps)if(v.as!==void 0)if("row"in v){const b=O(v.row);p.attrs[v.as]=L.get(b)??L.get(W.get(b)??"")??s.slice(v.row[1][0],v.row[1][1])}else p.attrs[v.as]=v.text}const te=r=>{if(!r.children.length&&(r!==K||!r.end))return;const i=[];let p=r.start;const v=b=>{b>p&&i.push({tag:"text",start:p,end:b,attrs:{},children:[]})};for(const b of r.children)v(b.start),i.push(b),p=b.end,te(b);v(r.end),r.children=i};return te(K),K}const Pe=/^[.,;:!?)\]}]/;function B(e){return e.children.length?e.children.flatMap(B):[e]}function me(e,t){const n=e.text;if(n===void 0)throw new Error("the tree has no text: build it with dom, or keep `text` on the root when you parse it back");const s=new Set,o=l=>{s.add(l),l.children.forEach(o)},a=l=>l.fate==="separator",c=l=>n.slice(l.start,l.end),g=l=>{for(const h of l.children)t(h)?o(h):g(h)};g(e);const f=e.children.length?B(e):[];for(let l=f.length-1;l>=0;l--){if(f[l].fate!=="connector"||s.has(f[l]))continue;const h=f.slice(l+1).find($=>!a($));h&&s.has(h)&&s.add(f[l])}const u=new Set,w=l=>{if(l.children.forEach(w),l.fate!=="bracket"||s.has(l)||l.children.length<2)return;const h=l.children,$=E=>E.tag==="text"&&E.end-E.start===1,d=$(h[0])?h[0]:void 0,R=$(h[h.length-1])?h[h.length-1]:void 0,j=B(l).filter(E=>E!==d&&E!==R&&!a(E));j.some(E=>s.has(E))&&j.every(E=>s.has(E))?o(l):d&&u.add(d)};w(e);const y=l=>{const h=c(l);return h.includes("-")?4:h.includes(";")?3:h.includes(":")?2:h.includes(",")?1:0};let m,x=[];const M=l=>{if(x.some(h=>s.has(h))){const h=x.filter(d=>a(d)&&!s.has(d));let $;for(const d of h)(!$||y(d)>y($))&&($=d);for(const d of h)d!==$&&s.add(d);$&&(!m||!l||u.has(m)||Pe.test(c(l)))&&s.add($)}x=[]};for(const l of f)a(l)||s.has(l)?x.push(l):(M(l),m=l);return M(void 0),s}function Fe(e,t){const n=me(e,t),s=e.text;return(e.children.length?B(e):[]).filter(o=>!n.has(o)).map(o=>s.slice(o.start,o.end)).join("")}export{I as Chart,Ae as compile,Le as dom,ee as edges,Te as find,Fe as gist,me as hidden,H as rel};
1
+ const G=["^","$","*"],Ae=["doc","text"],Se=["value",...G],V={conf:["version","patterns","tags"],tag:["rx","search","resolve","weak","fate"],search:["from","back","forward"],atom:["tag","rx","as","optional"]},je=/^[A-Za-z_][A-Za-z0-9_]*$/,Ne=/%\{([A-Za-z_][A-Za-z0-9_]*)\}/g,T=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),H=e=>typeof e=="string"&&e!=="",Re=(e,t)=>e?`${e}.${t}`:t;function J(e,t,n,s){for(const r of Object.keys(e))t.includes(r)||s.push({path:Re(n,r),message:`unknown key "${r}"`})}function fe(e,t,n=[]){return e.replace(Ne,(s,r)=>{const a=[...n,r];if(n.includes(r))throw new Error(`circular pattern ${a.join(" \u2192 ")}`);const i=t.get(r);if(i===void 0)throw new Error(`unknown pattern %{${r}}`);return`(?:${fe(i,t,a)})`})}function le(e,t,n,s){if(e===void 0)return void s.push({path:t,message:"is required"});const r=typeof e=="string"&&e[0]==="/"?e.lastIndexOf("/"):-1;if(typeof e!="string"||r<1)return void s.push({path:t,message:"must be a pattern string like /source/flags"});try{return new RegExp(fe(e.slice(1,r),n.patterns),e.slice(r+1))}catch(a){s.push({path:t,message:a instanceof Error?a.message:String(a)})}}function ue(e,t,n,s){if(!H(e))return void s.push({path:t,message:"must be a non-empty string (a tag name)"});!G.includes(e)&&!n.declared.has(e)&&s.push({path:t,message:`unknown tag "${e}"`})}function Ie(e,t,n,s){if(!T(e))return void s.push({path:t,message:"must be an object (an atom)"});const r=s.length;J(e,V.atom,t,s),"tag"in e=="rx"in e&&s.push({path:t,message:"must have exactly one of tag and rx"}),e.as!==void 0&&(H(e.as)?Se.includes(e.as)&&s.push({path:`${t}.as`,message:`"${e.as}" is reserved and cannot name a role`}):s.push({path:`${t}.as`,message:"must be a non-empty string (a role name)"})),e.optional!==void 0&&typeof e.optional!="boolean"&&s.push({path:`${t}.optional`,message:"must be a boolean"});let a;if("rx"in e?a=le(e.rx,`${t}.rx`,n,s):"tag"in e&&(Array.isArray(e.tag)?(e.tag.length||s.push({path:`${t}.tag`,message:"must not be an empty list"}),e.tag.forEach((p,c)=>ue(p,`${t}.tag[${c}]`,n,s))):ue(e.tag,`${t}.tag`,n,s)),s.length>r)return;const i={...e.as!==void 0&&{as:e.as},...e.optional===!0&&{optional:!0}};return a?{rx:a,...i}:{tag:e.tag,...i}}function he(e,t,n,s){if(!Array.isArray(e)||!e.length)return void s.push({path:t,message:"must be a non-empty array of atoms"});const r=s.length;let a=!1;const i=e.map((c,w)=>{const $=`${t}[${w}]`,k=Ie(c,$,n,s),v=T(c)&&"rx"in c;return v&&a&&s.push({path:$,message:"two regex atoms in a row (optional atoms between do not separate them): merge them into one pattern"}),a=v||a&&T(c)&&c.optional===!0,k}),p=e[e.length-1];return T(p)&&p.optional===!0&&s.push({path:`${t}[${e.length-1}]`,message:"the outermost atom must not be optional"}),s.length>r?void 0:i}function ze(e,t,n,s){if(!T(e))return void s.push({path:t,message:"must be an object (a search)"});const r=s.length;J(e,V.search,t,s),H(e.from)?(G.includes(e.from)||!n.declared.has(e.from))&&s.push({path:`${t}.from`,message:`unknown tag "${e.from}"`}):s.push({path:`${t}.from`,message:"must be a non-empty string (a tag name)"}),"back"in e=="forward"in e&&s.push({path:t,message:"must have exactly one of back and forward"});const a="back"in e?he(e.back,`${t}.back`,n,s):void 0,i="forward"in e?he(e.forward,`${t}.forward`,n,s):void 0;if(!(s.length>r))return{from:e.from,...a&&{back:a},...i&&{forward:i}}}function Te(e,t,n,s){if(!T(e))return void s.push({path:t,message:"must be an object (a tag)"});const r=s.length;J(e,V.tag,t,s),"rx"in e=="search"in e&&s.push({path:t,message:"must have exactly one of rx and search"});const a="rx"in e?le(e.rx,`${t}.rx`,n,s):void 0;let i;"search"in e&&(!Array.isArray(e.search)||!e.search.length?s.push({path:`${t}.search`,message:"must be a non-empty array of searches"}):i=e.search.map((c,w)=>ze(c,`${t}.search[${w}]`,n,s)).filter(c=>c!==void 0));let p;if(e.resolve!==void 0&&(typeof e.resolve!="string"?s.push({path:`${t}.resolve`,message:"must be a string (a resolver name)"}):Object.hasOwn(n.resolvers,e.resolve)?p=n.resolvers[e.resolve]:s.push({path:`${t}.resolve`,message:`unknown resolver "${e.resolve}"`})),e.weak!==void 0&&typeof e.weak!="boolean"&&s.push({path:`${t}.weak`,message:"must be a boolean"}),e.fate!==void 0&&e.fate!=="separator"&&e.fate!=="connector"&&e.fate!=="bracket"&&s.push({path:`${t}.fate`,message:'must be "separator", "connector" or "bracket"'}),!(s.length>r))return{...a&&{rx:a},...i&&{search:i},...p&&{resolve:p},...e.weak===!0&&{weak:!0},...e.fate!==void 0&&{fate:e.fate}}}function We(e,t={}){const n=[];if(!T(e))return{conf:{version:"",tags:{}},errors:[{path:"",message:"must be an object (a conf)"}]};J(e,V.conf,"",n);let s="";H(e.version)?s=e.version:n.push({path:"version",message:e.version===void 0?"is required":"must be a non-empty string"});const r=new Map;if(e.patterns!==void 0&&!T(e.patterns))n.push({path:"patterns",message:"must be an object of name \u2192 pattern source"});else if(e.patterns)for(const[c,w]of Object.entries(e.patterns))je.test(c)?typeof w!="string"?n.push({path:`patterns.${c}`,message:"must be a string (pattern source)"}):r.set(c,w):n.push({path:`patterns.${c}`,message:"a pattern name is letters, digits and _, not starting with a digit"});const a=T(e.patterns)?{patterns:e.patterns}:{};if(!T(e.tags))return n.push({path:"tags",message:e.tags===void 0?"is required":"must be an object of tags"}),{conf:{version:s,...a,tags:{}},errors:n};const i={patterns:r,resolvers:t.resolvers??{},declared:new Set(Object.keys(e.tags))},p={};for(const[c,w]of Object.entries(e.tags)){const $=[];(G.includes(c)||Ae.includes(c))&&$.push({path:`tags.${c}`,message:`"${c}" is reserved and cannot be declared as a tag`});const k=Te(w,`tags.${c}`,i,$);for(const v of $)n.push({...v,tag:c});!$.length&&k&&(p[c]=k)}return{conf:{version:s,...a,tags:p},errors:n}}function ee(e){const t={starts:new Map,ends:new Map,all:[...e]};for(const n of t.all)for(const[s,r]of[[t.starts,n[1][0]],[t.ends,n[1][1]]]){const a=s.get(r);a?a.push(n):s.set(r,[n])}return t}const Z=(e,t)=>e[0]-t[0]||e[1]-t[1],ge=(e,t)=>e<t?-1:e>t?1:0,de=[];class q{#e;#s;constructor(t,n){this.#e=t,this.#s=n}static empty(t){return new q(new Map([["^",[[0,0]]],["$",[[t,t]]]]),t)}static from(t){const n=Object.entries(t).flatMap(([r,a])=>a.map(i=>[r,[i[0],i[1]]])),s=t.$?.[0]?.[1]??Math.max(0,...n.map(([,r])=>r[1]));return q.empty(s).with(n)}toJSON(){const t={};for(const n of[...this.#e.keys()].sort(ge))t[n]=this.#e.get(n).map(s=>[s[0],s[1]]);return t}with(t){const n=new Map;for(const[r,a]of t){const i=n.get(r);i?i.push(a):n.set(r,[a])}let s;for(const[r,a]of n){const i=this.#e.get(r)??de,p=[...i,...a].sort(Z),c=p.filter((w,$)=>$===0||Z(w,p[$-1])!==0);c.length!==i.length&&(s??=new Map(this.#e)).set(r,c)}return s?new q(s,this.#s):this}size(){let t=0;for(const n of this.#e.values())t+=n.length;return t}#t(t){if(t==="*")return this.#e.values();const n=this.#e.get(t);return n?[n]:[]}has(t,n,s){for(const r of this.#t(t))if(r.some(a=>a[0]===n&&a[1]===s))return!0;return!1}after(t,n){let s;for(const r of this.#t(t)){let a=0,i=r.length;for(;a<i;){const p=a+i>>1;r[p][0]<n?a=p+1:i=p}a<r.length&&(!s||Z(r[a],s)<0)&&(s=r[a])}return s}before(t,n){let s;for(const r of this.#t(t))for(let a=r.length-1;a>=0;a--)if(!(r[a][1]>n)){(!s||Z(r[a],s)>0)&&(s=r[a]);break}return s}spans(t){return this.#e.get(t)??de}all(){const t=[];for(const[n,s]of this.#e)for(const r of s)t.push([n,r]);return t.sort((n,s)=>Z(n[1],s[1])||ge(n[0],s[0]))}edges(){return ee(this.all())}}function pe(e,t){const[n,s]=e,[r,a]=t;return n===r&&s===a?"equals":s<r?"before":a<n?"after":s===r?"meets":a===n?"met-by":n<r?s<a?"overlaps":s===a?"finished-by":"contains":n===r?s<a?"starts":"started-by":s<a?"during":s===a?"finishes":"overlapped-by"}const Q=e=>e.replace(/[gyd]/g,""),me=new WeakMap;function U(e,t){let n=me.get(e);return n||me.set(e,n={}),n[t]??=t==="scan"?new RegExp(e.source,`${Q(e.flags)}g`):t==="back"?new RegExp(`(?:${e.source})$`,Q(e.flags)):t==="forward"?new RegExp(e.source,`${Q(e.flags)}y`):new RegExp(`^(?:${e.source})$`,Q(e.flags))}function ve(e,t,n,s,r,a){const i=s.forward!==void 0,p=s.forward??s.back??[],c=(k,v,M)=>v===void 0?k:{...k,[v]:M},w=(k,v,M)=>v===void 0?k:[...k,{text:M,as:v}],$=(k,v,M,N,h,m,u)=>{if(k===p.length){if(!u)return a(M,N,h,m);if(i){const b=U(u.rx,"forward");b.lastIndex=v;const A=b.exec(e);A&&a(M,v+A[0].length,c(h,u.as,A[0]),w(m,u.as,A[0]))}else{const b=U(u.rx,"back").exec(e.slice(0,v));b&&a(b.index,N,c(h,u.as,b[0]),w(m,u.as,b[0]))}return}const f=p[k];if(f.optional&&$(k+1,v,M,N,h,m,u),"rx"in f){u||$(k+1,v,M,N,h,m,{rx:f.rx,...f.as!==void 0&&{as:f.as}});return}const E=typeof f.tag=="string"?[f.tag]:f.tag,O=b=>E.includes("*")||E.includes(b),x=(b,A,z)=>{const[I,D]=b[1];$(k+1,i?D:I,i?M:I,i?D:N,c(A,f.as,n(b)),[...z,{row:b,...f.as!==void 0&&{as:f.as}}])};if(!u){for(const b of(i?t.starts:t.ends).get(v)??[])O(b[0])&&x(b,h,m);return}const R=U(u.rx,"whole");for(const b of t.all){if(!O(b[0])||(i?b[1][0]<v:b[1][1]>v))continue;const A=i?e.slice(v,b[1][0]):e.slice(b[1][1],v);R.test(A)&&x(b,c(h,u.as,A),w(m,u.as,A))}};$(0,i?r[1][1]:r[1][0],r[1][0],r[1][1],{},[{row:r}])}const Pe=/[‐-―−]/g,qe=/[‘-‟]/g,De=e=>"\u201C\u201D\u201E\u201F".includes(e)?'"':"'";function we(e){let t="";const n=[];let s=!1;for(let r=0;r<e.length;){const a=e.charCodeAt(r);if(a<128){a===32||a>=9&&a<=13?(s||(n.push(r),t+=" "),s=!0):(n.push(r),t+=e[r],s=!1),r++;continue}const i=String.fromCodePoint(e.codePointAt(r));if(/\s/.test(i))s||(n.push(r),t+=" "),s=!0;else{const p=i.normalize("NFKC").replace(Pe,"-").replace(qe,De);for(let c=0;c<p.length;c++)n.push(r);t+=p,s=!1}r+=i.length}return n.push(e.length),{text:t,at:n}}const Ke=/[\p{L}\p{N}]/u,Y=(e,t)=>{const n=e.charCodeAt(t);return Number.isNaN(n)?!1:n<128?n>=48&&n<=57||n>=65&&n<=90||n>=97&&n<=122:Ke.test(e[t])},Ze=(e,t)=>Y(e,t)&&Y(e,t-1),_e=(e,t)=>Y(e,t-1)&&Y(e,t),Ce=(e,t)=>e<t?-1:e>t?1:0,be=(e,t)=>e[0]-t[0]||e[1]-t[1],B=(e,t)=>be(e[1],t[1])||Ce(e[0],t[0]);function te(e,t,n){if(!e.length||n(e[e.length-1],t)<=0)return void e.push(t);let s=0,r=e.length;for(;s<r;){const a=s+r>>1;n(e[a],t)<=0?s=a+1:r=a}e.splice(s,0,t)}const $e=new WeakMap;function Le(e){let t=$e.get(e);if(!t){const n=e.forward??e.back??[];$e.set(e,t=[e.from,...n.flatMap(s=>"tag"in s?s.tag:[])])}return t}function Fe(e,t,n,s){const r=[];for(const[a,i]of Object.entries(t.tags)){if(!i.rx)continue;const p=U(i.rx,"scan");p.lastIndex=0;for(let c=p.exec(e);c;c=p.exec(e)){const w=c.index,$=w+c[0].length;if(p.lastIndex=w+(e.codePointAt(w)>65535?2:1),!($===w||Ze(e,w)||_e(e,$))){if(i.resolve){const k=i.resolve({value:c[0]});if(k===null)continue;s.set(n(a,w,$),k)}r.push([a,[w,$]])}}}return r}function Ge(e,t){const{text:n,at:s}=we(e),r=n.length,a=Object.keys(t.tags).sort(),i=new Map(["^","$",...a].map((u,f)=>[u,f])),p=(u,f,E)=>(i.get(u)*(r+1)+f)*(r+1)+E,c=new Map,w=([u,[f,E]])=>c.get(p(u,f,E))??n.slice(f,E),$=new Set,k=new Map,v={starts:new Map,ends:new Map,all:[]},M=(u,f,E)=>{const O=u.get(f);O?te(O,E,B):u.set(f,[E])},N=u=>{const[f,[E,O]]=u,x=p(f,E,O);if($.has(x))return!1;$.add(x);const R=k.get(f);return R?te(R,u[1],be):k.set(f,[u[1]]),te(v.all,u,B),M(v.starts,E,u),M(v.ends,O,u),!0},h=[["^",[0,0]],["$",[r,r]],...Fe(n,t,p,c)];for(const u of h.sort(B))N(u);let m=new Set(k.keys());for(;m.size;){const u=[];for(const f of a){const E=t.tags[f];for(const O of E.search??[])if(Le(O).some(x=>x==="*"||m.has(x)))for(const x of k.get(O.from)??[])ve(n,v,w,O,[O.from,x],(R,b,A)=>{if(E.resolve){if(c.has(p(f,R,b)))return;const z=E.resolve({...A,value:n.slice(R,b)});if(z===null)return;c.set(p(f,R,b),z)}u.push([f,[R,b]])})}m=new Set;for(const f of u.sort(B))N(f)&&m.add(f[0])}return q.empty(e.length).with(v.all.map(([u,[f,E]])=>[u,[s[f],s[E]]]))}const Ve=(e,t)=>e[1][0]-t[1][0]||e[1][1]-t[1][1]||(e[0]<t[0]?-1:e[0]>t[0]?1:0),He=(e,t)=>{const n=pe(e,t);return n==="starts"||n==="during"||n==="finishes"},Je=e=>e[0]==="^"||e[0]==="$";function Qe(e,t,n){const{text:s,at:r}=we(e),a=s.length,i=new Int32Array(e.length+1).fill(-1),p=new Int32Array(e.length+1).fill(-1);for(let o=0;o<r.length;o++)i[r[o]]<0&&(i[r[o]]=o),p[r[o]]=o;const c=new Map(Object.keys(n.tags).map((o,l)=>[o,l])),w=[],$=[];for(const[o,[l,g]]of t.all())if(o==="^")w.push(["^",[0,0]]);else if(o==="$")w.push(["$",[a,a]]);else{const d=c.get(o);if(d===void 0)throw new Error(`the chart has rows of "${o}", which the conf does not declare`);const y=i[l]??-1,j=p[g]??-1;if(y<0||j<0)throw new Error(`${o} (${l},${g}) is not on a boundary of the text: is this the chart of another text?`);const S=[o,[y,j]];w.push(S),$.push({row:S,weak:n.tags[o].weak===!0,rank:d})}w.sort(Ve);const k=o=>`${o[0]} (${r[o[1][0]]},${r[o[1][1]]})`,v=new Map,M=new Map,N=new Set,h=new Set;function m(o){const l=v.get(o);if(l)return l;if(N.has(o))return[];N.add(o);const[g,[d,y]]=o,j=n.tags[g],S=w.filter(P=>d<=P[1][0]&&P[1][1]<=y&&P!==o),F=ee(S),K=[];for(const P of j.search??[])for(const ae of S)ae[0]===P.from&&ve(s,F,u,P,ae,(Ee,Me,Oe,ie)=>{if(Ee!==d||Me!==y)return;if(!j.resolve)return void K.push({steps:ie});const ce=j.resolve({...Oe,value:s.slice(d,y)});ce===null?h.add(o):K.push({steps:ie,value:ce})});if(N.delete(o),!K.length)throw new Error(h.has(o)?`resolver of "${g}" refused (${r[d]},${r[y]}) at dom time`:`no derivation of ${k(o)}: is this the chart of this conf?`);return v.set(o,K),K}function u(o){const l=n.tags[o[0]];if(!l?.resolve)return s.slice(o[1][0],o[1][1]);let g=M.get(o);if(g===void 0){if(l.search)g=m(o)[0].value;else{const d=l.resolve({value:s.slice(o[1][0],o[1][1])});if(d===null)throw new Error(`resolver of "${o[0]}" refused (${r[o[1][0]]},${r[o[1][1]]}) at dom time`);g=d}M.set(o,g)}return g}$.sort((o,l)=>Number(o.weak)-Number(l.weak)||l.row[1][1]-l.row[1][0]-(o.row[1][1]-o.row[1][0])||o.rank-l.rank||o.row[1][0]-l.row[1][0]);const f=[],E=new Map,O=new Map,x=new Int32Array(a+1).fill(-1),R=new Int32Array(a+1).fill(a+1),b=new Map,A=new Map,z=new Map,I=[],D=o=>{for(;I.length>o;)I.pop()()},_=([,[o,l]])=>o*(a+1)+l,se=([o,l])=>{for(let g=o+1;g<l;g++)if(x[g]>l||R[g]<o)return!0;return!1};function ke(o){const[l,[g,d]]=o;let y=O.get(l);y||O.set(l,y=[]);const[j,S]=[x[g],R[d]];f.push(o),y.push(o),E.set(_(o),o),d>j&&(x[g]=d),g<S&&(R[d]=g),I.push(()=>{f.pop(),y.pop(),E.delete(_(o)),x[g]=j,R[d]=S})}function xe(o,l){const g=b.get(l);g?g.includes(o[0])||(g.push(o[0]),I.push(()=>g.pop())):(b.set(l,[o[0]]),I.push(()=>b.delete(l)));const d=z.get(o);return d!==l&&(z.set(o,l),I.push(()=>d===void 0?z.delete(o):z.set(o,d))),!0}function ne(o,l){if(l!==void 0&&o[0]===l)return oe(o,l,!1);const g=E.get(_(o));if(g)return g[0]===o[0]||xe(o,g);if(se(o[1]))return!1;const d=I.length;return ke(o),!n.tags[o[0]].search||oe(o,o[0],!0)?!0:(D(d),!1)}function oe(o,l,g){for(const d of m(o)){const y=I.length;if(d.steps.every(j=>!("row"in j)||Je(j.row)||ne(j.row,l)))return g&&(A.set(o,d),I.push(()=>A.delete(o))),!0;D(y)}return!1}for(const{row:o}of $)E.get(_(o))?.[0]!==o[0]&&(se(o[1])||O.get(o[0])?.some(l=>He(o[1],l[1]))||ne(o));const C={tag:"doc",start:0,end:e.length,attrs:{},text:e,children:[]},L=new Map,W=[{node:C,lo:0,hi:a}];for(const o of[...f].sort((l,g)=>l[1][0]-g[1][0]||g[1][1]-l[1][1])){const[l,[g,d]]=o;for(;W[W.length-1].lo>g||W[W.length-1].hi<d;)W.pop();const y=n.tags[l],j=y.resolve?y.search?A.get(o).value:u(o):void 0,S={tag:l,start:r[g],end:r[d]};j!==void 0&&(S.value=j),S.attrs={};const F=b.get(o);F&&(S.also=F),y.fate!==void 0&&(S.fate=y.fate),S.children=[],W[W.length-1].node.children.push(S),W.push({node:S,lo:g,hi:d}),L.set(o,S)}for(const o of f){const l=A.get(o);if(!l)continue;const g=L.get(o);for(const d of l.steps)if(d.as!==void 0)if("row"in d){const y=z.get(d.row);g.attrs[d.as]=L.get(d.row)??(y&&L.get(y))??s.slice(d.row[1][0],d.row[1][1])}else g.attrs[d.as]=d.text}const re=o=>{if(!o.children.length&&(o!==C||!o.end))return;const l=[];let g=o.start;const d=y=>{y>g&&l.push({tag:"text",start:g,end:y,attrs:{},children:[]})};for(const y of o.children)d(y.start),l.push(y),g=y.end,re(y);d(o.end),o.children=l};return re(C),C}const Ue=/^[.,;:!?)\]}]/;function X(e){return e.children.length?e.children.flatMap(X):[e]}function ye(e,t){const n=e.text;if(n===void 0)throw new Error("the tree has no text: build it with dom, or keep `text` on the root when you parse it back");const s=new Set,r=h=>{s.add(h),h.children.forEach(r)},a=h=>h.fate==="separator",i=h=>n.slice(h.start,h.end),p=h=>{for(const m of h.children)t(m)?r(m):p(m)};p(e);const c=e.children.length?X(e):[];for(let h=c.length-1;h>=0;h--){if(c[h].fate!=="connector"||s.has(c[h]))continue;const m=c.slice(h+1).find(u=>!a(u));m&&s.has(m)&&s.add(c[h])}const w=new Set,$=h=>{if(h.children.forEach($),h.fate!=="bracket"||s.has(h)||h.children.length<2)return;const m=h.children,u=x=>x.tag==="text"&&x.end-x.start===1,f=u(m[0])?m[0]:void 0,E=u(m[m.length-1])?m[m.length-1]:void 0,O=X(h).filter(x=>x!==f&&x!==E&&!a(x));O.some(x=>s.has(x))&&O.every(x=>s.has(x))?r(h):f&&w.add(f)};$(e);const k=h=>{const m=i(h);return m.includes("-")?4:m.includes(";")?3:m.includes(":")?2:m.includes(",")?1:0};let v,M=[];const N=h=>{if(M.some(m=>s.has(m))){const m=M.filter(f=>a(f)&&!s.has(f));let u;for(const f of m)(!u||k(f)>k(u))&&(u=f);for(const f of m)f!==u&&s.add(f);u&&(!v||!h||w.has(v)||Ue.test(i(h)))&&s.add(u)}M=[]};for(const h of c)a(h)||s.has(h)?M.push(h):(N(h),v=h);return N(void 0),s}function Ye(e,t){const n=ye(e,t),s=e.text;return(e.children.length?X(e):[]).filter(r=>!n.has(r)).map(r=>s.slice(r.start,r.end)).join("")}export{q as Chart,We as compile,Qe as dom,ee as edges,Ge as find,Ye as gist,ye as hidden,pe as rel};
@@ -25,9 +25,9 @@ type Compiled = Conf<RegExp, Resolve>;
25
25
  * row, water as `text` nodes. Throws when the chart holds a row of a tag the
26
26
  * conf does not declare, or when a resolver refuses a row it accepted in `find`.
27
27
  *
28
- * ponytail: the chosen set is scanned linearly for every test (O(rows²) over
29
- * the walk), and a composed row's derivations are all enumerated, as in `find`.
30
- * Index chosen rows by position if subjects grow.
28
+ * ponytail: the crossing test walks the boundaries inside the row (O(its length))
29
+ * and the same-tag test scans the chosen rows of that tag; a composed row's
30
+ * derivations are all enumerated, as in `find`. An interval tree if subjects grow.
31
31
  */
32
32
  export declare function dom(text: string, chart: Chart, conf: Compiled): Node;
33
33
  export {};
@@ -1,6 +1,12 @@
1
1
  import { Chart } from './chart.ts';
2
2
  import type { Conf, Resolve } from './conf.ts';
3
3
  type Compiled = Conf<RegExp, Resolve>;
4
- /** Every row the chart has for `text` under `conf`: complete, neutral, in original coordinates. */
4
+ /**
5
+ * Every row the chart has for `text` under `conf`: complete, neutral, in original coordinates.
6
+ *
7
+ * The chart grows in place, sorted: a pass adds its rows once it is over, and
8
+ * the next pass runs only the searches that read a tag the last one added to.
9
+ * ponytail: each insert is a splice, O(rows); fine at subject length.
10
+ */
5
11
  export declare function find(text: string, conf: Compiled): Chart;
6
12
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fewrd",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Finds what recurs in a string, and lets a reader fold it away.",
@@ -32,15 +32,17 @@
32
32
  "default": "./dist/playground.js"
33
33
  }
34
34
  },
35
- "devDependencies": {
36
- "@types/node": "^26.6.3",
37
- "typescript": "^5.9.3",
38
- "vite": "^7.3.6"
39
- },
40
35
  "scripts": {
41
36
  "test": "node --test \"test/*.test.ts\"",
42
37
  "typecheck": "tsc",
38
+ "bench": "node bench.ts",
43
39
  "build": "vite build --config vite.lib.ts && tsc -p tsconfig.build.json",
40
+ "prepublishOnly": "npm run build",
44
41
  "dev": "vite playground --port 5577 --strictPort"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^26.6.3",
45
+ "typescript": "^5.9.3",
46
+ "vite": "^7.3.6"
45
47
  }
46
- }
48
+ }