@dogsbay/search 0.0.0-stage → 0.2.0-beta.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,79 @@
1
- # Temporary Holding Version
1
+ # @dogsbay/search
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Static search for documentation sites. The index builds anywhere JavaScript runs, including a Cloudflare Worker, and is queried with classic scripts, so search works when a site is opened from disk (`file://`) as well as from a server.
4
+
5
+ - **Ranking:** field weights, BM25-style length normalisation and saturation, and inverse document frequency. The top results are refined with their headings.
6
+ - **Queries:** prefix matching on the last word, English stemming, hyphenated compounds, typo tolerance, `"phrases"`, `-word`, `OR`, and `tag:` and `section:` filters.
7
+ - **Results:** a page's matching headings come with it, linking to the section.
8
+ - **Downloads:** small, on demand. The first query on the 1,800-page OpenShift docs downloads about 110 KB, against 353 KB with Pagefind (`docs-dev/evaluation.md`).
9
+
10
+ dogsbay-ssg uses it for its built-in search. It also indexes the built HTML of any other generator.
11
+
12
+ ## Index a folder of HTML
13
+
14
+ ```bash
15
+ npx dogsbay-search dist/ # writes dist/_search/
16
+ npx dogsbay-search dist/ --out search --lang en
17
+ ```
18
+
19
+ Every `.html` page is indexed, except redirects and `noindex` pages. The indexer reads:
20
+
21
+ - **Content:** `<main>` (or `<article>`, or `<body>`). Mark a different element with `data-search-body`. Pagefind's `data-pagefind-body` also works.
22
+ - **Skipped:** navigation, header, footer, asides, forms, code blocks, `aria-hidden` content, and anything marked `data-search-ignore`.
23
+ - **Title:** the first `<h1>`, else `<title>`.
24
+ - **Headings:** h2 and h3 with an `id` become links to that section in results.
25
+ - **Keywords:** `<meta name="keywords">` and tag lists (`data-tag-list`) rank above body text, and tags are also filters.
26
+ - **Excerpt:** `<meta name="description">`.
27
+
28
+ ## Query it in the page
29
+
30
+ ```html
31
+ <script src="dogsbay-search.iife.js"></script>
32
+ <script>
33
+ const data = DogsbaySearch.scriptData("_search/", DogsbaySearch.loadScript);
34
+ async function find(q) {
35
+ const { results, parsed } = await DogsbaySearch.search(q, data);
36
+ // results: [{ title, url, section, fragment: [excerpt, headings], sections: [[heading, id]] }, …]
37
+ // parsed.highlight: the terms to highlight
38
+ return results;
39
+ }
40
+ </script>
41
+ ```
42
+
43
+ `dogsbay-search.iife.js` is in `dist/` (about 10 KB). URLs in results are relative to the site root the index was built from. The package has no search box of its own: dogsbay-ssg's dialog (`src/client/search.ts` there) is an example.
44
+
45
+ ## Use it from code
46
+
47
+ ```js
48
+ import { SearchIndexBuilder } from "@dogsbay/search/index";
49
+ import { extractHtml } from "@dogsbay/search/html";
50
+
51
+ const index = new SearchIndexBuilder({ lang: "en" });
52
+ const page = extractHtml(html, "guide/install.html"); // or build the fields yourself
53
+ if (page) index.add(page.doc, page.parts);
54
+ for (const [file, content] of index.emitFiles()) write(`_search/${file}`, content);
55
+ ```
56
+
57
+ - `@dogsbay/search/index`: `SearchIndexBuilder`, and `extractText()` for a dogsbay document tree.
58
+ - `@dogsbay/search/html`: `extractHtml()`.
59
+ - `@dogsbay/search/query`: `search()`, `parseQuery()`, `scriptData()`, `memoryData()`.
60
+ - `@dogsbay/search/site`: `indexSite(dir)`, for Node.
61
+
62
+ Everything except `site` is Worker- and browser-safe.
63
+
64
+ ## Query syntax
65
+
66
+ | Query | Finds |
67
+ |---|---|
68
+ | `docker image` | Pages with both words; if none, pages with either. |
69
+ | `"docker image"` | Both words, with the exact phrase ranked higher. |
70
+ | `docker -windows` | Pages with "docker" and without "windows". Also `NOT windows`. |
71
+ | `linux OR macos` | Either word. |
72
+ | `install tag:setup` | Pages tagged `setup`. |
73
+ | `upgrade section:"Getting started"` | Pages in that section. |
74
+
75
+ ## More
76
+
77
+ - `docs-dev/design.md`: how indexing and ranking work, and the file format.
78
+ - `docs-dev/evaluation.md`: results against Pagefind on three documentation corpora.
79
+ - `eval/README.md`: running the evaluation.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { indexSite, isDir } from "./site.js";
3
+ const args = process.argv.slice(2);
4
+ const opt = (name) => {
5
+ const i = args.indexOf(name);
6
+ if (i < 0) return void 0;
7
+ const v = args[i + 1];
8
+ args.splice(i, 2);
9
+ if (!v || v.startsWith("--")) usage(1);
10
+ return v;
11
+ };
12
+ function usage(code) {
13
+ console.log("dogsbay-search <site-dir> [--out <dir>] [--lang <code>]\n\nIndexes every .html page under <site-dir> into <site-dir>/_search/ (or --out, relative to <site-dir>).");
14
+ process.exit(code);
15
+ }
16
+ if (args.includes("-h") || args.includes("--help")) usage(0);
17
+ const out = opt("--out");
18
+ const lang = opt("--lang");
19
+ const dir = args[0];
20
+ if (!dir || args.length > 1 || !isDir(dir)) usage(1);
21
+ const t0 = performance.now();
22
+ const r = indexSite(dir, { out, lang });
23
+ console.log(`indexed ${r.pages} pages (${r.skipped} skipped) into ${r.files} files in ${r.out} (${Math.round(performance.now() - t0)} ms)`);
24
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * dogsbay-search <site-dir> [--out <dir>] [--lang <code>]\n *\n * Index a folder of built HTML (any site generator) into <site-dir>/_search/.\n */\nimport { indexSite, isDir } from \"./site.js\";\n\nconst args = process.argv.slice(2);\nconst opt = (name: string) => {\n const i = args.indexOf(name);\n if (i < 0) return undefined;\n const v = args[i + 1];\n args.splice(i, 2);\n if (!v || v.startsWith(\"--\")) usage(1);\n return v;\n};\nfunction usage(code: number): never {\n console.log(\"dogsbay-search <site-dir> [--out <dir>] [--lang <code>]\\n\\nIndexes every .html page under <site-dir> into <site-dir>/_search/ (or --out, relative to <site-dir>).\");\n process.exit(code);\n}\nif (args.includes(\"-h\") || args.includes(\"--help\")) usage(0);\nconst out = opt(\"--out\");\nconst lang = opt(\"--lang\");\nconst dir = args[0];\nif (!dir || args.length > 1 || !isDir(dir)) usage(1);\nconst t0 = performance.now();\nconst r = indexSite(dir, { out, lang });\nconsole.log(`indexed ${r.pages} pages (${r.skipped} skipped) into ${r.files} files in ${r.out} (${Math.round(performance.now() - t0)} ms)`);\n"],"mappings":";AAMA,SAAS,WAAW,aAAa;AAEjC,MAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,MAAM,MAAM,CAAC,SAAiB;AAC5B,QAAM,IAAI,KAAK,QAAQ,IAAI;AAC3B,MAAI,IAAI,EAAG,QAAO;AAClB,QAAM,IAAI,KAAK,IAAI,CAAC;AACpB,OAAK,OAAO,GAAG,CAAC;AAChB,MAAI,CAAC,KAAK,EAAE,WAAW,IAAI,EAAG,OAAM,CAAC;AACrC,SAAO;AACT;AACA,SAAS,MAAM,MAAqB;AAClC,UAAQ,IAAI,mKAAmK;AAC/K,UAAQ,KAAK,IAAI;AACnB;AACA,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,QAAQ,EAAG,OAAM,CAAC;AAC3D,MAAM,MAAM,IAAI,OAAO;AACvB,MAAM,OAAO,IAAI,QAAQ;AACzB,MAAM,MAAM,KAAK,CAAC;AAClB,IAAI,CAAC,OAAO,KAAK,SAAS,KAAK,CAAC,MAAM,GAAG,EAAG,OAAM,CAAC;AACnD,MAAM,KAAK,YAAY,IAAI;AAC3B,MAAM,IAAI,UAAU,KAAK,EAAE,KAAK,KAAK,CAAC;AACtC,QAAQ,IAAI,WAAW,EAAE,KAAK,WAAW,EAAE,OAAO,kBAAkB,EAAE,KAAK,aAAa,EAAE,GAAG,KAAK,KAAK,MAAM,YAAY,IAAI,IAAI,EAAE,CAAC,MAAM;","names":[]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * V8 represents a substring (regex match, slice, split piece) as a view on
3
+ * its parent, and a concatenation as a rope of its parts. Either keeps the
4
+ * whole parent alive: a term taken from a page keeps that page's text in
5
+ * memory for the rest of the build. Anything the indexer keeps beyond one
6
+ * page goes through `detach`, which returns a standalone flat copy.
7
+ */
8
+ declare function detach(s: string): string;
9
+
10
+ export { detach };
package/dist/detach.js ADDED
@@ -0,0 +1,7 @@
1
+ function detach(s) {
2
+ return JSON.parse(JSON.stringify(s));
3
+ }
4
+ export {
5
+ detach
6
+ };
7
+ //# sourceMappingURL=detach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/detach.ts"],"sourcesContent":["/**\n * V8 represents a substring (regex match, slice, split piece) as a view on\n * its parent, and a concatenation as a rope of its parts. Either keeps the\n * whole parent alive: a term taken from a page keeps that page's text in\n * memory for the rest of the build. Anything the indexer keeps beyond one\n * page goes through `detach`, which returns a standalone flat copy.\n */\nexport function detach(s: string): string {\n return JSON.parse(JSON.stringify(s)) as string;\n}\n"],"mappings":"AAOO,SAAS,OAAO,GAAmB;AACxC,SAAO,KAAK,MAAM,KAAK,UAAU,CAAC,CAAC;AACrC;","names":[]}
@@ -0,0 +1,2 @@
1
+ "use strict";var DogsbaySearch=(()=>{var z=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var Z=Object.getOwnPropertyNames;var tt=Object.prototype.hasOwnProperty;var et=(n,t)=>{for(var o in t)z(n,o,{get:t[o],enumerable:!0})},nt=(n,t,o,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of Z(t))!tt.call(n,r)&&r!==o&&z(n,r,{get:()=>t[r],enumerable:!(e=X(t,r))||e.enumerable});return n};var st=n=>nt(z({},"__esModule",{value:!0}),n);var Et={};et(Et,{loadDetails:()=>K,loadScript:()=>Rt,parseQuery:()=>E,scriptData:()=>J,search:()=>q});var v={docs:"docs.js",shard:n=>`t-${n}.js`,fragment:n=>`f-${n}.js`},$={docs:"docs",shard:n=>n,fragment:n=>`frag:${n}`};var rt={ational:"ate",tional:"tion",enci:"ence",anci:"ance",izer:"ize",bli:"ble",alli:"al",entli:"ent",eli:"e",ousli:"ous",ization:"ize",ation:"ate",ator:"ate",alism:"al",iveness:"ive",fulness:"ful",ousness:"ous",aliti:"al",iviti:"ive",biliti:"ble",logi:"log"},ot={icate:"ic",ative:"",alize:"al",iciti:"ic",ical:"ic",ful:"",ness:""},it="[^aeiou]",F="[aeiouy]",y="("+it+"[^aeiouy]*)",R="("+F+"[aeiou]*)",O=new RegExp("^"+y+"?"+R+y),at=new RegExp("^"+y+"?"+R+y+R+"?$"),D=new RegExp("^"+y+"?("+R+y+"){2,}"),C=new RegExp("^"+y+"?"+F),L=new RegExp("^"+y+F+"[^aeiouwxy]$"),ct=/ll$/,lt=/^(.+?)e$/,ft=/^(.+?)y$/,gt=/^(.+?(s|t))(ion)$/,ut=/^(.+?)(ed|ing)$/,ht=/(at|bl|iz)$/,dt=/^(.+?)eed$/,pt=/^.+?[^s]s$/,mt=/^.+?(ss|i)es$/,wt=/([^aeiouylsz])\1$/,xt=/^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/,bt=/^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/,yt=/^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;function N(n){let t=String(n).toLowerCase();if(t.length<3)return t;let o=!1;t.codePointAt(0)===121&&(o=!0,t="Y"+t.slice(1)),mt.test(t)?t=t.slice(0,-2):pt.test(t)&&(t=t.slice(0,-1));let e;return(e=dt.exec(t))?O.test(e[1])&&(t=t.slice(0,-1)):(e=ut.exec(t))&&C.test(e[1])&&(t=e[1],ht.test(t)?t+="e":wt.test(t)?t=t.slice(0,-1):L.test(t)&&(t+="e")),(e=ft.exec(t))&&C.test(e[1])&&(t=e[1]+"i"),(e=xt.exec(t))&&O.test(e[1])&&(t=e[1]+rt[e[2]]),(e=bt.exec(t))&&O.test(e[1])&&(t=e[1]+ot[e[2]]),(e=yt.exec(t))?D.test(e[1])&&(t=e[1]):(e=gt.exec(t))&&D.test(e[1])&&(t=e[1]),(e=lt.exec(t))&&(D.test(e[1])||at.test(e[1])&&!L.test(e[1]))&&(t=e[1]),ct.test(t)&&D.test(t)&&(t=t.slice(0,-1)),o&&(t="y"+t.slice(1)),t}var j=new Set("a an and are as at be by for from has have in is it its of on or that the this to was were will with you your can not".split(" ")),St=/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u,_;function Mt(n){if(_===void 0)try{_=typeof Intl<"u"&&"Segmenter"in Intl?new Intl.Segmenter(void 0,{granularity:"word"}):null}catch{_=null}if(_)return[..._.segment(n)].filter(e=>e.isWordLike).map(e=>e.segment);let t=[...n];if(t.length<2)return t;let o=[];for(let e=0;e<t.length-1;e++)o.push(t[e]+t[e+1]);return o}function S(n){let t=[];for(let o of n.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").normalize("NFC").matchAll(/[\p{L}\p{N}][\p{L}\p{N}_.-]*[\p{L}\p{N}]|[\p{L}\p{N}]/gu)){let e=o[0],r=e.split(/[._-]+/).filter(Boolean);if(r.length>1&&t.push(e),r.length>1&&!/[._]/.test(e)){let s=r.join("");s.length>=3&&!j.has(s)&&t.push(s)}for(let s of r)St.test(s)?t.push(...Mt(s)):s.length>=2&&!j.has(s)&&t.push(s)}return t}function G(n,t){let o="";for(let e of n.slice(0,t))o+=/[a-z0-9]/.test(e)?e:`_${e.codePointAt(0).toString(16)}_`;return o}function P(n){if(!(!n||!/^en(-|$)/i.test(n)))return t=>/^[a-z]{3,}$/.test(t)?N(t):t}function E(n){let t=[],o=[],e=[],r=!1,s={tag:[],section:[]},f={tag:[],section:[]},p=[...n.matchAll(/(?<!\S)(-?)(tag|section):(?:"([^"]*)"?|(\S*))|(-?)"([^"]*)"?|(\S+)/gi)].map(c=>({facetNeg:c[1]==="-",facet:c[2]?.toLowerCase(),value:c[3]??c[4],neg:c[5],phrase:c[6],word:c[7]})),a=!1,u=!1;for(let[c,g]of p.entries()){if(g.facet){let b=Y(g.value??"");b&&(g.facetNeg?f:s)[g.facet].push(b);continue}let l=g.word;if(l==="OR"){u=t.length>0,r=!0;continue}if(l==="NOT"){a=!0,r=!0;continue}if(l==="AND"){r=!0;continue}let h=g.phrase!==void 0,m=a||g.neg==="-"||l?.startsWith("-")&&l.length>1;a=!1;let w=h?g.phrase:m&&l?.startsWith("-")?l.slice(1):l,d=[...new Set(S(w))];if(!d.length){u=!1;continue}if(m){o.push(h?d:[d[0]]),r=!0,u=!1;continue}if(h){u=!1,r=!0,d.length>1&&e.push(S(w).join(" "));for(let b of d)t.push({terms:[b],prefix:!1,exact:!0});continue}let M=c===p.length-1&&!/\s$/.test(n);if(u){t[t.length-1].terms.push(d[0]);for(let b of d.slice(1))t.push({terms:[b],prefix:!1});u=!1;continue}let A=d[0],U=/[._-]/.test(A),V=A.replace(/-/g,"");for(let[b,k]of d.entries())t.push({terms:[k],prefix:M&&k.length>=2&&(b===d.length-1||U&&(k===A||k===V))})}let i=[...new Set(t.flatMap(c=>c.terms))];return{groups:t,not:o,phrases:e,strict:r,highlight:i,filters:s,notFilters:f}}async function _t(n,t){let o=await t.meta(),e=typeof n=="string"?E(n):n,r=new Set;for(let u of e.not){let i;for(let c of u){let[g]=I(c,o),l=g?await t.shard(g):void 0,h=l&&Object.hasOwn(l,c)?l[c]:[],m=new Set;for(let w=0;w<h.length;w+=2)(!i||i.has(h[w]))&&m.add(h[w]);i=m}for(let c of i??[])r.add(c)}let s=vt(e,o);if(!e.groups.length){if(!s)return[];let u=[];return o.docs.forEach(([i,c,g],l)=>s(l)&&!r.has(l)&&u.push({id:l,title:i,url:c,section:o.sections[g]??"",score:1})),u}let f=[];for(let u of e.groups)f.push(await $t(u,o,t));let p=u=>{let i=new Map,c=new Map;for(let l of f)for(let[h,m]of l)i.set(h,(i.get(h)??0)+m),c.set(h,(c.get(h)??0)+1);let g=[];for(let[l,h]of i){if(r.has(l)||s&&!s(l)||u==="and"&&c.get(l)!==f.length)continue;let[m,w,d]=o.docs[l];g.push({id:l,title:m,url:w,section:o.sections[d]??"",score:h*(c.get(l)/f.length)})}return g.sort((l,h)=>h.score-l.score)},a=p("and");return a.length||e.strict?a:p("or")}var x={prefix:.6,stem:.4,titleAll:1.5,titleExact:2,headingAll:1.4,headingExact:1.8,phrase:1.5,typo:.3};function Pt(n,t){if(n===t)return!1;if(n.length===t.length){let s=[];for(let f=0;f<n.length&&s.length<=2;f++)n[f]!==t[f]&&s.push(f);return s.length===1?!0:s.length===2&&s[1]===s[0]+1&&n[s[0]]===t[s[1]]&&n[s[1]]===t[s[0]]}let[o,e]=n.length<t.length?[n,t]:[t,n];if(e.length-o.length!==1)return!1;let r=0;for(;r<o.length&&o[r]===e[r];)r++;return o.slice(r)===e.slice(r+1)}function I(n,t,o=!1){let e=kt(t);for(let s=Math.min(n.length,W);s>=1;s--){let f=G(n,s);if(e.has(f))return!o||s<n.length?[f]:[f,...t.shards.filter(p=>p.length>f.length&&p.startsWith(f)).slice(0,10)]}let r=G(n,Math.min(n.length,W));return o?t.shards.filter(s=>s.startsWith(r)).slice(0,10):[]}var W=3,Q=new WeakMap;function kt(n){let t=Q.get(n);return t||Q.set(n,t=new Set(n.shards)),t}function Y(n){return n.replace(/"/g,"").trim().toLowerCase()}function vt(n,t){let{tag:o,section:e}=n.filters,r=n.notFilters??{tag:[],section:[]};if(!o.length&&!e.length&&!r.tag.length&&!r.section.length)return;let s=(i,c)=>new Set(i.flatMap((g,l)=>c.includes(Y(g))?[l]:[])),f=s(t.sections,e),p=s(t.tags??[],o),a=s(t.sections,r.section),u=s(t.tags??[],r.tag);return i=>{let c=t.docs[i],g=c[3]??[];return!(e.length&&!f.has(c[2])||o.length&&!g.some(l=>p.has(l))||a.has(c[2])||g.some(l=>u.has(l)))}}async function $t(n,t,o){n.corrected=void 0;let e=P(t.lang),r=new Map,s=new Map;for(let i of n.terms)for(let c of I(i,t,n.prefix)){let g=await o.shard(c);if(!g)continue;let l=(d,M)=>{M>(r.get(d)??0)&&(r.set(d,M),s.set(d,g))};Object.hasOwn(g,i)&&l(i,1);let h=e?.(i),m=h?h.slice(0,Math.max(3,h.length-1)):void 0,w=0;for(let d in g)d!==i&&(n.prefix&&d.startsWith(i)&&w++<200&&l(d,x.prefix),h&&m&&d.startsWith(m)&&e(d)===h&&l(d,x.stem));if(!r.size&&!n.exact&&i.length>=4&&!/[._-]/.test(i))for(let d in g)Math.abs(d.length-i.length)<=1&&Pt(i,d)&&l(d,x.typo)}[...r.values()].some(i=>i===x.typo)&&(n.corrected=[...r].filter(([,i])=>i===x.typo).map(([i])=>i));let f=[...r].map(([i,c])=>[s.get(i)[i],c]),p=new Set;for(let[i]of f)for(let c=0;c<i.length;c+=2)p.add(i[c]);let a=Math.log(1+(t.count-p.size+.5)/(p.size+.5)),u=new Map;for(let[i,c]of f)for(let g=0;g<i.length;g+=2)u.set(i[g],Math.max(u.get(i[g])??0,i[g+1]*c*a));return u}async function B(n,t){let o=await t.meta(),e=[...new Set(n.map(s=>Math.floor(s.id/o.block)))],r=new Map;await Promise.all(e.map(async s=>r.set(s,await t.fragment(s))));for(let s of n)s.fragment??(s.fragment=r.get(Math.floor(s.id/o.block))?.[s.id%o.block])}async function Dt(n,t,o,e=10){let r=t.slice(0,e);await B(r,o);let s=n.groups,f=P((await o.meta()).lang),p=(a,u)=>u.terms.some(i=>a===i||u.prefix&&a.startsWith(i)||f!==void 0&&f(a)===f(i))||(u.corrected??[]).includes(a);for(let a of r){let[u,i]=a.fragment??["",[]],c=S(a.title),g=1;s.length&&s.every(h=>c.some(m=>p(m,h)))&&(g=H(c,s,p)?x.titleExact:x.titleAll);let l=[];for(let[h,m]of i){let w=S(h);!s.length||!s.every(d=>w.some(M=>p(M,d)))||(l.push([h,m]),g=Math.max(g,H(w,s,p)?x.headingExact:x.headingAll))}if(a.sections=l.slice(0,3),n.phrases.length){let h=[a.title,u,...i.map(([m])=>m)].map(m=>S(m).join(" ")).join(" \0 ");n.phrases.every(m=>h.includes(m))&&(g*=x.phrase)}a.score*=g}return r.sort((a,u)=>u.score-a.score),[...r,...t.slice(e)]}async function K(n,t,o){await B(t,o);let e=P((await o.meta()).lang),r=(s,f)=>f.terms.some(p=>s===p||f.prefix&&s.startsWith(p)||e!==void 0&&e(s)===e(p))||(f.corrected??[]).includes(s);for(let s of t)!s.fragment||s.sections||(s.sections=s.fragment[1].filter(([f])=>{let p=S(f);return n.groups.length>0&&n.groups.every(a=>p.some(u=>r(u,a)))}).slice(0,3))}function H(n,t,o){return n.every(e=>t.some(r=>o(e,r)))&&t.every(e=>n.some(r=>o(r,e)))}async function q(n,t,o=10){let e=E(n),r=P((await t.meta()).lang);r&&(e.highlight=[...new Set([...e.highlight,...e.highlight.map(r).filter(f=>f.length>=3)])]);let s=await _t(e,t);return e.highlight=[...new Set([...e.highlight,...e.groups.flatMap(f=>f.corrected??[])])],{parsed:e,results:await Dt(e,s,t,o)}}function J(n,t,o=globalThis){let e=new Map,r=o.__DOGSBAY_SEARCH__??{q:[],load(){}};for(let[a,u]of r.q)e.set(a,u);r.q=[],r.load=(a,u)=>{e.set(a,u)},o.__DOGSBAY_SEARCH__=r;let s=async(a,u)=>(e.has(a)||await t(n+u),e.get(a)),f,p={meta:()=>f??(f=s($.docs,v.docs).then(a=>{if(!a||a.format!==4)throw new Error(`search index format ${a?.format??"missing"}, expected ${4}: rebuild the site`);return a})),shard:async a=>(await p.meta()).shards.includes(a)?s($.shard(a),v.shard(a)):void 0,fragment:a=>s($.fragment(a),v.fragment(a))};return p}function Rt(n){return new Promise((t,o)=>{let e=document.createElement("script");e.src=n,e.onload=()=>t(),e.onerror=()=>o(new Error(`failed to load ${n}`)),document.head.append(e)})}return st(Et);})();
2
+ //# sourceMappingURL=dogsbay-search.iife.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/browser.ts","../src/format.ts","../../../node_modules/.pnpm/stemmer@2.0.1/node_modules/stemmer/index.js","../src/text.ts","../src/query.ts"],"sourcesContent":["/**\n * The query side as one classic script (dist/dogsbay-search.iife.js), for\n * sites that are not built with dogsbay: include it with a <script> tag and\n * call `DogsbaySearch.search(query, DogsbaySearch.scriptData(\"_search/\", loadScript))`.\n * Works from file://.\n */\nexport { parseQuery, search, scriptData, loadDetails, type Result } from \"./query.js\";\n\n/** Loads one index file as a classic <script>, the way scriptData needs. */\nexport function loadScript(src: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const s = document.createElement(\"script\");\n s.src = src;\n s.onload = () => resolve();\n s.onerror = () => reject(new Error(`failed to load ${src}`));\n document.head.append(s);\n });\n}\n","/**\n * The search index format: what the indexer writes and the query code\n * reads. Every file is a classic script that calls\n * `window.__DOGSBAY_SEARCH__.load(key, data)`, so it loads from `file://`\n * as well as from a server.\n *\n * docs.js Meta: titles, URLs and sections of every document\n * t-<k>.js Postings for terms whose first `depth` characters are <k>\n * f-<n>.js Fragments of documents n·block … n·block+block−1:\n * excerpt and headings, loaded only for results being shown\n */\n\n/** Version 4: tags for filters (3 split fragments out of docs.js, 2 added field-weighted scores). */\nexport const FORMAT = 4;\n\n/** Documents per fragment file. */\nexport const BLOCK = 32;\n\n/** Section and tags are indexes into Meta.sections and Meta.tags; tags are left out when there are none. */\nexport type Doc = [title: string, url: string, section: number, tags?: number[]];\n\nexport interface Meta {\n format: number;\n /** Characters of a term that pick its shard: 1, or 2 for large units. */\n depth: number;\n count: number;\n shards: string[];\n sections: string[];\n tags: string[];\n block: number;\n /** The unit's language, which picks the query-time stemmer. */\n lang?: string;\n docs: Doc[];\n}\n\n/** term → [doc, score, doc, score, …], highest score first. */\nexport type Postings = Record<string, number[]>;\n\n/** A document's excerpt and its h2/h3 headings with their anchors. */\nexport type Fragment = [excerpt: string, headings: [text: string, id: string][]];\n\nexport const file = {\n docs: \"docs.js\",\n shard: (key: string) => `t-${key}.js`,\n fragment: (block: number) => `f-${block}.js`,\n};\n\n/** Keys passed to load(); fragment keys contain \":\" so they can never equal a shard key ([a-z0-9_]). */\nexport const key = {\n docs: \"docs\",\n shard: (k: string) => k,\n fragment: (block: number) => `frag:${block}`,\n};\n\n/** One data file as a classic script. \"</\" is escaped so a term like \"</script>\" cannot end the tag. */\nexport function wrap(k: string, data: unknown): string {\n const json = JSON.stringify(data).replace(/<\\//g, \"<\\\\/\");\n return wrapJson(k, json);\n}\n\n/** As wrap(), for data already serialised (fragments are kept as JSON strings while building). */\nexport function wrapJson(k: string, json: string): string {\n return `(window.__DOGSBAY_SEARCH__=window.__DOGSBAY_SEARCH__||{q:[],load:function(k,d){this.q.push([k,d])}}).load(${JSON.stringify(k)},${json});\\n`;\n}\n","// Standard suffix manipulations.\n/** @type {Record<string, string>} */\nconst step2list = {\n ational: 'ate',\n tional: 'tion',\n enci: 'ence',\n anci: 'ance',\n izer: 'ize',\n bli: 'ble',\n alli: 'al',\n entli: 'ent',\n eli: 'e',\n ousli: 'ous',\n ization: 'ize',\n ation: 'ate',\n ator: 'ate',\n alism: 'al',\n iveness: 'ive',\n fulness: 'ful',\n ousness: 'ous',\n aliti: 'al',\n iviti: 'ive',\n biliti: 'ble',\n logi: 'log'\n}\n\n/** @type {Record<string, string>} */\nconst step3list = {\n icate: 'ic',\n ative: '',\n alize: 'al',\n iciti: 'ic',\n ical: 'ic',\n ful: '',\n ness: ''\n}\n\n// Consonant-vowel sequences.\nconst consonant = '[^aeiou]'\nconst vowel = '[aeiouy]'\nconst consonants = '(' + consonant + '[^aeiouy]*)'\nconst vowels = '(' + vowel + '[aeiou]*)'\n\nconst gt0 = new RegExp('^' + consonants + '?' + vowels + consonants)\nconst eq1 = new RegExp(\n '^' + consonants + '?' + vowels + consonants + vowels + '?$'\n)\nconst gt1 = new RegExp('^' + consonants + '?(' + vowels + consonants + '){2,}')\nconst vowelInStem = new RegExp('^' + consonants + '?' + vowel)\nconst consonantLike = new RegExp('^' + consonants + vowel + '[^aeiouwxy]$')\n\n// Exception expressions.\nconst sfxLl = /ll$/\nconst sfxE = /^(.+?)e$/\nconst sfxY = /^(.+?)y$/\nconst sfxIon = /^(.+?(s|t))(ion)$/\nconst sfxEdOrIng = /^(.+?)(ed|ing)$/\nconst sfxAtOrBlOrIz = /(at|bl|iz)$/\nconst sfxEED = /^(.+?)eed$/\nconst sfxS = /^.+?[^s]s$/\nconst sfxSsesOrIes = /^.+?(ss|i)es$/\nconst sfxMultiConsonantLike = /([^aeiouylsz])\\1$/\nconst step2 =\n /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/\nconst step3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/\nconst step4 =\n /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/\n\n/**\n * Get the stem from a given value.\n *\n * @param {string} value\n * Value to stem.\n * @returns {string}\n * Stem for `value`\n */\n// eslint-disable-next-line complexity\nexport function stemmer(value) {\n let result = String(value).toLowerCase()\n\n // Exit early.\n if (result.length < 3) {\n return result\n }\n\n /** @type {boolean} */\n let firstCharacterWasLowerCaseY = false\n\n // Detect initial `y`, make sure it never matches.\n if (\n result.codePointAt(0) === 121 // Lowercase Y\n ) {\n firstCharacterWasLowerCaseY = true\n result = 'Y' + result.slice(1)\n }\n\n // Step 1a.\n if (sfxSsesOrIes.test(result)) {\n // Remove last two characters.\n result = result.slice(0, -2)\n } else if (sfxS.test(result)) {\n // Remove last character.\n result = result.slice(0, -1)\n }\n\n /** @type {RegExpMatchArray|null} */\n let match\n\n // Step 1b.\n if ((match = sfxEED.exec(result))) {\n if (gt0.test(match[1])) {\n // Remove last character.\n result = result.slice(0, -1)\n }\n } else if ((match = sfxEdOrIng.exec(result)) && vowelInStem.test(match[1])) {\n result = match[1]\n\n if (sfxAtOrBlOrIz.test(result)) {\n // Append `e`.\n result += 'e'\n } else if (sfxMultiConsonantLike.test(result)) {\n // Remove last character.\n result = result.slice(0, -1)\n } else if (consonantLike.test(result)) {\n // Append `e`.\n result += 'e'\n }\n }\n\n // Step 1c.\n if ((match = sfxY.exec(result)) && vowelInStem.test(match[1])) {\n // Remove suffixing `y` and append `i`.\n result = match[1] + 'i'\n }\n\n // Step 2.\n if ((match = step2.exec(result)) && gt0.test(match[1])) {\n result = match[1] + step2list[match[2]]\n }\n\n // Step 3.\n if ((match = step3.exec(result)) && gt0.test(match[1])) {\n result = match[1] + step3list[match[2]]\n }\n\n // Step 4.\n if ((match = step4.exec(result))) {\n if (gt1.test(match[1])) {\n result = match[1]\n }\n } else if ((match = sfxIon.exec(result)) && gt1.test(match[1])) {\n result = match[1]\n }\n\n // Step 5.\n if (\n (match = sfxE.exec(result)) &&\n (gt1.test(match[1]) ||\n (eq1.test(match[1]) && !consonantLike.test(match[1])))\n ) {\n result = match[1]\n }\n\n if (sfxLl.test(result) && gt1.test(result)) {\n result = result.slice(0, -1)\n }\n\n // Turn initial `Y` back to `y`.\n if (firstCharacterWasLowerCaseY) {\n result = 'y' + result.slice(1)\n }\n\n return result\n}\n","/**\n * Text to terms, shared by the indexer and the query code so both split\n * words the same way.\n */\nimport { stemmer } from \"stemmer\";\nconst STOP = new Set(\n \"a an and are as at be by for from has have in is it its of on or that the this to was were will with you your can not\".split(\" \"),\n);\n\n/** Han, Hiragana, Katakana and Hangul: scripts written without spaces between words (Hangul uses some). */\nconst CJK = /[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]/u;\n\nlet segmenter: { segment(t: string): Iterable<{ segment: string; isWordLike?: boolean }> } | null | undefined;\n\n/**\n * Words of a CJK run, by the runtime's dictionary word breaker\n * (Intl.Segmenter), else by overlapping character pairs. Browsers and\n * Workers have the segmenter; their dictionaries can differ slightly, so a\n * rare word may split differently at build and query time.\n */\nfunction cjkWords(run: string): string[] {\n if (segmenter === undefined) {\n try {\n segmenter = typeof Intl !== \"undefined\" && \"Segmenter\" in Intl ? new (Intl as any).Segmenter(undefined, { granularity: \"word\" }) : null;\n } catch {\n segmenter = null;\n }\n }\n if (segmenter) return [...segmenter.segment(run)].filter((s) => s.isWordLike).map((s) => s.segment);\n const chars = [...run];\n if (chars.length < 2) return chars;\n const out: string[] = [];\n for (let i = 0; i < chars.length - 1; i++) out.push(chars[i] + chars[i + 1]);\n return out;\n}\n\n/** Lowercased terms of 2+ letters or digits; CJK text split into words. */\nexport function tokenize(text: string): string[] {\n const out: string[] = [];\n for (const m of text.toLowerCase().normalize(\"NFKD\").replace(/[̀-ͯ]/g, \"\").normalize(\"NFC\").matchAll(/[\\p{L}\\p{N}][\\p{L}\\p{N}_.-]*[\\p{L}\\p{N}]|[\\p{L}\\p{N}]/gu)) {\n const raw = m[0];\n // Keep dotted and hyphenated identifiers (args.input, dita-ot) and also index their parts.\n const parts = raw.split(/[._-]+/).filter(Boolean);\n if (parts.length > 1) out.push(raw);\n // A hyphenated compound is also indexed joined: \"plug-ins\" is found by \"plugins\".\n if (parts.length > 1 && !/[._]/.test(raw)) {\n const joined = parts.join(\"\");\n if (joined.length >= 3 && !STOP.has(joined)) out.push(joined);\n }\n for (const p of parts) {\n if (CJK.test(p)) out.push(...cjkWords(p));\n else if (p.length >= 2 && !STOP.has(p)) out.push(p);\n }\n }\n return out;\n}\n\n/** Shard key: first `depth` chars of the term, ASCII-safe for file names. */\nexport function shardKey(term: string, depth: number): string {\n let k = \"\";\n // Escapes are terminated (\"_432_\"), so \"в2\" (_432_2) can never equal a longer escape (_4322_).\n for (const ch of term.slice(0, depth)) k += /[a-z0-9]/.test(ch) ? ch : `_${ch.codePointAt(0)!.toString(16)}_`;\n return k;\n}\n\n/**\n * Stemming, for units whose language has a stemmer (English, with the\n * Porter stemmer). Applied at query time only: a query word also matches\n * index terms with the same stem, so the index does not grow.\n */\nexport function stemFor(lang: string | undefined): ((term: string) => string) | undefined {\n if (!lang || !/^en(-|$)/i.test(lang)) return undefined;\n return (t) => (/^[a-z]{3,}$/.test(t) ? stemmer(t) : t);\n}\n","/**\n * The query side: parse a query, rank documents from the postings, then\n * refine the top results with their fragments (heading matches, phrases)\n * and pick the headings to show under each result. No DOM and no UI; the\n * data comes through a SearchData, loaded as scripts in a browser.\n */\nimport { FORMAT, file, key, type Fragment, type Meta, type Postings } from \"./format.js\";\nimport { shardKey, stemFor, tokenize } from \"./text.js\";\n\nexport interface SearchData {\n meta(): Promise<Meta>;\n shard(k: string): Promise<Postings | undefined>;\n fragment(block: number): Promise<Fragment[] | undefined>;\n}\n\nexport interface Result {\n id: number;\n title: string;\n url: string;\n section: string;\n score: number;\n /** Loaded for refined results: the excerpt and headings. */\n fragment?: Fragment;\n /** Headings that match the query, to show under the result: [text, anchor id]. */\n sections?: [string, string][];\n}\n\n// ─── Parsing ─────────────────────────────────────────────────────────\n\n/** One required part of a query: any of its terms matches it. */\ninterface Group {\n terms: string[];\n /** From a quoted phrase: matched exactly (no typo correction). */\n exact?: boolean;\n /** Terms one edit away that were used for a word with no other match (for highlighting). */\n corrected?: string[];\n /** The last word of a natural query also matches longer terms. */\n prefix: boolean;\n}\n\nexport interface ParsedQuery {\n groups: Group[];\n /** Exclusions: a document is excluded when it has every term of one set (a word is one term; a phrase is its words). */\n not: string[][];\n /** Quoted phrases, as lower-cased text, checked against titles, headings and excerpts. */\n phrases: string[];\n /** True when the query used quotes or operators: then no fall-back to matching any word. */\n strict: boolean;\n /** Every positive term, for highlighting. */\n highlight: string[];\n /** `tag:x` and `section:\"x\"`, lower-cased: values of one kind are alternatives, and every kind must match. */\n filters: { tag: string[]; section: string[] };\n /** `-tag:x` and `-section:x`: documents with any of these are excluded. */\n notFilters: { tag: string[]; section: string[] };\n}\n\n/**\n * Query syntax, deliberately small: `\"a phrase\"` (all its words, with the\n * exact phrase ranked higher), `-word` or `NOT word` (exclude), `a OR b`\n * (either). Operators are upper case, so ordinary words such as \"or\" and\n * \"not\" in a natural query are not read as operators (they are stop words).\n */\nexport function parseQuery(q: string): ParsedQuery {\n const groups: Group[] = [];\n const not: string[][] = [];\n const phrases: string[] = [];\n let strict = false;\n const filters = { tag: [] as string[], section: [] as string[] };\n const notFilters = { tag: [] as string[], section: [] as string[] };\n // A filter token starts a word: \"mytag:x\" is a word, \"-tag:x\" excludes, \"tag:\" alone is ignored.\n const tokens = [...q.matchAll(/(?<!\\S)(-?)(tag|section):(?:\"([^\"]*)\"?|(\\S*))|(-?)\"([^\"]*)\"?|(\\S+)/gi)].map((m) => ({\n facetNeg: m[1] === \"-\",\n facet: m[2]?.toLowerCase() as \"tag\" | \"section\" | undefined,\n value: m[3] ?? m[4],\n neg: m[5],\n phrase: m[6],\n word: m[7],\n }));\n let negateNext = false;\n let orNext = false;\n for (const [i, m] of tokens.entries()) {\n if (m.facet) {\n const v = facetValue(m.value ?? \"\");\n if (v) (m.facetNeg ? notFilters : filters)[m.facet].push(v);\n continue;\n }\n const word = m.word;\n if (word === \"OR\") {\n orNext = groups.length > 0;\n strict = true;\n continue;\n }\n if (word === \"NOT\") {\n negateNext = true;\n strict = true;\n continue;\n }\n if (word === \"AND\") {\n strict = true;\n continue;\n }\n const isPhrase = m.phrase !== undefined;\n const negate = negateNext || m.neg === \"-\" || (word?.startsWith(\"-\") && word.length > 1);\n negateNext = false;\n const text = isPhrase ? m.phrase! : negate && word?.startsWith(\"-\") ? word.slice(1) : word!;\n const terms = [...new Set(tokenize(text))];\n if (!terms.length) {\n orNext = false;\n continue;\n }\n if (negate) {\n // \"-plug-ins\" excludes pages with \"plug-ins\", not every page with \"plug\" or \"ins\";\n // \"-\\\"foo bar\\\"\" excludes pages with both words.\n not.push(isPhrase ? terms : [terms[0]]);\n strict = true;\n orNext = false;\n continue;\n }\n if (isPhrase) {\n orNext = false;\n strict = true;\n if (terms.length > 1) phrases.push(tokenize(text).join(\" \"));\n for (const t of terms) groups.push({ terms: [t], prefix: false, exact: true });\n continue;\n }\n const last = i === tokens.length - 1 && !/\\s$/.test(q);\n if (orNext) {\n // \"a OR b\": b joins a's group (first term of a multi-term word; the rest stay required).\n groups[groups.length - 1].terms.push(terms[0]);\n for (const t of terms.slice(1)) groups.push({ terms: [t], prefix: false });\n orNext = false;\n continue;\n }\n // The last word typed is a prefix: its last part, and for a compound (\"dita-o\") the whole and joined forms too.\n const whole = terms[0];\n const compound = /[._-]/.test(whole);\n const joined = whole.replace(/-/g, \"\");\n for (const [j, t] of terms.entries())\n groups.push({ terms: [t], prefix: last && t.length >= 2 && (j === terms.length - 1 || (compound && (t === whole || t === joined))) });\n }\n const highlight = [...new Set(groups.flatMap((g) => g.terms))];\n return { groups, not, phrases, strict, highlight, filters, notFilters };\n}\n\n// ─── Ranking ─────────────────────────────────────────────────────────\n\n/** Rank documents from the postings alone (no fragments). */\nexport async function query(q: string | ParsedQuery, data: SearchData): Promise<Result[]> {\n const meta = await data.meta();\n const p = typeof q === \"string\" ? parseQuery(q) : q;\n const excluded = new Set<number>();\n for (const set of p.not) {\n // Documents with every term of the set.\n let docs: Set<number> | undefined;\n for (const t of set) {\n const [k] = shardsFor(t, meta);\n const shard = k ? await data.shard(k) : undefined;\n const list = shard && Object.hasOwn(shard, t) ? shard[t] : [];\n const has = new Set<number>();\n for (let i = 0; i < list.length; i += 2) if (!docs || docs.has(list[i])) has.add(list[i]);\n docs = has;\n }\n for (const d of docs ?? []) excluded.add(d);\n }\n\n const keep = facetFilter(p, meta);\n if (!p.groups.length) {\n // Filters alone list the matching documents, in site order.\n if (!keep) return [];\n const out: Result[] = [];\n meta.docs.forEach(([title, url, section], d) => keep(d) && !excluded.has(d) && out.push({ id: d, title, url, section: meta.sections[section] ?? \"\", score: 1 }));\n return out;\n }\n const perGroup: Map<number, number>[] = [];\n for (const g of p.groups) perGroup.push(await scoreGroup(g, meta, data));\n\n // Every group required; a natural query falls back to any group, scaled by the share matched.\n const combine = (mode: \"and\" | \"or\") => {\n const total = new Map<number, number>();\n const hits = new Map<number, number>();\n for (const m of perGroup)\n for (const [d, s] of m) {\n total.set(d, (total.get(d) ?? 0) + s);\n hits.set(d, (hits.get(d) ?? 0) + 1);\n }\n const out: Result[] = [];\n for (const [d, s] of total) {\n if (excluded.has(d) || (keep && !keep(d)) || (mode === \"and\" && hits.get(d) !== perGroup.length)) continue;\n const [title, url, section] = meta.docs[d];\n out.push({ id: d, title, url, section: meta.sections[section] ?? \"\", score: s * (hits.get(d)! / perGroup.length) });\n }\n return out.sort((a, b) => b.score - a.score);\n };\n const and = combine(\"and\");\n return and.length || p.strict ? and : combine(\"or\");\n}\n\n/**\n * Query-time weights. Mutable so eval/tune.mjs can sweep them; the values are\n * chosen from that evaluation (docs-dev/design.md).\n */\nexport const QUERY_RANK = {\n prefix: 0.6, // a longer term starting with the last word typed\n stem: 0.4, // another form of the word (\"installing\" for \"install\")\n titleAll: 1.5, // the title holds every query word\n titleExact: 2, // the title is the query\n headingAll: 1.4, // a single heading holds every query word\n headingExact: 1.8, // a heading is the query\n phrase: 1.5, // every quoted phrase is in the title, a heading or the excerpt\n typo: 0.3, // a term one edit away, only for a word that matches nothing else\n};\n\n/** True when b is one insertion, deletion, substitution or swap of neighbours away from a. */\nexport function oneEdit(a: string, b: string): boolean {\n if (a === b) return false;\n if (a.length === b.length) {\n const diff: number[] = [];\n for (let i = 0; i < a.length && diff.length <= 2; i++) if (a[i] !== b[i]) diff.push(i);\n if (diff.length === 1) return true;\n return diff.length === 2 && diff[1] === diff[0] + 1 && a[diff[0]] === b[diff[1]] && a[diff[1]] === b[diff[0]];\n }\n const [short, long] = a.length < b.length ? [a, b] : [b, a];\n if (long.length - short.length !== 1) return false;\n let i = 0;\n while (i < short.length && short[i] === long[i]) i++;\n return short.slice(i) === long.slice(i + 1);\n}\n\n/**\n * The shard files holding a term: the one with the longest key that exists\n * (large shards are split on further characters; see index.ts). A prefix\n * shorter than a split also needs the sub-shards under it, up to ten.\n */\nexport function shardsFor(term: string, meta: Meta, prefix = false): string[] {\n const have = shardSet(meta);\n for (let d = Math.min(term.length, MAX_DEPTH); d >= 1; d--) {\n const k = shardKey(term, d);\n if (!have.has(k)) continue;\n if (!prefix || d < term.length) return [k];\n // The term is exactly this key's length: longer terms may sit in deeper shards.\n return [k, ...meta.shards.filter((x) => x.length > k.length && x.startsWith(k)).slice(0, 10)];\n }\n // No file for any key of the term itself: only deeper shards, if the term is a prefix of their keys.\n const k = shardKey(term, Math.min(term.length, MAX_DEPTH));\n return prefix ? meta.shards.filter((x) => x.startsWith(k)).slice(0, 10) : [];\n}\n\nconst MAX_DEPTH = 3;\nconst shardSets = new WeakMap<Meta, Set<string>>();\nfunction shardSet(meta: Meta): Set<string> {\n let s = shardSets.get(meta);\n if (!s) shardSets.set(meta, (s = new Set(meta.shards)));\n return s;\n}\n\n/** Filter values compare lower-cased and without quotes (\"section:\\\"Say hi\\\"\" matches the section `Say \"hi\"`). */\nexport function facetValue(v: string): string {\n return v.replace(/\"/g, \"\").trim().toLowerCase();\n}\n\n/** A test for the query's tag and section filters (and exclusions), or undefined when it has none. */\nfunction facetFilter(p: ParsedQuery, meta: Meta): ((doc: number) => boolean) | undefined {\n const { tag, section } = p.filters;\n const not = p.notFilters ?? { tag: [], section: [] };\n if (!tag.length && !section.length && !not.tag.length && !not.section.length) return undefined;\n const indexes = (names: string[], want: string[]) => new Set(names.flatMap((x, i) => (want.includes(facetValue(x)) ? [i] : [])));\n const sections = indexes(meta.sections, section);\n const tags = indexes(meta.tags ?? [], tag);\n const notSections = indexes(meta.sections, not.section);\n const notTags = indexes(meta.tags ?? [], not.tag);\n return (d) => {\n const doc = meta.docs[d];\n const docTags = doc[3] ?? [];\n if (section.length && !sections.has(doc[2])) return false;\n if (tag.length && !docTags.some((t) => tags.has(t))) return false;\n if (notSections.has(doc[2]) || docTags.some((t) => notTags.has(t))) return false;\n return true;\n };\n}\n\n/** Scores for one group: its terms, terms with the same stem, and completions of a prefix term. */\nasync function scoreGroup(g: Group, meta: Meta, data: SearchData): Promise<Map<number, number>> {\n g.corrected = undefined; // a parse can be run again, against another index\n const stem = stemFor(meta.lang);\n const weights = new Map<string, number>(); // index term → weight\n const shards = new Map<string, Postings>();\n for (const term of g.terms)\n for (const key of shardsFor(term, meta, g.prefix)) {\n const shard = await data.shard(key);\n if (!shard) continue;\n const use = (k: string, w: number) => {\n if (w > (weights.get(k) ?? 0)) {\n weights.set(k, w);\n shards.set(k, shard);\n }\n };\n // Own keys only: shards are plain objects, so \"constructor\" would otherwise find Object.prototype's.\n if (Object.hasOwn(shard, term)) use(term, 1);\n const s = stem?.(term);\n // Same-stem terms share the stem's start (the stemmer only strips suffixes), so they are in this shard.\n const lead = s ? s.slice(0, Math.max(3, s.length - 1)) : undefined;\n let n = 0;\n for (const k in shard) {\n if (k === term) continue;\n if (g.prefix && k.startsWith(term) && n++ < 200) use(k, QUERY_RANK.prefix);\n if (s && lead && k.startsWith(lead) && stem!(k) === s) use(k, QUERY_RANK.stem);\n }\n // Typo tolerance, as a fall-back only: a word of four or more letters that matches nothing\n // tries terms one edit away (a swap of neighbours counts as one) in the same shard. A typo in\n // the first characters, which pick the shard, is not found.\n if (!weights.size && !g.exact && term.length >= 4 && !/[._-]/.test(term))\n for (const k in shard) if (Math.abs(k.length - term.length) <= 1 && oneEdit(term, k)) use(k, QUERY_RANK.typo);\n }\n if ([...weights.values()].some((w) => w === QUERY_RANK.typo)) g.corrected = [...weights].filter(([, w]) => w === QUERY_RANK.typo).map(([k]) => k);\n const lists: [number[], number][] = [...weights].map(([k, w]) => [shards.get(k)![k], w]);\n // Rare query words count for more (BM25's idf). One idf for the whole\n // group, from the number of documents any of its terms appears in: a\n // per-completion idf would let a rare completion (\"nodeselector\") outrank\n // the word typed (\"node\"). The stored scores already carry field weights\n // and length normalisation.\n const docs = new Set<number>();\n for (const [flat] of lists) for (let i = 0; i < flat.length; i += 2) docs.add(flat[i]);\n const idf = Math.log(1 + (meta.count - docs.size + 0.5) / (docs.size + 0.5));\n const scores = new Map<number, number>();\n for (const [flat, w] of lists)\n for (let i = 0; i < flat.length; i += 2) scores.set(flat[i], Math.max(scores.get(flat[i]) ?? 0, flat[i + 1] * w * idf));\n return scores;\n}\n\n// ─── Refinement with fragments ───────────────────────────────────────\n\n/** Load the fragments of these results (one file per block of documents). */\nexport async function loadFragments(results: Result[], data: SearchData): Promise<void> {\n const meta = await data.meta();\n const blocks = [...new Set(results.map((r) => Math.floor(r.id / meta.block)))];\n const loaded = new Map<number, Fragment[] | undefined>();\n await Promise.all(blocks.map(async (b) => loaded.set(b, await data.fragment(b))));\n for (const r of results) r.fragment ??= loaded.get(Math.floor(r.id / meta.block))?.[r.id % meta.block];\n}\n\n\n/**\n * Re-rank the top `n` results with their fragments, which the postings\n * cannot see: whether one heading holds every query word (the postings only\n * know that the words are somewhere in the headings), and whether quoted\n * phrases appear. Also picks the headings to show under each result.\n */\nexport async function refine(p: ParsedQuery, results: Result[], data: SearchData, n = 10): Promise<Result[]> {\n const top = results.slice(0, n);\n await loadFragments(top, data);\n const words = p.groups;\n const stem = stemFor((await data.meta()).lang);\n const hit = (x: string, g: Group) =>\n g.terms.some((t) => x === t || (g.prefix && x.startsWith(t)) || (stem !== undefined && stem(x) === stem(t))) || (g.corrected ?? []).includes(x);\n for (const r of top) {\n const [excerpt, headings] = r.fragment ?? [\"\", []];\n const titleTerms = tokenize(r.title);\n let best = 1;\n if (words.length && words.every((g) => titleTerms.some((x) => hit(x, g)))) best = sameTerms(titleTerms, words, hit) ? QUERY_RANK.titleExact : QUERY_RANK.titleAll;\n const sections: [string, string][] = [];\n for (const [text, id] of headings) {\n const terms = tokenize(text);\n if (!words.length || !words.every((g) => terms.some((x) => hit(x, g)))) continue;\n sections.push([text, id]);\n best = Math.max(best, sameTerms(terms, words, hit) ? QUERY_RANK.headingExact : QUERY_RANK.headingAll);\n }\n r.sections = sections.slice(0, 3);\n if (p.phrases.length) {\n const hay = [r.title, excerpt, ...headings.map(([t]) => t)].map((s) => tokenize(s).join(\" \")).join(\" \\u0000 \");\n if (p.phrases.every((ph) => hay.includes(ph))) best *= QUERY_RANK.phrase;\n }\n r.score *= best;\n }\n top.sort((a, b) => b.score - a.score);\n return [...top, ...results.slice(n)];\n}\n\n/** Load fragments for results outside the refined top, and pick their matching headings (no re-ranking). */\nexport async function loadDetails(p: ParsedQuery, results: Result[], data: SearchData): Promise<void> {\n await loadFragments(results, data);\n const stem = stemFor((await data.meta()).lang);\n const hit = (x: string, g: Group) =>\n g.terms.some((t) => x === t || (g.prefix && x.startsWith(t)) || (stem !== undefined && stem(x) === stem(t))) || (g.corrected ?? []).includes(x);\n for (const r of results) {\n if (!r.fragment || r.sections) continue;\n r.sections = r.fragment[1].filter(([text]) => {\n const terms = tokenize(text);\n return p.groups.length > 0 && p.groups.every((g) => terms.some((x) => hit(x, g)));\n }).slice(0, 3);\n }\n}\n\n/** The heading is the query: every heading term answers a query word, and every word is answered. */\nfunction sameTerms(terms: string[], groups: Group[], hit: (x: string, g: Group) => boolean): boolean {\n return terms.every((x) => groups.some((g) => hit(x, g))) && groups.every((g) => terms.some((x) => hit(x, g)));\n}\n\n/** Query, then refine the top results: what a search box shows. */\nexport async function search(q: string, data: SearchData, n = 10): Promise<{ parsed: ParsedQuery; results: Result[] }> {\n const parsed = parseQuery(q);\n // Highlight other forms of the words too: a stem matches at the start of a word (\"instal\" marks \"installing\").\n const stem = stemFor((await data.meta()).lang);\n if (stem) parsed.highlight = [...new Set([...parsed.highlight, ...parsed.highlight.map(stem).filter((s) => s.length >= 3)])];\n const ranked = await query(parsed, data);\n // Mark the words a typo was read as, too.\n parsed.highlight = [...new Set([...parsed.highlight, ...parsed.groups.flatMap((g) => g.corrected ?? [])])];\n return { parsed, results: await refine(parsed, ranked, data, n) };\n}\n\n// ─── Loading as classic scripts ──────────────────────────────────────\n\ninterface SearchGlobal {\n q: [string, unknown][];\n load: (k: string, d: unknown) => void;\n}\n\n/**\n * SearchData that loads the index files as <script> tags from `base` (the\n * unit's `_dogsbay/search/` URL), which works from file:// and over HTTP.\n */\nexport function scriptData(base: string, loadScript: (src: string) => Promise<void>, g: { __DOGSBAY_SEARCH__?: SearchGlobal } = globalThis as never): SearchData {\n const store = new Map<string, unknown>();\n const global: SearchGlobal = g.__DOGSBAY_SEARCH__ ?? { q: [], load() {} };\n for (const [k, d] of global.q) store.set(k, d);\n global.q = [];\n global.load = (k, d) => void store.set(k, d);\n g.__DOGSBAY_SEARCH__ = global;\n\n const get = async <T>(k: string, src: string): Promise<T | undefined> => {\n if (!store.has(k)) await loadScript(base + src);\n return store.get(k) as T | undefined;\n };\n let meta: Promise<Meta> | undefined;\n const data: SearchData = {\n meta: () =>\n (meta ??= get<Meta>(key.docs, file.docs).then((m) => {\n if (!m || m.format !== FORMAT) throw new Error(`search index format ${m?.format ?? \"missing\"}, expected ${FORMAT}: rebuild the site`);\n return m;\n })),\n shard: async (k) => ((await data.meta()).shards.includes(k) ? get<Postings>(key.shard(k), file.shard(k)) : undefined),\n fragment: (b) => get<Fragment[]>(key.fragment(b), file.fragment(b)),\n };\n return data;\n}\n\n/** SearchData over already-loaded data, keyed as load() keys it (tests, tools). */\nexport function memoryData(store: Map<string, unknown>): SearchData {\n return {\n meta: async () => store.get(key.docs) as Meta,\n shard: async (k) => store.get(key.shard(k)) as Postings | undefined,\n fragment: async (b) => store.get(key.fragment(b)) as Fragment[] | undefined,\n };\n}\n"],"mappings":"ucAAA,IAAAA,GAAA,GAAAC,GAAAD,GAAA,iBAAAE,EAAA,eAAAC,GAAA,eAAAC,EAAA,eAAAC,EAAA,WAAAC,ICyCO,IAAMC,EAAO,CAClB,KAAM,UACN,MAAQC,GAAgB,KAAKA,CAAG,MAChC,SAAWC,GAAkB,KAAKA,CAAK,KACzC,EAGaD,EAAM,CACjB,KAAM,OACN,MAAQE,GAAcA,EACtB,SAAWD,GAAkB,QAAQA,CAAK,EAC5C,EClDA,IAAME,GAAY,CAChB,QAAS,MACT,OAAQ,OACR,KAAM,OACN,KAAM,OACN,KAAM,MACN,IAAK,MACL,KAAM,KACN,MAAO,MACP,IAAK,IACL,MAAO,MACP,QAAS,MACT,MAAO,MACP,KAAM,MACN,MAAO,KACP,QAAS,MACT,QAAS,MACT,QAAS,MACT,MAAO,KACP,MAAO,MACP,OAAQ,MACR,KAAM,KACR,EAGMC,GAAY,CAChB,MAAO,KACP,MAAO,GACP,MAAO,KACP,MAAO,KACP,KAAM,KACN,IAAK,GACL,KAAM,EACR,EAGMC,GAAY,WACZC,EAAQ,WACRC,EAAa,IAAMF,GAAY,cAC/BG,EAAS,IAAMF,EAAQ,YAEvBG,EAAM,IAAI,OAAO,IAAMF,EAAa,IAAMC,EAASD,CAAU,EAC7DG,GAAM,IAAI,OACd,IAAMH,EAAa,IAAMC,EAASD,EAAaC,EAAS,IAC1D,EACMG,EAAM,IAAI,OAAO,IAAMJ,EAAa,KAAOC,EAASD,EAAa,OAAO,EACxEK,EAAc,IAAI,OAAO,IAAML,EAAa,IAAMD,CAAK,EACvDO,EAAgB,IAAI,OAAO,IAAMN,EAAaD,EAAQ,cAAc,EAGpEQ,GAAQ,MACRC,GAAO,WACPC,GAAO,WACPC,GAAS,oBACTC,GAAa,kBACbC,GAAgB,cAChBC,GAAS,aACTC,GAAO,aACPC,GAAe,gBACfC,GAAwB,oBACxBC,GACJ,2IACIC,GAAQ,iDACRC,GACJ,sFAWK,SAASC,EAAQC,EAAO,CAC7B,IAAIC,EAAS,OAAOD,CAAK,EAAE,YAAY,EAGvC,GAAIC,EAAO,OAAS,EAClB,OAAOA,EAIT,IAAIC,EAA8B,GAIhCD,EAAO,YAAY,CAAC,IAAM,MAE1BC,EAA8B,GAC9BD,EAAS,IAAMA,EAAO,MAAM,CAAC,GAI3BP,GAAa,KAAKO,CAAM,EAE1BA,EAASA,EAAO,MAAM,EAAG,EAAE,EAClBR,GAAK,KAAKQ,CAAM,IAEzBA,EAASA,EAAO,MAAM,EAAG,EAAE,GAI7B,IAAIE,EAGJ,OAAKA,EAAQX,GAAO,KAAKS,CAAM,GACzBpB,EAAI,KAAKsB,EAAM,CAAC,CAAC,IAEnBF,EAASA,EAAO,MAAM,EAAG,EAAE,IAEnBE,EAAQb,GAAW,KAAKW,CAAM,IAAMjB,EAAY,KAAKmB,EAAM,CAAC,CAAC,IACvEF,EAASE,EAAM,CAAC,EAEZZ,GAAc,KAAKU,CAAM,EAE3BA,GAAU,IACDN,GAAsB,KAAKM,CAAM,EAE1CA,EAASA,EAAO,MAAM,EAAG,EAAE,EAClBhB,EAAc,KAAKgB,CAAM,IAElCA,GAAU,OAKTE,EAAQf,GAAK,KAAKa,CAAM,IAAMjB,EAAY,KAAKmB,EAAM,CAAC,CAAC,IAE1DF,EAASE,EAAM,CAAC,EAAI,MAIjBA,EAAQP,GAAM,KAAKK,CAAM,IAAMpB,EAAI,KAAKsB,EAAM,CAAC,CAAC,IACnDF,EAASE,EAAM,CAAC,EAAI5B,GAAU4B,EAAM,CAAC,CAAC,IAInCA,EAAQN,GAAM,KAAKI,CAAM,IAAMpB,EAAI,KAAKsB,EAAM,CAAC,CAAC,IACnDF,EAASE,EAAM,CAAC,EAAI3B,GAAU2B,EAAM,CAAC,CAAC,IAInCA,EAAQL,GAAM,KAAKG,CAAM,GACxBlB,EAAI,KAAKoB,EAAM,CAAC,CAAC,IACnBF,EAASE,EAAM,CAAC,IAERA,EAAQd,GAAO,KAAKY,CAAM,IAAMlB,EAAI,KAAKoB,EAAM,CAAC,CAAC,IAC3DF,EAASE,EAAM,CAAC,IAKfA,EAAQhB,GAAK,KAAKc,CAAM,KACxBlB,EAAI,KAAKoB,EAAM,CAAC,CAAC,GACfrB,GAAI,KAAKqB,EAAM,CAAC,CAAC,GAAK,CAAClB,EAAc,KAAKkB,EAAM,CAAC,CAAC,KAErDF,EAASE,EAAM,CAAC,GAGdjB,GAAM,KAAKe,CAAM,GAAKlB,EAAI,KAAKkB,CAAM,IACvCA,EAASA,EAAO,MAAM,EAAG,EAAE,GAIzBC,IACFD,EAAS,IAAMA,EAAO,MAAM,CAAC,GAGxBA,CACT,CCxKA,IAAMG,EAAO,IAAI,IACf,wHAAwH,MAAM,GAAG,CACnI,EAGMC,GAAM,2EAERC,EAQJ,SAASC,GAASC,EAAuB,CACvC,GAAIF,IAAc,OAChB,GAAI,CACFA,EAAY,OAAO,KAAS,KAAe,cAAe,KAAO,IAAK,KAAa,UAAU,OAAW,CAAE,YAAa,MAAO,CAAC,EAAI,IACrI,MAAQ,CACNA,EAAY,IACd,CAEF,GAAIA,EAAW,MAAO,CAAC,GAAGA,EAAU,QAAQE,CAAG,CAAC,EAAE,OAAQC,GAAMA,EAAE,UAAU,EAAE,IAAKA,GAAMA,EAAE,OAAO,EAClG,IAAMC,EAAQ,CAAC,GAAGF,CAAG,EACrB,GAAIE,EAAM,OAAS,EAAG,OAAOA,EAC7B,IAAMC,EAAgB,CAAC,EACvB,QAASC,EAAI,EAAGA,EAAIF,EAAM,OAAS,EAAGE,IAAKD,EAAI,KAAKD,EAAME,CAAC,EAAIF,EAAME,EAAI,CAAC,CAAC,EAC3E,OAAOD,CACT,CAGO,SAASE,EAASC,EAAwB,CAC/C,IAAMH,EAAgB,CAAC,EACvB,QAAWI,KAAKD,EAAK,YAAY,EAAE,UAAU,MAAM,EAAE,QAAQ,SAAU,EAAE,EAAE,UAAU,KAAK,EAAE,SAAS,yDAAyD,EAAG,CAC/J,IAAME,EAAMD,EAAE,CAAC,EAETE,EAAQD,EAAI,MAAM,QAAQ,EAAE,OAAO,OAAO,EAGhD,GAFIC,EAAM,OAAS,GAAGN,EAAI,KAAKK,CAAG,EAE9BC,EAAM,OAAS,GAAK,CAAC,OAAO,KAAKD,CAAG,EAAG,CACzC,IAAME,EAASD,EAAM,KAAK,EAAE,EACxBC,EAAO,QAAU,GAAK,CAACd,EAAK,IAAIc,CAAM,GAAGP,EAAI,KAAKO,CAAM,CAC9D,CACA,QAAWC,KAAKF,EACVZ,GAAI,KAAKc,CAAC,EAAGR,EAAI,KAAK,GAAGJ,GAASY,CAAC,CAAC,EAC/BA,EAAE,QAAU,GAAK,CAACf,EAAK,IAAIe,CAAC,GAAGR,EAAI,KAAKQ,CAAC,CAEtD,CACA,OAAOR,CACT,CAGO,SAASS,EAASC,EAAcC,EAAuB,CAC5D,IAAIC,EAAI,GAER,QAAWC,KAAMH,EAAK,MAAM,EAAGC,CAAK,EAAGC,GAAK,WAAW,KAAKC,CAAE,EAAIA,EAAK,IAAIA,EAAG,YAAY,CAAC,EAAG,SAAS,EAAE,CAAC,IAC1G,OAAOD,CACT,CAOO,SAASE,EAAQC,EAAkE,CACxF,GAAI,GAACA,GAAQ,CAAC,YAAY,KAAKA,CAAI,GACnC,OAAQ,GAAO,cAAc,KAAK,CAAC,EAAIC,EAAQ,CAAC,EAAI,CACtD,CCXO,SAASC,EAAWC,EAAwB,CACjD,IAAMC,EAAkB,CAAC,EACnBC,EAAkB,CAAC,EACnBC,EAAoB,CAAC,EACvBC,EAAS,GACPC,EAAU,CAAE,IAAK,CAAC,EAAe,QAAS,CAAC,CAAc,EACzDC,EAAa,CAAE,IAAK,CAAC,EAAe,QAAS,CAAC,CAAc,EAE5DC,EAAS,CAAC,GAAGP,EAAE,SAAS,sEAAsE,CAAC,EAAE,IAAKQ,IAAO,CACjH,SAAUA,EAAE,CAAC,IAAM,IACnB,MAAOA,EAAE,CAAC,GAAG,YAAY,EACzB,MAAOA,EAAE,CAAC,GAAKA,EAAE,CAAC,EAClB,IAAKA,EAAE,CAAC,EACR,OAAQA,EAAE,CAAC,EACX,KAAMA,EAAE,CAAC,CACX,EAAE,EACEC,EAAa,GACbC,EAAS,GACb,OAAW,CAACC,EAAGH,CAAC,IAAKD,EAAO,QAAQ,EAAG,CACrC,GAAIC,EAAE,MAAO,CACX,IAAMI,EAAIC,EAAWL,EAAE,OAAS,EAAE,EAC9BI,IAAIJ,EAAE,SAAWF,EAAaD,GAASG,EAAE,KAAK,EAAE,KAAKI,CAAC,EAC1D,QACF,CACA,IAAME,EAAON,EAAE,KACf,GAAIM,IAAS,KAAM,CACjBJ,EAAST,EAAO,OAAS,EACzBG,EAAS,GACT,QACF,CACA,GAAIU,IAAS,MAAO,CAClBL,EAAa,GACbL,EAAS,GACT,QACF,CACA,GAAIU,IAAS,MAAO,CAClBV,EAAS,GACT,QACF,CACA,IAAMW,EAAWP,EAAE,SAAW,OACxBQ,EAASP,GAAcD,EAAE,MAAQ,KAAQM,GAAM,WAAW,GAAG,GAAKA,EAAK,OAAS,EACtFL,EAAa,GACb,IAAMQ,EAAOF,EAAWP,EAAE,OAAUQ,GAAUF,GAAM,WAAW,GAAG,EAAIA,EAAK,MAAM,CAAC,EAAIA,EAChFI,EAAQ,CAAC,GAAG,IAAI,IAAIC,EAASF,CAAI,CAAC,CAAC,EACzC,GAAI,CAACC,EAAM,OAAQ,CACjBR,EAAS,GACT,QACF,CACA,GAAIM,EAAQ,CAGVd,EAAI,KAAKa,EAAWG,EAAQ,CAACA,EAAM,CAAC,CAAC,CAAC,EACtCd,EAAS,GACTM,EAAS,GACT,QACF,CACA,GAAIK,EAAU,CACZL,EAAS,GACTN,EAAS,GACLc,EAAM,OAAS,GAAGf,EAAQ,KAAKgB,EAASF,CAAI,EAAE,KAAK,GAAG,CAAC,EAC3D,QAAWG,KAAKF,EAAOjB,EAAO,KAAK,CAAE,MAAO,CAACmB,CAAC,EAAG,OAAQ,GAAO,MAAO,EAAK,CAAC,EAC7E,QACF,CACA,IAAMC,EAAOV,IAAMJ,EAAO,OAAS,GAAK,CAAC,MAAM,KAAKP,CAAC,EACrD,GAAIU,EAAQ,CAEVT,EAAOA,EAAO,OAAS,CAAC,EAAE,MAAM,KAAKiB,EAAM,CAAC,CAAC,EAC7C,QAAWE,KAAKF,EAAM,MAAM,CAAC,EAAGjB,EAAO,KAAK,CAAE,MAAO,CAACmB,CAAC,EAAG,OAAQ,EAAM,CAAC,EACzEV,EAAS,GACT,QACF,CAEA,IAAMY,EAAQJ,EAAM,CAAC,EACfK,EAAW,QAAQ,KAAKD,CAAK,EAC7BE,EAASF,EAAM,QAAQ,KAAM,EAAE,EACrC,OAAW,CAACG,EAAGL,CAAC,IAAKF,EAAM,QAAQ,EACjCjB,EAAO,KAAK,CAAE,MAAO,CAACmB,CAAC,EAAG,OAAQC,GAAQD,EAAE,QAAU,IAAMK,IAAMP,EAAM,OAAS,GAAMK,IAAaH,IAAME,GAASF,IAAMI,GAAU,CAAC,CACxI,CACA,IAAME,EAAY,CAAC,GAAG,IAAI,IAAIzB,EAAO,QAAS0B,GAAMA,EAAE,KAAK,CAAC,CAAC,EAC7D,MAAO,CAAE,OAAA1B,EAAQ,IAAAC,EAAK,QAAAC,EAAS,OAAAC,EAAQ,UAAAsB,EAAW,QAAArB,EAAS,WAAAC,CAAW,CACxE,CAKA,eAAsBsB,GAAM5B,EAAyB6B,EAAqC,CACxF,IAAMC,EAAO,MAAMD,EAAK,KAAK,EACvBE,EAAI,OAAO/B,GAAM,SAAWD,EAAWC,CAAC,EAAIA,EAC5CgC,EAAW,IAAI,IACrB,QAAWC,KAAOF,EAAE,IAAK,CAEvB,IAAIG,EACJ,QAAWd,KAAKa,EAAK,CACnB,GAAM,CAACE,CAAC,EAAIC,EAAUhB,EAAGU,CAAI,EACvBO,EAAQF,EAAI,MAAMN,EAAK,MAAMM,CAAC,EAAI,OAClCG,EAAOD,GAAS,OAAO,OAAOA,EAAOjB,CAAC,EAAIiB,EAAMjB,CAAC,EAAI,CAAC,EACtDmB,EAAM,IAAI,IAChB,QAAS5B,EAAI,EAAGA,EAAI2B,EAAK,OAAQ3B,GAAK,GAAO,CAACuB,GAAQA,EAAK,IAAII,EAAK3B,CAAC,CAAC,IAAG4B,EAAI,IAAID,EAAK3B,CAAC,CAAC,EACxFuB,EAAOK,CACT,CACA,QAAWC,KAAKN,GAAQ,CAAC,EAAGF,EAAS,IAAIQ,CAAC,CAC5C,CAEA,IAAMC,EAAOC,GAAYX,EAAGD,CAAI,EAChC,GAAI,CAACC,EAAE,OAAO,OAAQ,CAEpB,GAAI,CAACU,EAAM,MAAO,CAAC,EACnB,IAAME,EAAgB,CAAC,EACvB,OAAAb,EAAK,KAAK,QAAQ,CAAC,CAACc,EAAOC,EAAKC,CAAO,EAAGN,IAAMC,EAAKD,CAAC,GAAK,CAACR,EAAS,IAAIQ,CAAC,GAAKG,EAAI,KAAK,CAAE,GAAIH,EAAG,MAAAI,EAAO,IAAAC,EAAK,QAASf,EAAK,SAASgB,CAAO,GAAK,GAAI,MAAO,CAAE,CAAC,CAAC,EACxJH,CACT,CACA,IAAMI,EAAkC,CAAC,EACzC,QAAWpB,KAAKI,EAAE,OAAQgB,EAAS,KAAK,MAAMC,GAAWrB,EAAGG,EAAMD,CAAI,CAAC,EAGvE,IAAMoB,EAAWC,GAAuB,CACtC,IAAMC,EAAQ,IAAI,IACZC,EAAO,IAAI,IACjB,QAAW5C,KAAKuC,EACd,OAAW,CAACP,EAAGa,CAAC,IAAK7C,EACnB2C,EAAM,IAAIX,GAAIW,EAAM,IAAIX,CAAC,GAAK,GAAKa,CAAC,EACpCD,EAAK,IAAIZ,GAAIY,EAAK,IAAIZ,CAAC,GAAK,GAAK,CAAC,EAEtC,IAAMG,EAAgB,CAAC,EACvB,OAAW,CAACH,EAAGa,CAAC,IAAKF,EAAO,CAC1B,GAAInB,EAAS,IAAIQ,CAAC,GAAMC,GAAQ,CAACA,EAAKD,CAAC,GAAOU,IAAS,OAASE,EAAK,IAAIZ,CAAC,IAAMO,EAAS,OAAS,SAClG,GAAM,CAACH,EAAOC,EAAKC,CAAO,EAAIhB,EAAK,KAAKU,CAAC,EACzCG,EAAI,KAAK,CAAE,GAAIH,EAAG,MAAAI,EAAO,IAAAC,EAAK,QAASf,EAAK,SAASgB,CAAO,GAAK,GAAI,MAAOO,GAAKD,EAAK,IAAIZ,CAAC,EAAKO,EAAS,OAAQ,CAAC,CACpH,CACA,OAAOJ,EAAI,KAAK,CAACW,EAAGC,IAAMA,EAAE,MAAQD,EAAE,KAAK,CAC7C,EACME,EAAMP,EAAQ,KAAK,EACzB,OAAOO,EAAI,QAAUzB,EAAE,OAASyB,EAAMP,EAAQ,IAAI,CACpD,CAMO,IAAMQ,EAAa,CACxB,OAAQ,GACR,KAAM,GACN,SAAU,IACV,WAAY,EACZ,WAAY,IACZ,aAAc,IACd,OAAQ,IACR,KAAM,EACR,EAGO,SAASC,GAAQJ,EAAWC,EAAoB,CACrD,GAAID,IAAMC,EAAG,MAAO,GACpB,GAAID,EAAE,SAAWC,EAAE,OAAQ,CACzB,IAAMI,EAAiB,CAAC,EACxB,QAAShD,EAAI,EAAGA,EAAI2C,EAAE,QAAUK,EAAK,QAAU,EAAGhD,IAAS2C,EAAE3C,CAAC,IAAM4C,EAAE5C,CAAC,GAAGgD,EAAK,KAAKhD,CAAC,EACrF,OAAIgD,EAAK,SAAW,EAAU,GACvBA,EAAK,SAAW,GAAKA,EAAK,CAAC,IAAMA,EAAK,CAAC,EAAI,GAAKL,EAAEK,EAAK,CAAC,CAAC,IAAMJ,EAAEI,EAAK,CAAC,CAAC,GAAKL,EAAEK,EAAK,CAAC,CAAC,IAAMJ,EAAEI,EAAK,CAAC,CAAC,CAC9G,CACA,GAAM,CAACC,EAAOC,CAAI,EAAIP,EAAE,OAASC,EAAE,OAAS,CAACD,EAAGC,CAAC,EAAI,CAACA,EAAGD,CAAC,EAC1D,GAAIO,EAAK,OAASD,EAAM,SAAW,EAAG,MAAO,GAC7C,IAAIjD,EAAI,EACR,KAAOA,EAAIiD,EAAM,QAAUA,EAAMjD,CAAC,IAAMkD,EAAKlD,CAAC,GAAGA,IACjD,OAAOiD,EAAM,MAAMjD,CAAC,IAAMkD,EAAK,MAAMlD,EAAI,CAAC,CAC5C,CAOO,SAASyB,EAAU0B,EAAchC,EAAYiC,EAAS,GAAiB,CAC5E,IAAMC,EAAOC,GAASnC,CAAI,EAC1B,QAASU,EAAI,KAAK,IAAIsB,EAAK,OAAQI,CAAS,EAAG1B,GAAK,EAAGA,IAAK,CAC1D,IAAML,EAAIgC,EAASL,EAAMtB,CAAC,EAC1B,GAAKwB,EAAK,IAAI7B,CAAC,EACf,MAAI,CAAC4B,GAAUvB,EAAIsB,EAAK,OAAe,CAAC3B,CAAC,EAElC,CAACA,EAAG,GAAGL,EAAK,OAAO,OAAQsC,GAAMA,EAAE,OAASjC,EAAE,QAAUiC,EAAE,WAAWjC,CAAC,CAAC,EAAE,MAAM,EAAG,EAAE,CAAC,CAC9F,CAEA,IAAMA,EAAIgC,EAASL,EAAM,KAAK,IAAIA,EAAK,OAAQI,CAAS,CAAC,EACzD,OAAOH,EAASjC,EAAK,OAAO,OAAQsC,GAAMA,EAAE,WAAWjC,CAAC,CAAC,EAAE,MAAM,EAAG,EAAE,EAAI,CAAC,CAC7E,CAEA,IAAM+B,EAAY,EACZG,EAAY,IAAI,QACtB,SAASJ,GAASnC,EAAyB,CACzC,IAAIuB,EAAIgB,EAAU,IAAIvC,CAAI,EAC1B,OAAKuB,GAAGgB,EAAU,IAAIvC,EAAOuB,EAAI,IAAI,IAAIvB,EAAK,MAAM,CAAE,EAC/CuB,CACT,CAGO,SAASxC,EAAWD,EAAmB,CAC5C,OAAOA,EAAE,QAAQ,KAAM,EAAE,EAAE,KAAK,EAAE,YAAY,CAChD,CAGA,SAAS8B,GAAYX,EAAgBD,EAAoD,CACvF,GAAM,CAAE,IAAAwC,EAAK,QAAAxB,CAAQ,EAAIf,EAAE,QACrB7B,EAAM6B,EAAE,YAAc,CAAE,IAAK,CAAC,EAAG,QAAS,CAAC,CAAE,EACnD,GAAI,CAACuC,EAAI,QAAU,CAACxB,EAAQ,QAAU,CAAC5C,EAAI,IAAI,QAAU,CAACA,EAAI,QAAQ,OAAQ,OAC9E,IAAMqE,EAAU,CAACC,EAAiBC,IAAmB,IAAI,IAAID,EAAM,QAAQ,CAACJ,EAAGzD,IAAO8D,EAAK,SAAS5D,EAAWuD,CAAC,CAAC,EAAI,CAACzD,CAAC,EAAI,CAAC,CAAE,CAAC,EACzH+D,EAAWH,EAAQzC,EAAK,SAAUgB,CAAO,EACzC6B,EAAOJ,EAAQzC,EAAK,MAAQ,CAAC,EAAGwC,CAAG,EACnCM,EAAcL,EAAQzC,EAAK,SAAU5B,EAAI,OAAO,EAChD2E,EAAUN,EAAQzC,EAAK,MAAQ,CAAC,EAAG5B,EAAI,GAAG,EAChD,OAAQsC,GAAM,CACZ,IAAMsC,EAAMhD,EAAK,KAAKU,CAAC,EACjBuC,EAAUD,EAAI,CAAC,GAAK,CAAC,EAG3B,MAFI,EAAAhC,EAAQ,QAAU,CAAC4B,EAAS,IAAII,EAAI,CAAC,CAAC,GACtCR,EAAI,QAAU,CAACS,EAAQ,KAAM3D,GAAMuD,EAAK,IAAIvD,CAAC,CAAC,GAC9CwD,EAAY,IAAIE,EAAI,CAAC,CAAC,GAAKC,EAAQ,KAAM3D,GAAMyD,EAAQ,IAAIzD,CAAC,CAAC,EAEnE,CACF,CAGA,eAAe4B,GAAWrB,EAAUG,EAAYD,EAAgD,CAC9FF,EAAE,UAAY,OACd,IAAMqD,EAAOC,EAAQnD,EAAK,IAAI,EACxBoD,EAAU,IAAI,IACdC,EAAS,IAAI,IACnB,QAAWrB,KAAQnC,EAAE,MACnB,QAAWyD,KAAOhD,EAAU0B,EAAMhC,EAAMH,EAAE,MAAM,EAAG,CACnD,IAAMU,EAAQ,MAAMR,EAAK,MAAMuD,CAAG,EAClC,GAAI,CAAC/C,EAAO,SACZ,IAAMgD,EAAM,CAAClD,EAAWmD,IAAc,CAChCA,GAAKJ,EAAQ,IAAI/C,CAAC,GAAK,KACzB+C,EAAQ,IAAI/C,EAAGmD,CAAC,EAChBH,EAAO,IAAIhD,EAAGE,CAAK,EAEvB,EAEI,OAAO,OAAOA,EAAOyB,CAAI,GAAGuB,EAAIvB,EAAM,CAAC,EAC3C,IAAMT,EAAI2B,IAAOlB,CAAI,EAEfyB,EAAOlC,EAAIA,EAAE,MAAM,EAAG,KAAK,IAAI,EAAGA,EAAE,OAAS,CAAC,CAAC,EAAI,OACrDmC,EAAI,EACR,QAAWrD,KAAKE,EACVF,IAAM2B,IACNnC,EAAE,QAAUQ,EAAE,WAAW2B,CAAI,GAAK0B,IAAM,KAAKH,EAAIlD,EAAGsB,EAAW,MAAM,EACrEJ,GAAKkC,GAAQpD,EAAE,WAAWoD,CAAI,GAAKP,EAAM7C,CAAC,IAAMkB,GAAGgC,EAAIlD,EAAGsB,EAAW,IAAI,GAK/E,GAAI,CAACyB,EAAQ,MAAQ,CAACvD,EAAE,OAASmC,EAAK,QAAU,GAAK,CAAC,QAAQ,KAAKA,CAAI,EACrE,QAAW3B,KAAKE,EAAW,KAAK,IAAIF,EAAE,OAAS2B,EAAK,MAAM,GAAK,GAAKJ,GAAQI,EAAM3B,CAAC,GAAGkD,EAAIlD,EAAGsB,EAAW,IAAI,CAC9G,CACE,CAAC,GAAGyB,EAAQ,OAAO,CAAC,EAAE,KAAMI,GAAMA,IAAM7B,EAAW,IAAI,IAAG9B,EAAE,UAAY,CAAC,GAAGuD,CAAO,EAAE,OAAO,CAAC,CAAC,CAAEI,CAAC,IAAMA,IAAM7B,EAAW,IAAI,EAAE,IAAI,CAAC,CAACtB,CAAC,IAAMA,CAAC,GAChJ,IAAMsD,EAA8B,CAAC,GAAGP,CAAO,EAAE,IAAI,CAAC,CAAC/C,EAAGmD,CAAC,IAAM,CAACH,EAAO,IAAIhD,CAAC,EAAGA,CAAC,EAAGmD,CAAC,CAAC,EAMjFpD,EAAO,IAAI,IACjB,OAAW,CAACwD,CAAI,IAAKD,EAAO,QAAS9E,EAAI,EAAGA,EAAI+E,EAAK,OAAQ/E,GAAK,EAAGuB,EAAK,IAAIwD,EAAK/E,CAAC,CAAC,EACrF,IAAMgF,EAAM,KAAK,IAAI,GAAK7D,EAAK,MAAQI,EAAK,KAAO,KAAQA,EAAK,KAAO,GAAI,EACrE0D,EAAS,IAAI,IACnB,OAAW,CAACF,EAAMJ,CAAC,IAAKG,EACtB,QAAS9E,EAAI,EAAGA,EAAI+E,EAAK,OAAQ/E,GAAK,EAAGiF,EAAO,IAAIF,EAAK/E,CAAC,EAAG,KAAK,IAAIiF,EAAO,IAAIF,EAAK/E,CAAC,CAAC,GAAK,EAAG+E,EAAK/E,EAAI,CAAC,EAAI2E,EAAIK,CAAG,CAAC,EACxH,OAAOC,CACT,CAKA,eAAsBC,EAAcC,EAAmBjE,EAAiC,CACtF,IAAMC,EAAO,MAAMD,EAAK,KAAK,EACvBkE,EAAS,CAAC,GAAG,IAAI,IAAID,EAAQ,IAAKE,GAAM,KAAK,MAAMA,EAAE,GAAKlE,EAAK,KAAK,CAAC,CAAC,CAAC,EACvEmE,EAAS,IAAI,IACnB,MAAM,QAAQ,IAAIF,EAAO,IAAI,MAAOxC,GAAM0C,EAAO,IAAI1C,EAAG,MAAM1B,EAAK,SAAS0B,CAAC,CAAC,CAAC,CAAC,EAChF,QAAWyC,KAAKF,EAASE,EAAE,WAAFA,EAAE,SAAaC,EAAO,IAAI,KAAK,MAAMD,EAAE,GAAKlE,EAAK,KAAK,CAAC,IAAIkE,EAAE,GAAKlE,EAAK,KAAK,EACvG,CASA,eAAsBoE,GAAOnE,EAAgB+D,EAAmBjE,EAAkB2D,EAAI,GAAuB,CAC3G,IAAMW,EAAML,EAAQ,MAAM,EAAGN,CAAC,EAC9B,MAAMK,EAAcM,EAAKtE,CAAI,EAC7B,IAAMuE,EAAQrE,EAAE,OACViD,EAAOC,GAAS,MAAMpD,EAAK,KAAK,GAAG,IAAI,EACvCwE,EAAM,CAACjC,EAAWzC,IACtBA,EAAE,MAAM,KAAMP,GAAMgD,IAAMhD,GAAMO,EAAE,QAAUyC,EAAE,WAAWhD,CAAC,GAAO4D,IAAS,QAAaA,EAAKZ,CAAC,IAAMY,EAAK5D,CAAC,CAAE,IAAMO,EAAE,WAAa,CAAC,GAAG,SAASyC,CAAC,EAChJ,QAAW4B,KAAKG,EAAK,CACnB,GAAM,CAACG,EAASC,CAAQ,EAAIP,EAAE,UAAY,CAAC,GAAI,CAAC,CAAC,EAC3CQ,EAAarF,EAAS6E,EAAE,KAAK,EAC/BS,EAAO,EACPL,EAAM,QAAUA,EAAM,MAAOzE,GAAM6E,EAAW,KAAMpC,GAAMiC,EAAIjC,EAAGzC,CAAC,CAAC,CAAC,IAAG8E,EAAOC,EAAUF,EAAYJ,EAAOC,CAAG,EAAI5C,EAAW,WAAaA,EAAW,UACzJ,IAAMiB,EAA+B,CAAC,EACtC,OAAW,CAACzD,EAAM0F,CAAE,IAAKJ,EAAU,CACjC,IAAMrF,EAAQC,EAASF,CAAI,EACvB,CAACmF,EAAM,QAAU,CAACA,EAAM,MAAOzE,GAAMT,EAAM,KAAMkD,GAAMiC,EAAIjC,EAAGzC,CAAC,CAAC,CAAC,IACrE+C,EAAS,KAAK,CAACzD,EAAM0F,CAAE,CAAC,EACxBF,EAAO,KAAK,IAAIA,EAAMC,EAAUxF,EAAOkF,EAAOC,CAAG,EAAI5C,EAAW,aAAeA,EAAW,UAAU,EACtG,CAEA,GADAuC,EAAE,SAAWtB,EAAS,MAAM,EAAG,CAAC,EAC5B3C,EAAE,QAAQ,OAAQ,CACpB,IAAM6E,EAAM,CAACZ,EAAE,MAAOM,EAAS,GAAGC,EAAS,IAAI,CAAC,CAACnF,CAAC,IAAMA,CAAC,CAAC,EAAE,IAAKiC,GAAMlC,EAASkC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,MAAU,EACzGtB,EAAE,QAAQ,MAAO8E,GAAOD,EAAI,SAASC,CAAE,CAAC,IAAGJ,GAAQhD,EAAW,OACpE,CACAuC,EAAE,OAASS,CACb,CACA,OAAAN,EAAI,KAAK,CAAC,EAAG5C,IAAMA,EAAE,MAAQ,EAAE,KAAK,EAC7B,CAAC,GAAG4C,EAAK,GAAGL,EAAQ,MAAMN,CAAC,CAAC,CACrC,CAGA,eAAsBsB,EAAY/E,EAAgB+D,EAAmBjE,EAAiC,CACpG,MAAMgE,EAAcC,EAASjE,CAAI,EACjC,IAAMmD,EAAOC,GAAS,MAAMpD,EAAK,KAAK,GAAG,IAAI,EACvCwE,EAAM,CAACjC,EAAWzC,IACtBA,EAAE,MAAM,KAAMP,GAAMgD,IAAMhD,GAAMO,EAAE,QAAUyC,EAAE,WAAWhD,CAAC,GAAO4D,IAAS,QAAaA,EAAKZ,CAAC,IAAMY,EAAK5D,CAAC,CAAE,IAAMO,EAAE,WAAa,CAAC,GAAG,SAASyC,CAAC,EAChJ,QAAW4B,KAAKF,EACV,CAACE,EAAE,UAAYA,EAAE,WACrBA,EAAE,SAAWA,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,CAAC/E,CAAI,IAAM,CAC5C,IAAMC,EAAQC,EAASF,CAAI,EAC3B,OAAOc,EAAE,OAAO,OAAS,GAAKA,EAAE,OAAO,MAAOJ,GAAMT,EAAM,KAAMkD,GAAMiC,EAAIjC,EAAGzC,CAAC,CAAC,CAAC,CAClF,CAAC,EAAE,MAAM,EAAG,CAAC,EAEjB,CAGA,SAAS+E,EAAUxF,EAAiBjB,EAAiBoG,EAAgD,CACnG,OAAOnF,EAAM,MAAOkD,GAAMnE,EAAO,KAAM0B,GAAM0E,EAAIjC,EAAGzC,CAAC,CAAC,CAAC,GAAK1B,EAAO,MAAO0B,GAAMT,EAAM,KAAMkD,GAAMiC,EAAIjC,EAAGzC,CAAC,CAAC,CAAC,CAC9G,CAGA,eAAsBoF,EAAO/G,EAAW6B,EAAkB2D,EAAI,GAAyD,CACrH,IAAMwB,EAASjH,EAAWC,CAAC,EAErBgF,EAAOC,GAAS,MAAMpD,EAAK,KAAK,GAAG,IAAI,EACzCmD,IAAMgC,EAAO,UAAY,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGA,EAAO,UAAW,GAAGA,EAAO,UAAU,IAAIhC,CAAI,EAAE,OAAQ3B,GAAMA,EAAE,QAAU,CAAC,CAAC,CAAC,CAAC,GAC3H,IAAM4D,EAAS,MAAMrF,GAAMoF,EAAQnF,CAAI,EAEvC,OAAAmF,EAAO,UAAY,CAAC,GAAG,IAAI,IAAI,CAAC,GAAGA,EAAO,UAAW,GAAGA,EAAO,OAAO,QAASrF,GAAMA,EAAE,WAAa,CAAC,CAAC,CAAC,CAAC,CAAC,EAClG,CAAE,OAAAqF,EAAQ,QAAS,MAAMd,GAAOc,EAAQC,EAAQpF,EAAM2D,CAAC,CAAE,CAClE,CAaO,SAAS0B,EAAWC,EAAcC,EAA4CzF,EAA2C,WAAiC,CAC/J,IAAM0F,EAAQ,IAAI,IACZC,EAAuB3F,EAAE,oBAAsB,CAAE,EAAG,CAAC,EAAG,MAAO,CAAC,CAAE,EACxE,OAAW,CAACQ,EAAGK,CAAC,IAAK8E,EAAO,EAAGD,EAAM,IAAIlF,EAAGK,CAAC,EAC7C8E,EAAO,EAAI,CAAC,EACZA,EAAO,KAAO,CAACnF,EAAGK,IAAG,CAAQ6E,EAAM,IAAIlF,EAAGK,CAAC,GAC3Cb,EAAE,mBAAqB2F,EAEvB,IAAMC,EAAM,MAAUpF,EAAWqF,KAC1BH,EAAM,IAAIlF,CAAC,GAAG,MAAMiF,EAAWD,EAAOK,CAAG,EACvCH,EAAM,IAAIlF,CAAC,GAEhBL,EACED,EAAmB,CACvB,KAAM,IACHC,MAASyF,EAAUnC,EAAI,KAAMqC,EAAK,IAAI,EAAE,KAAMjH,GAAM,CACnD,GAAI,CAACA,GAAKA,EAAE,SAAW,EAAQ,MAAM,IAAI,MAAM,uBAAuBA,GAAG,QAAU,SAAS,cAAc,CAAM,oBAAoB,EACpI,OAAOA,CACT,CAAC,GACH,MAAO,MAAO2B,IAAQ,MAAMN,EAAK,KAAK,GAAG,OAAO,SAASM,CAAC,EAAIoF,EAAcnC,EAAI,MAAMjD,CAAC,EAAGsF,EAAK,MAAMtF,CAAC,CAAC,EAAI,OAC3G,SAAWoB,GAAMgE,EAAgBnC,EAAI,SAAS7B,CAAC,EAAGkE,EAAK,SAASlE,CAAC,CAAC,CACpE,EACA,OAAO1B,CACT,CJlbO,SAAS6F,GAAWC,EAA4B,CACrD,OAAO,IAAI,QAAQ,CAACC,EAASC,IAAW,CACtC,IAAMC,EAAI,SAAS,cAAc,QAAQ,EACzCA,EAAE,IAAMH,EACRG,EAAE,OAAS,IAAMF,EAAQ,EACzBE,EAAE,QAAU,IAAMD,EAAO,IAAI,MAAM,kBAAkBF,CAAG,EAAE,CAAC,EAC3D,SAAS,KAAK,OAAOG,CAAC,CACxB,CAAC,CACH","names":["browser_exports","__export","loadDetails","loadScript","parseQuery","scriptData","search","file","key","block","k","step2list","step3list","consonant","vowel","consonants","vowels","gt0","eq1","gt1","vowelInStem","consonantLike","sfxLl","sfxE","sfxY","sfxIon","sfxEdOrIng","sfxAtOrBlOrIz","sfxEED","sfxS","sfxSsesOrIes","sfxMultiConsonantLike","step2","step3","step4","stemmer","value","result","firstCharacterWasLowerCaseY","match","STOP","CJK","segmenter","cjkWords","run","s","chars","out","i","tokenize","text","m","raw","parts","joined","p","shardKey","term","depth","k","ch","stemFor","lang","stemmer","parseQuery","q","groups","not","phrases","strict","filters","notFilters","tokens","m","negateNext","orNext","i","v","facetValue","word","isPhrase","negate","text","terms","tokenize","t","last","whole","compound","joined","j","highlight","g","query","data","meta","p","excluded","set","docs","k","shardsFor","shard","list","has","d","keep","facetFilter","out","title","url","section","perGroup","scoreGroup","combine","mode","total","hits","s","a","b","and","QUERY_RANK","oneEdit","diff","short","long","term","prefix","have","shardSet","MAX_DEPTH","shardKey","x","shardSets","tag","indexes","names","want","sections","tags","notSections","notTags","doc","docTags","stem","stemFor","weights","shards","key","use","w","lead","n","lists","flat","idf","scores","loadFragments","results","blocks","r","loaded","refine","top","words","hit","excerpt","headings","titleTerms","best","sameTerms","id","hay","ph","loadDetails","search","parsed","ranked","scriptData","base","loadScript","store","global","get","src","file","loadScript","src","resolve","reject","s"]}
@@ -0,0 +1,6 @@
1
+ export { BLOCK, Doc, FORMAT, Fragment, Meta, Postings, file, key, wrap, wrapJson } from './format.js';
2
+ export { shardKey, stemFor, tokenize } from './text.js';
3
+ export { RANK, SearchDoc, SearchIndexBuilder, extractText } from './index.js';
4
+ export { HtmlPage, extractHtml } from './html.js';
5
+ export { ParsedQuery, QUERY_RANK, Result, SearchData, loadDetails, loadFragments, memoryData, oneEdit, parseQuery, query, refine, scriptData, search, shardsFor } from './query.js';
6
+ import '@dogsbay/types';
package/dist/entry.js ADDED
@@ -0,0 +1,38 @@
1
+ export * from "./format.js";
2
+ import { tokenize, shardKey, stemFor } from "./text.js";
3
+ import { SearchIndexBuilder, extractText, RANK } from "./index.js";
4
+ import { extractHtml } from "./html.js";
5
+ import {
6
+ parseQuery,
7
+ query,
8
+ refine,
9
+ search,
10
+ loadFragments,
11
+ loadDetails,
12
+ scriptData,
13
+ memoryData,
14
+ shardsFor,
15
+ oneEdit,
16
+ QUERY_RANK
17
+ } from "./query.js";
18
+ export {
19
+ QUERY_RANK,
20
+ RANK,
21
+ SearchIndexBuilder,
22
+ extractHtml,
23
+ extractText,
24
+ loadDetails,
25
+ loadFragments,
26
+ memoryData,
27
+ oneEdit,
28
+ parseQuery,
29
+ query,
30
+ refine,
31
+ scriptData,
32
+ search,
33
+ shardKey,
34
+ shardsFor,
35
+ stemFor,
36
+ tokenize
37
+ };
38
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/entry.ts"],"sourcesContent":["/**\n * @dogsbay/search: a static search index that builds anywhere JavaScript\n * runs (including a Cloudflare Worker) and is queried from classic scripts,\n * so it works from file://. See README.md.\n */\nexport * from \"./format.js\";\nexport { tokenize, shardKey, stemFor } from \"./text.js\";\nexport { SearchIndexBuilder, extractText, RANK, type SearchDoc } from \"./index.js\";\nexport { extractHtml, type HtmlPage } from \"./html.js\";\nexport {\n parseQuery,\n query,\n refine,\n search,\n loadFragments,\n loadDetails,\n scriptData,\n memoryData,\n shardsFor,\n oneEdit,\n QUERY_RANK,\n type ParsedQuery,\n type Result,\n type SearchData,\n} from \"./query.js\";\n"],"mappings":"AAKA,cAAc;AACd,SAAS,UAAU,UAAU,eAAe;AAC5C,SAAS,oBAAoB,aAAa,YAA4B;AACtE,SAAS,mBAAkC;AAC3C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;","names":[]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The search index format: what the indexer writes and the query code
3
+ * reads. Every file is a classic script that calls
4
+ * `window.__DOGSBAY_SEARCH__.load(key, data)`, so it loads from `file://`
5
+ * as well as from a server.
6
+ *
7
+ * docs.js Meta: titles, URLs and sections of every document
8
+ * t-<k>.js Postings for terms whose first `depth` characters are <k>
9
+ * f-<n>.js Fragments of documents n·block … n·block+block−1:
10
+ * excerpt and headings, loaded only for results being shown
11
+ */
12
+ /** Version 4: tags for filters (3 split fragments out of docs.js, 2 added field-weighted scores). */
13
+ declare const FORMAT = 4;
14
+ /** Documents per fragment file. */
15
+ declare const BLOCK = 32;
16
+ /** Section and tags are indexes into Meta.sections and Meta.tags; tags are left out when there are none. */
17
+ type Doc = [title: string, url: string, section: number, tags?: number[]];
18
+ interface Meta {
19
+ format: number;
20
+ /** Characters of a term that pick its shard: 1, or 2 for large units. */
21
+ depth: number;
22
+ count: number;
23
+ shards: string[];
24
+ sections: string[];
25
+ tags: string[];
26
+ block: number;
27
+ /** The unit's language, which picks the query-time stemmer. */
28
+ lang?: string;
29
+ docs: Doc[];
30
+ }
31
+ /** term → [doc, score, doc, score, …], highest score first. */
32
+ type Postings = Record<string, number[]>;
33
+ /** A document's excerpt and its h2/h3 headings with their anchors. */
34
+ type Fragment = [excerpt: string, headings: [text: string, id: string][]];
35
+ declare const file: {
36
+ docs: string;
37
+ shard: (key: string) => string;
38
+ fragment: (block: number) => string;
39
+ };
40
+ /** Keys passed to load(); fragment keys contain ":" so they can never equal a shard key ([a-z0-9_]). */
41
+ declare const key: {
42
+ docs: string;
43
+ shard: (k: string) => string;
44
+ fragment: (block: number) => string;
45
+ };
46
+ /** One data file as a classic script. "</" is escaped so a term like "</script>" cannot end the tag. */
47
+ declare function wrap(k: string, data: unknown): string;
48
+ /** As wrap(), for data already serialised (fragments are kept as JSON strings while building). */
49
+ declare function wrapJson(k: string, json: string): string;
50
+
51
+ export { BLOCK, type Doc, FORMAT, type Fragment, type Meta, type Postings, file, key, wrap, wrapJson };
package/dist/format.js ADDED
@@ -0,0 +1,29 @@
1
+ const FORMAT = 4;
2
+ const BLOCK = 32;
3
+ const file = {
4
+ docs: "docs.js",
5
+ shard: (key2) => `t-${key2}.js`,
6
+ fragment: (block) => `f-${block}.js`
7
+ };
8
+ const key = {
9
+ docs: "docs",
10
+ shard: (k) => k,
11
+ fragment: (block) => `frag:${block}`
12
+ };
13
+ function wrap(k, data) {
14
+ const json = JSON.stringify(data).replace(/<\//g, "<\\/");
15
+ return wrapJson(k, json);
16
+ }
17
+ function wrapJson(k, json) {
18
+ return `(window.__DOGSBAY_SEARCH__=window.__DOGSBAY_SEARCH__||{q:[],load:function(k,d){this.q.push([k,d])}}).load(${JSON.stringify(k)},${json});
19
+ `;
20
+ }
21
+ export {
22
+ BLOCK,
23
+ FORMAT,
24
+ file,
25
+ key,
26
+ wrap,
27
+ wrapJson
28
+ };
29
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/format.ts"],"sourcesContent":["/**\n * The search index format: what the indexer writes and the query code\n * reads. Every file is a classic script that calls\n * `window.__DOGSBAY_SEARCH__.load(key, data)`, so it loads from `file://`\n * as well as from a server.\n *\n * docs.js Meta: titles, URLs and sections of every document\n * t-<k>.js Postings for terms whose first `depth` characters are <k>\n * f-<n>.js Fragments of documents n·block … n·block+block−1:\n * excerpt and headings, loaded only for results being shown\n */\n\n/** Version 4: tags for filters (3 split fragments out of docs.js, 2 added field-weighted scores). */\nexport const FORMAT = 4;\n\n/** Documents per fragment file. */\nexport const BLOCK = 32;\n\n/** Section and tags are indexes into Meta.sections and Meta.tags; tags are left out when there are none. */\nexport type Doc = [title: string, url: string, section: number, tags?: number[]];\n\nexport interface Meta {\n format: number;\n /** Characters of a term that pick its shard: 1, or 2 for large units. */\n depth: number;\n count: number;\n shards: string[];\n sections: string[];\n tags: string[];\n block: number;\n /** The unit's language, which picks the query-time stemmer. */\n lang?: string;\n docs: Doc[];\n}\n\n/** term → [doc, score, doc, score, …], highest score first. */\nexport type Postings = Record<string, number[]>;\n\n/** A document's excerpt and its h2/h3 headings with their anchors. */\nexport type Fragment = [excerpt: string, headings: [text: string, id: string][]];\n\nexport const file = {\n docs: \"docs.js\",\n shard: (key: string) => `t-${key}.js`,\n fragment: (block: number) => `f-${block}.js`,\n};\n\n/** Keys passed to load(); fragment keys contain \":\" so they can never equal a shard key ([a-z0-9_]). */\nexport const key = {\n docs: \"docs\",\n shard: (k: string) => k,\n fragment: (block: number) => `frag:${block}`,\n};\n\n/** One data file as a classic script. \"</\" is escaped so a term like \"</script>\" cannot end the tag. */\nexport function wrap(k: string, data: unknown): string {\n const json = JSON.stringify(data).replace(/<\\//g, \"<\\\\/\");\n return wrapJson(k, json);\n}\n\n/** As wrap(), for data already serialised (fragments are kept as JSON strings while building). */\nexport function wrapJson(k: string, json: string): string {\n return `(window.__DOGSBAY_SEARCH__=window.__DOGSBAY_SEARCH__||{q:[],load:function(k,d){this.q.push([k,d])}}).load(${JSON.stringify(k)},${json});\\n`;\n}\n"],"mappings":"AAaO,MAAM,SAAS;AAGf,MAAM,QAAQ;AAyBd,MAAM,OAAO;AAAA,EAClB,MAAM;AAAA,EACN,OAAO,CAACA,SAAgB,KAAKA,IAAG;AAAA,EAChC,UAAU,CAAC,UAAkB,KAAK,KAAK;AACzC;AAGO,MAAM,MAAM;AAAA,EACjB,MAAM;AAAA,EACN,OAAO,CAAC,MAAc;AAAA,EACtB,UAAU,CAAC,UAAkB,QAAQ,KAAK;AAC5C;AAGO,SAAS,KAAK,GAAW,MAAuB;AACrD,QAAM,OAAO,KAAK,UAAU,IAAI,EAAE,QAAQ,QAAQ,MAAM;AACxD,SAAO,SAAS,GAAG,IAAI;AACzB;AAGO,SAAS,SAAS,GAAW,MAAsB;AACxD,SAAO,6GAA6G,KAAK,UAAU,CAAC,CAAC,IAAI,IAAI;AAAA;AAC/I;","names":["key"]}
package/dist/html.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { SearchDoc } from './index.js';
2
+ import '@dogsbay/types';
3
+
4
+ /**
5
+ * Index built HTML, from any site generator: extract a page's title,
6
+ * headings, body text, keywords and tags from its markup, for
7
+ * SearchIndexBuilder.add(). A small tokenizer, not a DOM, so it runs in a
8
+ * Worker and on large sites without a parser dependency.
9
+ *
10
+ * What is indexed:
11
+ * root the element with a `data-search-body` attribute (or Pagefind's
12
+ * `data-pagefind-body`), else the first <main>, else <article>,
13
+ * else <body>; never one inside a skipped element
14
+ * skipped script, style, template, noscript, svg, nav, aside, form,
15
+ * button, select, textarea, pre (code blocks), footer, header
16
+ * outside the root, aria-hidden content, and anything with
17
+ * `data-search-ignore` or `data-pagefind-ignore`
18
+ * title the first <h1> in the root, else <title>; later h1s are headings
19
+ * headings h2–h6 (and later h1s) in the root; h2/h3 with an id are anchors
20
+ * keywords <meta name="keywords">, and tag lists (`data-tag-list`)
21
+ * A page with a meta refresh or a robots `noindex`, or with neither a
22
+ * title nor body text, is not indexed.
23
+ */
24
+
25
+ interface HtmlPage {
26
+ doc: SearchDoc;
27
+ parts: {
28
+ title: string;
29
+ headings: string[];
30
+ body: string;
31
+ keywords: string[];
32
+ };
33
+ lang?: string;
34
+ }
35
+ /**
36
+ * Extract one page. `url` is the page's URL relative to the site (what
37
+ * results link to); `section` defaults to its first folder.
38
+ */
39
+ declare function extractHtml(html: string, url: string, opts?: {
40
+ section?: string;
41
+ }): HtmlPage | undefined;
42
+ /** "guide/install.html" → "Guide"; top-level pages have no section. Works on encoded and raw paths. */
43
+ declare function folderName(url: string): string;
44
+
45
+ export { type HtmlPage, extractHtml, folderName };