@dogsbay/search 0.2.0-beta.122 → 0.2.0-beta.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -1
- package/dist/detach.js +0 -1
- package/dist/dogsbay-search.iife.js +0 -1
- package/dist/entry.js +0 -1
- package/dist/format.js +0 -1
- package/dist/html.js +0 -1
- package/dist/index.js +0 -1
- package/dist/query.js +0 -1
- package/dist/site.js +0 -1
- package/dist/text.js +0 -1
- package/package.json +3 -3
- package/dist/cli.js.map +0 -1
- package/dist/detach.js.map +0 -1
- package/dist/dogsbay-search.iife.js.map +0 -1
- package/dist/entry.js.map +0 -1
- package/dist/format.js.map +0 -1
- package/dist/html.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/query.js.map +0 -1
- package/dist/site.js.map +0 -1
- package/dist/text.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -21,4 +21,3 @@ if (!dir || args.length > 1 || !isDir(dir)) usage(1);
|
|
|
21
21
|
const t0 = performance.now();
|
|
22
22
|
const r = indexSite(dir, { out, lang });
|
|
23
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
|
package/dist/detach.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
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
|
package/dist/entry.js
CHANGED
package/dist/format.js
CHANGED
package/dist/html.js
CHANGED
package/dist/index.js
CHANGED
package/dist/query.js
CHANGED
package/dist/site.js
CHANGED
package/dist/text.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/search",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.124",
|
|
4
4
|
"description": "Static search for docs sites: builds in a Worker, queries from file://",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/entry.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"stemmer": "^2.0.1",
|
|
45
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
45
|
+
"@dogsbay/types": "0.2.0-beta.124"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@playwright/test": "1.61.1",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
53
|
"typescript": "^5.9.3",
|
|
54
54
|
"vitest": "^4.1.10",
|
|
55
|
-
"@dogsbay/format-openapi": "0.2.0-beta.
|
|
55
|
+
"@dogsbay/format-openapi": "0.2.0-beta.124"
|
|
56
56
|
},
|
|
57
57
|
"license": "MIT",
|
|
58
58
|
"repository": {
|
package/dist/cli.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|
package/dist/detach.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|
|
@@ -1 +0,0 @@
|
|
|
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":"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","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"]}
|
package/dist/entry.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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":[]}
|
package/dist/format.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/html.ts"],"sourcesContent":["/**\n * Index built HTML, from any site generator: extract a page's title,\n * headings, body text, keywords and tags from its markup, for\n * SearchIndexBuilder.add(). A small tokenizer, not a DOM, so it runs in a\n * Worker and on large sites without a parser dependency.\n *\n * What is indexed:\n * root the element with a `data-search-body` attribute (or Pagefind's\n * `data-pagefind-body`), else the first <main>, else <article>,\n * else <body>; never one inside a skipped element\n * skipped script, style, template, noscript, svg, nav, aside, form,\n * button, select, textarea, pre (code blocks), footer, header\n * outside the root, aria-hidden content, and anything with\n * `data-search-ignore` or `data-pagefind-ignore`\n * title the first <h1> in the root, else <title>; later h1s are headings\n * headings h2–h6 (and later h1s) in the root; h2/h3 with an id are anchors\n * keywords <meta name=\"keywords\">, and tag lists (`data-tag-list`)\n * A page with a meta refresh or a robots `noindex`, or with neither a\n * title nor body text, is not indexed.\n */\nimport type { SearchDoc } from \"./index.js\";\n\nexport interface HtmlPage {\n doc: SearchDoc;\n parts: { title: string; headings: string[]; body: string; keywords: string[] };\n lang?: string;\n}\n\ntype Attrs = Map<string, string>;\ntype Token = { kind: \"open\"; name: string; attrs: Attrs; selfClosing: boolean } | { kind: \"close\"; name: string } | { kind: \"text\"; text: string };\n\nconst VOID = new Set([\"area\", \"base\", \"br\", \"col\", \"embed\", \"hr\", \"img\", \"input\", \"link\", \"meta\", \"param\", \"source\", \"track\", \"wbr\"]);\nconst SKIP = new Set([\"script\", \"style\", \"noscript\", \"template\", \"svg\", \"nav\", \"aside\", \"form\", \"button\", \"select\", \"pre\", \"textarea\", \"footer\"]);\nconst BLOCK = /^(p|div|li|ul|ol|dl|dt|dd|h[1-6]|tr|td|th|table|section|article|main|header|blockquote|figure|figcaption|details|summary|br|hr)$/;\n/** Elements that close an open <p> (the HTML parser's implied end tag). */\nconst CLOSES_P = /^(p|div|ul|ol|dl|h[1-6]|table|section|article|main|header|footer|nav|aside|blockquote|figure|details|pre|hr|form)$/;\n\n/**\n * Tokens of a document: tags with parsed attributes, and text. Comments,\n * CDATA, doctypes and the contents of script, style and template are left\n * out here, so nothing inside them is ever mistaken for markup.\n */\nfunction* tokens(html: string): Generator<Token> {\n const re = /<!--[\\s\\S]*?(?:-->|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>|$)|<![^>]*>|<\\?[^>]*>|<(script|style|template|textarea)\\b((?:[^>\"']|\"[^\"]*\"|'[^']*')*)>([\\s\\S]*?)(?:<\\/\\1\\s*>|$)|<(\\/?)([a-zA-Z][\\w:.-]*)((?:[^>\"']|\"[^\"]*\"|'[^']*')*)>|([^<]+)|</gi;\n for (const m of html.matchAll(re)) {\n const [whole, rawName, rawAttrs, , slash, name, attrs, text] = m;\n if (text !== undefined) yield { kind: \"text\", text };\n else if (whole === \"<\") yield { kind: \"text\", text: \"<\" };\n else if (rawName) {\n // Kept as an element with no content, so skip rules and structure still apply.\n const n = rawName.toLowerCase();\n yield { kind: \"open\", name: n, attrs: parseAttrs(rawAttrs ?? \"\"), selfClosing: false };\n yield { kind: \"close\", name: n };\n } else if (name) {\n const n = name.toLowerCase();\n if (slash) yield { kind: \"close\", name: n };\n else yield { kind: \"open\", name: n, attrs: parseAttrs(attrs ?? \"\"), selfClosing: /(^|\\s|[\"'])\\/$/.test((attrs ?? \"\").trim()) };\n }\n }\n}\n\nfunction parseAttrs(s: string): Attrs {\n const out: Attrs = new Map();\n for (const m of s.matchAll(/([^\\s\"'>\\/=]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'>]+)))?/g)) {\n const name = m[1].toLowerCase();\n if (!out.has(name)) out.set(name, decode(m[2] ?? m[3] ?? m[4] ?? \"\"));\n }\n return out;\n}\n\ninterface Open {\n name: string;\n skip: boolean;\n root: boolean;\n heading?: { level: number; id?: string; text: string[] };\n /** A heading's permalink (<a href=\"#…\">): its text is dropped when it is only a mark. */\n permalink?: { heading: string[]; from: number };\n tagList?: boolean;\n tagItem?: string[];\n}\n\n/**\n * Extract one page. `url` is the page's URL relative to the site (what\n * results link to); `section` defaults to its first folder.\n */\nexport function extractHtml(html: string, url: string, opts: { section?: string } = {}): HtmlPage | undefined {\n const all = [...tokens(html)];\n\n // Head: <title>, <meta>, <html lang>. Everything before <body>, by token, not by text search.\n let lang: string | undefined;\n let titleTag = \"\";\n const metas = new Map<string, string>();\n let refresh = false;\n let inTitle = false;\n let bodyAt = all.length;\n for (let i = 0; i < all.length; i++) {\n const t = all[i];\n if (t.kind === \"open\" && t.name === \"body\") {\n bodyAt = i;\n break;\n }\n if (t.kind === \"open\" && t.name === \"html\") lang = t.attrs.get(\"lang\") || undefined;\n else if (t.kind === \"open\" && t.name === \"title\") inTitle = true;\n else if (t.kind === \"close\" && t.name === \"title\") inTitle = false;\n else if (t.kind === \"text\" && inTitle) titleTag += t.text;\n else if (t.kind === \"open\" && t.name === \"meta\") {\n if ((t.attrs.get(\"http-equiv\") ?? \"\").toLowerCase() === \"refresh\") refresh = true;\n const name = (t.attrs.get(\"name\") ?? \"\").toLowerCase();\n if (name) metas.set(name, t.attrs.get(\"content\") ?? \"\");\n }\n }\n if (refresh || /\\bnoindex\\b/i.test(metas.get(\"robots\") ?? \"\")) return undefined;\n\n // The root: the marked element, else the first <main>, <article> or <body> that is not inside a skipped element.\n const marker = all.some((t) => t.kind === \"open\" && t.attrs.has(\"data-search-body\"))\n ? \"data-search-body\"\n : all.some((t) => t.kind === \"open\" && t.attrs.has(\"data-pagefind-body\"))\n ? \"data-pagefind-body\"\n : undefined;\n const has = (n: string) => all.some((t) => t.kind === \"open\" && t.name === n);\n const rootTag = marker ? undefined : has(\"main\") ? \"main\" : has(\"article\") ? \"article\" : \"body\";\n\n const stack: Open[] = [];\n let skipDepth = 0; // open skipped elements\n let rootDepth = 0; // open root elements (0 or 1)\n let rootSeen = false;\n let heading: Open[\"heading\"];\n let tagItem: string[] | undefined;\n let tagListDepth = 0;\n const body: string[] = [];\n const headings: string[] = [];\n const anchors: [string, string][] = [];\n const tags: string[] = [];\n let h1: string | undefined;\n\n const push = (o: Open) => {\n stack.push(o);\n if (o.skip) skipDepth++;\n if (o.root) rootDepth++;\n if (o.heading) heading = o.heading;\n if (o.tagList) tagListDepth++;\n if (o.tagItem) tagItem = o.tagItem;\n };\n const pop = () => {\n const o = stack.pop()!;\n if (o.skip) skipDepth--;\n if (o.root) rootDepth--;\n if (o.tagList) tagListDepth--;\n if (BLOCK.test(o.name)) (heading ? heading.text : body).push(\" \");\n if (o.permalink && /^\\s*[#¶🔗§]?\\s*$/u.test(o.permalink.heading.slice(o.permalink.from).join(\"\"))) o.permalink.heading.length = o.permalink.from;\n if (o.tagItem) {\n const t = squash(o.tagItem.join(\"\")).replace(/^#\\s*/, \"\");\n if (t) tags.push(t);\n tagItem = undefined;\n }\n if (o.heading) {\n heading = undefined;\n // A trailing ¶ or 🔗 is a permalink mark left as text; a trailing # is kept (it ends \"C#\").\n const text = squash(o.heading.text.join(\"\")).replace(/\\s*[¶🔗]$/u, \"\");\n if (!text) return;\n if (o.heading.level === 1 && h1 === undefined) h1 = text;\n else {\n headings.push(text);\n if (o.heading.level <= 3 && o.heading.id) anchors.push([text, o.heading.id]);\n }\n }\n };\n /** Close open elements down to and including the last one named `name`, if it is open (within `limit` elements). */\n const closeTo = (names: RegExp, limit = Infinity) => {\n for (let i = stack.length - 1, n = 0; i >= 0 && n < limit; i--, n++) {\n if (names.test(stack[i].name)) {\n while (stack.length > i) pop();\n return;\n }\n if (/^(ul|ol|dl|table|body|main|article|section|div)$/.test(stack[i].name)) return; // not across a container\n }\n };\n\n for (let i = bodyAt; i < all.length; i++) {\n const t = all[i];\n if (t.kind === \"text\") {\n if (!rootDepth || skipDepth) continue;\n const text = decode(t.text);\n (heading ? heading.text : body).push(text);\n tagItem?.push(text);\n continue;\n }\n if (t.kind === \"close\") {\n let j = stack.length - 1;\n while (j >= 0 && stack[j].name !== t.name) j--;\n if (j >= 0) while (stack.length > j) pop();\n continue;\n }\n const { name, attrs } = t;\n // Implied end tags, as the HTML parser applies them, so unclosed <p> and <li> do not nest without limit.\n if (CLOSES_P.test(name)) closeTo(/^p$/, 64);\n if (name === \"li\") closeTo(/^li$/);\n else if (name === \"dt\" || name === \"dd\") closeTo(/^d[td]$/);\n else if (name === \"tr\") closeTo(/^tr$/);\n else if (name === \"td\" || name === \"th\") closeTo(/^t[dh]$/);\n else if (name === \"option\") closeTo(/^option$/);\n\n if (name === \"img\" && rootDepth && !skipDepth) {\n const alt = attrs.get(\"alt\");\n if (alt) (heading ? heading.text : body).push(` ${alt} `);\n }\n if (VOID.has(name) || t.selfClosing) {\n if ((name === \"br\" || name === \"hr\") && rootDepth) (heading ? heading.text : body).push(\" \");\n continue;\n }\n const ignore = attrs.has(\"data-search-ignore\") || attrs.has(\"data-pagefind-ignore\") || attrs.get(\"aria-hidden\") === \"true\";\n const isRoot = !rootSeen && !skipDepth && !ignore && (marker ? attrs.has(marker) : name === rootTag);\n if (isRoot) rootSeen = true;\n // A <header> inside the root is the page's own header (it often holds the <h1>); outside, it is the site's.\n const skip = (SKIP.has(name) || (name === \"header\" && !rootDepth && !isRoot) || ignore) && !isRoot;\n const o: Open = { name, root: isRoot, skip };\n const level = /^h([1-6])$/.exec(name)?.[1];\n if (level && !heading) o.heading = { level: Number(level), id: attrs.get(\"id\") || undefined, text: [] };\n if (name === \"a\" && heading && (attrs.get(\"href\") ?? \"\").startsWith(\"#\")) o.permalink = { heading: heading.text, from: heading.text.length };\n if (attrs.has(\"data-tag-list\")) o.tagList = true;\n if (name === \"li\" && tagListDepth) o.tagItem = [];\n if (BLOCK.test(name)) (heading ? heading.text : body).push(\" \");\n push(o);\n }\n while (stack.length) pop();\n\n const bodyText = squash(body.join(\"\"));\n const title = h1 ?? squash(decode(titleTag));\n if (!bodyText && !title) return undefined;\n const keywords = (metas.get(\"keywords\") ?? \"\").split(\",\").map((k) => k.trim()).filter(Boolean);\n return {\n doc: { title: title || url, url, section: opts.section ?? folderName(url), excerpt: excerpt(metas.get(\"description\") || bodyText), anchors, tags },\n parts: { title: title || url, headings, body: bodyText, keywords: [...keywords, ...tags] },\n lang,\n };\n}\n\nfunction squash(s: string): string {\n return s.replace(/\\s+/g, \" \").trim();\n}\n\nfunction excerpt(s: string): string {\n if (s.length <= 240) return s;\n const cut = s.slice(0, 240);\n return cut.slice(0, cut.lastIndexOf(\" \") > 180 ? cut.lastIndexOf(\" \") : 240) + \"…\";\n}\n\n/** \"guide/install.html\" → \"Guide\"; top-level pages have no section. Works on encoded and raw paths. */\nexport function folderName(url: string): string {\n const parts = url.split(\"/\");\n if (parts.length < 2) return \"\";\n let f = parts[0];\n try {\n f = decodeURIComponent(f);\n } catch {\n /* a raw name with \"%\" */\n }\n f = f.replace(/[-_]+/g, \" \");\n return f.charAt(0).toUpperCase() + f.slice(1);\n}\n\n// Named entities: all of Latin-1 plus the common typographic and symbol ones. Numeric references are always decoded.\nconst NAMED: Record<string, number> = {\n quot: 34, amp: 38, apos: 39, lt: 60, gt: 62, nbsp: 160, iexcl: 161, cent: 162, pound: 163, curren: 164, yen: 165, brvbar: 166, sect: 167, uml: 168, copy: 169,\n ordf: 170, laquo: 171, not: 172, shy: 173, reg: 174, macr: 175, deg: 176, plusmn: 177, sup2: 178, sup3: 179, acute: 180, micro: 181, para: 182, middot: 183,\n cedil: 184, sup1: 185, ordm: 186, raquo: 187, frac14: 188, frac12: 189, frac34: 190, iquest: 191, Agrave: 192, Aacute: 193, Acirc: 194, Atilde: 195, Auml: 196,\n Aring: 197, AElig: 198, Ccedil: 199, Egrave: 200, Eacute: 201, Ecirc: 202, Euml: 203, Igrave: 204, Iacute: 205, Icirc: 206, Iuml: 207, ETH: 208, Ntilde: 209,\n Ograve: 210, Oacute: 211, Ocirc: 212, Otilde: 213, Ouml: 214, times: 215, Oslash: 216, Ugrave: 217, Uacute: 218, Ucirc: 219, Uuml: 220, Yacute: 221, THORN: 222,\n szlig: 223, agrave: 224, aacute: 225, acirc: 226, atilde: 227, auml: 228, aring: 229, aelig: 230, ccedil: 231, egrave: 232, eacute: 233, ecirc: 234, euml: 235,\n igrave: 236, iacute: 237, icirc: 238, iuml: 239, eth: 240, ntilde: 241, ograve: 242, oacute: 243, ocirc: 244, otilde: 245, ouml: 246, divide: 247, oslash: 248,\n ugrave: 249, uacute: 250, ucirc: 251, uuml: 252, yacute: 253, thorn: 254, yuml: 255, OElig: 338, oelig: 339, Scaron: 352, scaron: 353, Yuml: 376, fnof: 402,\n circ: 710, tilde: 732, ensp: 8194, emsp: 8195, thinsp: 8201, zwnj: 8204, zwj: 8205, lrm: 8206, rlm: 8207, ndash: 8211, mdash: 8212, lsquo: 8216, rsquo: 8217,\n sbquo: 8218, ldquo: 8220, rdquo: 8221, bdquo: 8222, dagger: 8224, Dagger: 8225, bull: 8226, hellip: 8230, permil: 8240, prime: 8242, Prime: 8243, lsaquo: 8249,\n rsaquo: 8250, euro: 8364, trade: 8482, larr: 8592, uarr: 8593, rarr: 8594, darr: 8595, harr: 8596, rArr: 8658, lArr: 8656, hArr: 8660, minus: 8722, le: 8804,\n ge: 8805, ne: 8800, asymp: 8776, infin: 8734, check: 10003, hearts: 9829,\n};\n\nfunction decode(s: string): string {\n if (!s.includes(\"&\")) return s;\n return s.replace(/&(#x[0-9a-f]+|#\\d+|[a-z][a-z0-9]*);/gi, (m, e: string) => {\n if (e[0] === \"#\") {\n const n = e[1] === \"x\" || e[1] === \"X\" ? parseInt(e.slice(2), 16) : Number(e.slice(1));\n return Number.isFinite(n) && n > 0 && n < 0x110000 ? String.fromCodePoint(n) : m;\n }\n const n = NAMED[e] ?? NAMED[e.toLowerCase()];\n return n === undefined ? m : String.fromCodePoint(n);\n });\n}\n"],"mappings":"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","names":["n"]}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * The indexer: documents in, index files out (see format.ts). Pure and\n * Worker-safe: no Node built-ins, and memory for one page at a time plus\n * the postings.\n */\nimport type { InlineNode, TreeNode } from \"@dogsbay/types\";\nimport { detach } from \"./detach.js\";\nimport { BLOCK, FORMAT, file, key, wrap, wrapJson, type Doc, type Meta } from \"./format.js\";\nimport { shardKey, tokenize } from \"./text.js\";\n\nexport interface SearchDoc {\n title: string;\n url: string;\n section: string;\n excerpt: string;\n /** h2/h3 headings with their anchors, for results within the page. */\n anchors?: [text: string, id: string][];\n /** Tags, for filters (`tag:x`). */\n tags?: string[];\n}\n\ninterface StoredDoc {\n title: string;\n url: string;\n section: string;\n tags: string[];\n /** The document's Fragment, already serialised: one flat string per page instead of an object graph. */\n fragment: string;\n}\n\n/**\n * Ranking: each field is scored on its own and the fields are combined\n * afterwards, BM25-style: term frequency saturates (k1), and a field's\n * frequency is divided by its length relative to the corpus average (b),\n * so a long page that mentions a word fifty times does not outrank a page\n * with the word in its title. The inverse document frequency is applied at\n * query time from the length of the term's posting list.\n */\nexport const RANK = {\n k1: 1.2,\n fields: {\n title: { weight: 6, b: 0.3 },\n keywords: { weight: 4, b: 0 },\n headings: { weight: 2, b: 0.5 },\n body: { weight: 1, b: 0.75 },\n },\n};\n\n// Per-field term counts share one number per posting while the index is\n// built (the same memory as a single score): body in the low 12 bits, then\n// headings (4 bits), keywords (2) and title (2), each capped.\nconst B_HEAD = 4096;\nconst B_KW = 65536;\nconst B_TITLE = 262144;\n\n/** Estimated characters of a shard file above which it is split (about 12 KB gzipped). */\nconst SHARD_CHARS = 40_000;\nconst MAX_DEPTH = 3;\n\n/** Characters of a term a shard key stands for (\"_65e5_\" is one). */\nfunction keyDepth(k: string): number {\n return k.replace(/_[0-9a-f]+_/g, \"x\").length;\n}\n\nexport class SearchIndexBuilder {\n constructor(private opts: { lang?: string } = {}) {}\n private docs: StoredDoc[] = [];\n /** Token counts per document and field (title, headings, body), for length normalisation. */\n private lengths: number[] = [];\n /**\n * term → flat [doc, counts, doc, counts, …]. Documents arrive in order, so\n * each page's counts are summed locally and appended once. Flat arrays\n * cost a fraction of a Map per term, which matters inside a Worker.\n */\n private postings = new Map<string, number[]>();\n\n add(doc: SearchDoc, parts: { title: string; headings: string[]; body: string; keywords?: string[] }): void {\n const id = this.docs.length;\n // Kept until the index is written: copies, so a title or excerpt does not pin the page's text\n // (JSON.stringify builds a new flat string).\n this.docs.push({\n title: detach(doc.title),\n url: detach(doc.url),\n section: detach(doc.section),\n tags: (doc.tags ?? []).map(detach),\n fragment: JSON.stringify([doc.excerpt, doc.anchors ?? []]).replace(/<\\//g, \"<\\\\/\"),\n });\n // Counts go straight into the packed number (no per-term arrays: this runs\n // for every term of every page, inside a 128 MB Worker).\n const counts = new Map<string, number>();\n const len = [0, 0, 0];\n const count = (text: string, unit: number, cap: number, lenIndex: number) => {\n const terms = tokenize(text);\n if (lenIndex >= 0) len[lenIndex] += terms.length;\n for (const t of terms) {\n const v = counts.get(t) ?? 0;\n if (Math.floor(v / unit) % (cap + 1) < cap) counts.set(t, v + unit);\n else if (!counts.has(t)) counts.set(t, v);\n }\n };\n count(parts.title, B_TITLE, 3, 0);\n for (const k of parts.keywords ?? []) count(k, B_KW, 3, -1);\n for (const h of parts.headings) count(h, B_HEAD, 15, 1);\n count(parts.body, 1, 4095, 2);\n this.lengths.push(len[0], len[1], len[2]);\n for (const [t, v] of counts) {\n const list = this.postings.get(t);\n if (list) list.push(id, v);\n // Copy new terms into their own string: a regex match is a slice that\n // would otherwise keep the page's whole text alive for the entire build.\n else this.postings.set(detach(t), [id, v]);\n }\n }\n\n /** The stored score of one posting: saturated, length-normalised field scores, weighted and summed. */\n private scorer(): (doc: number, v: number) => number {\n const n = this.docs.length || 1;\n const avg = [0, 0, 0];\n for (let i = 0; i < this.lengths.length; i++) avg[i % 3] += this.lengths[i];\n for (let f = 0; f < 3; f++) avg[f] = avg[f] / n || 1;\n const { k1, fields: F } = RANK;\n const sat = (tf: number, len: number, avgLen: number, b: number) => {\n if (!tf) return 0;\n const x = tf / (1 - b + (b * len) / avgLen);\n return (x * (k1 + 1)) / (k1 + x); // 1 for a single occurrence in an average-length field\n };\n return (doc, v) => {\n const body = v % B_HEAD;\n const head = Math.floor(v / B_HEAD) % 16;\n const kw = Math.floor(v / B_KW) % 4;\n const title = Math.floor(v / B_TITLE);\n const L = this.lengths;\n const s =\n F.title.weight * sat(title, L[doc * 3], avg[0], F.title.b) +\n F.keywords.weight * sat(kw, 1, 1, F.keywords.b) +\n F.headings.weight * sat(head, L[doc * 3 + 1], avg[1], F.headings.b) +\n F.body.weight * sat(body, L[doc * 3 + 2], avg[2], F.body.b);\n return Math.max(1, Math.round(10 * s));\n };\n }\n\n get size(): number {\n return this.docs.length;\n }\n\n /**\n * Terms grouped into shards: by first character, and a group larger than\n * SHARD_CHARS split on the next character (up to MAX_DEPTH), so a query\n * loads small files on large sites. Terms no longer than a group's key stay\n * in the group's own file.\n */\n private shardTerms(): Map<string, string[]> {\n const out = new Map<string, string[]>();\n const size = (terms: string[]) => terms.reduce((n, t) => n + t.length + 4 + this.postings.get(t)!.length * 4, 0);\n const place = (terms: string[], depth: number) => {\n const groups = new Map<string, string[]>();\n for (const t of terms) {\n const k = shardKey(t, depth);\n const g = groups.get(k);\n if (g) g.push(t);\n else groups.set(k, [t]);\n }\n for (const [k, g] of groups) {\n if (depth < MAX_DEPTH && size(g) > SHARD_CHARS) {\n const short = g.filter((t) => t.length <= depth);\n if (short.length) out.set(k, short);\n place(g.filter((t) => t.length > depth), depth + 1);\n } else out.set(k, g);\n }\n };\n place([...this.postings.keys()], 1);\n return out;\n }\n\n /** Emit the index files, keyed by path relative to the unit's `_dogsbay/search/` dir. */\n emit(): Map<string, string> {\n return new Map(this.emitFiles());\n }\n\n /**\n * The index files one at a time: the document table, the fragments, then\n * one shard per term prefix. Each shard is built, serialised and handed\n * over before the next, and its postings are dropped, so writing the index\n * needs memory for one shard rather than for every shard and its JSON.\n */\n *emitFiles(): Generator<[string, string]> {\n const byShard = this.shardTerms();\n const depth = Math.max(1, ...[...byShard.keys()].map((k) => keyDepth(k)));\n const score = this.scorer();\n const sections: string[] = [];\n const sectionIndex = new Map<string, number>();\n const tags: string[] = [];\n const tagIndex = new Map<string, number>();\n const docs: Doc[] = this.docs.map((d) => {\n let i = sectionIndex.get(d.section);\n if (i === undefined) sectionIndex.set(d.section, (i = sections.push(d.section) - 1));\n if (!d.tags.length) return [d.title, d.url, i];\n const t = d.tags.map((tag) => {\n let j = tagIndex.get(tag);\n if (j === undefined) tagIndex.set(tag, (j = tags.push(tag) - 1));\n return j;\n });\n return [d.title, d.url, i, t];\n });\n const meta: Meta = { format: FORMAT, depth, count: this.docs.length, shards: [...byShard.keys()].sort(), sections, tags, block: BLOCK, lang: this.opts.lang, docs };\n yield [file.docs, wrap(key.docs, meta)];\n for (let b = 0; b * BLOCK < this.docs.length; b++) {\n const part = this.docs.slice(b * BLOCK, (b + 1) * BLOCK);\n yield [file.fragment(b), wrapJson(key.fragment(b), `[${part.map((d) => d.fragment).join(\",\")}]`)];\n }\n for (const [k, terms] of byShard) {\n const data: Record<string, number[]> = {};\n for (const term of terms) {\n const flat = this.postings.get(term)!;\n // Highest score first, as small integers (ten times the field-weighted score).\n const pairs: [number, number][] = [];\n for (let i = 0; i < flat.length; i += 2) pairs.push([flat[i], score(flat[i], flat[i + 1])]);\n pairs.sort((a, b) => b[1] - a[1]);\n const out: number[] = [];\n for (const [d, sc] of pairs) out.push(d, sc);\n data[term] = out;\n this.postings.delete(term);\n }\n yield [file.shard(k), wrap(key.shard(k), data)];\n }\n }\n}\n\ntype Json = Record<string, unknown>;\nconst str = (v: unknown): string => (typeof v === \"string\" ? v : \"\");\nconst objects = (v: unknown): Json[] => (Array.isArray(v) ? v.filter((x): x is Json => !!x && typeof x === \"object\") : []);\n\nconst INLINE_TAGS = new Set(\"a abbr b bdi bdo cite code data del dfn em i ins kbd mark q s samp small span strong sub sup time u var wbr\".split(\" \"));\nconst ENTITY: Record<string, string> = { lt: \"<\", gt: \">\", quot: '\"', \"#39\": \"'\", \"#x27\": \"'\", apos: \"'\", nbsp: \" \", amp: \"&\" };\nconst decode = (s: string): string => s.replace(/&(lt|gt|quot|#39|#x27|apos|nbsp|amp);/g, (_, e: string) => ENTITY[e]);\nconst squash = (s: string): string => decode(s).replace(/\\s+/g, \" \").trim();\n\n/** Longest start tag read as a tag; past it a `<` is text (keeps the scan linear on unclosed quotes). */\nconst MAX_TAG = 4096;\n/** Elements whose content is not text to index: their content is skipped to the closing tag. */\nconst RAW_TEXT = new Set([\"script\", \"style\", \"textarea\"]);\n\n/**\n * The end of the start or end tag at `lt` (the index after its `>`), or -1\n * when the `<` does not open one: a tag name must follow `<` or `</`\n * directly (`1 < 2` and `< pre>` are text, as in a browser), a quoted\n * attribute value may hold `>` or `<`, and an unquoted `<` ends the attempt.\n */\nfunction tagEnd(html: string, lt: number): number {\n const first = html.charCodeAt(lt + 1) === 47 /* / */ ? lt + 2 : lt + 1;\n if (!/[a-z]/i.test(html[first] ?? \"\")) return -1;\n let quote = \"\";\n let prev = \"\";\n const stop = Math.min(html.length, lt + MAX_TAG);\n for (let k = first; k < stop; k++) {\n const c = html[k];\n if (quote) {\n if (c === quote) quote = \"\";\n continue;\n }\n if ((c === '\"' || c === \"'\") && prev === \"=\") quote = c;\n else if (c === \">\") return k + 1;\n else if (c === \"<\") return -1;\n if (c !== \" \" && c !== \"\\t\" && c !== \"\\n\" && c !== \"\\r\") prev = c;\n }\n return -1;\n}\n\n/**\n * HTML as text in one linear pass (regexes over raw HTML were quadratic on\n * unclosed tags: 100 KB of `<` took ten seconds). Inline tags join their\n * text (`un<em>believ</em>able`), other tags separate it, `<pre>` is code\n * and skipped, comments and script/style content are dropped, and heading\n * text is returned apart. A `<` that does not open a tag is text.\n */\nfunction htmlParts(html: string): { text: string; headings: string[] } {\n let text = \"\";\n const headings: string[] = [];\n let heading: string | undefined;\n let pre = 0;\n let lower: string | undefined;\n const emit = (t: string) => {\n if (pre) return;\n if (heading !== undefined) heading += t;\n else text += t;\n };\n const endHeading = () => {\n if (heading !== undefined) headings.push(squash(heading));\n heading = undefined;\n };\n let i = 0;\n while (i < html.length) {\n const lt = html.indexOf(\"<\", i);\n emit(html.slice(i, lt < 0 ? html.length : lt));\n if (lt < 0) break;\n if (html.startsWith(\"<!--\", lt)) {\n const end = html.indexOf(\"-->\", lt + 4);\n i = end < 0 ? html.length : end + 3;\n emit(\" \");\n continue;\n }\n const end = tagEnd(html, lt);\n if (end < 0) {\n emit(\"<\");\n i = lt + 1;\n continue;\n }\n i = end;\n const tag = /^<(\\/?)([a-z][a-z0-9-]*)/i.exec(html.slice(lt, Math.min(end, lt + 64)))!;\n const closing = tag[1] === \"/\";\n const name = tag[2].toLowerCase();\n if (!closing && RAW_TEXT.has(name)) {\n lower ??= html.toLowerCase();\n const close = lower.indexOf(`</${name}`, i);\n i = close < 0 ? html.length : close;\n emit(\" \");\n } else if (name === \"pre\") {\n pre = Math.max(0, pre + (closing ? -1 : 1));\n text += \" \";\n } else if (/^h[1-6]$/.test(name)) {\n // An unclosed heading ends where the next one starts.\n endHeading();\n if (!closing) heading = \"\";\n text += \" \";\n } else if (!INLINE_TAGS.has(name)) emit(\" \");\n }\n endHeading();\n return { text: squash(text), headings: headings.filter(Boolean) };\n}\n\n/** HTML as body text, its headings included. */\nfunction htmlText(html: string): string {\n const { text, headings } = htmlParts(html);\n return [text, ...headings].join(\" \");\n}\n\n/** Parameter or schema property names and descriptions, nested `properties` / `items` included. */\nfunction propertyText(list: unknown, depth = 0): string[] {\n if (depth > 10) return [];\n return objects(list).flatMap((p) => [str(p.name), specText(p.description), ...propertyText(p.properties, depth + 1), ...propertyText(p.items, depth + 1)]);\n}\n\n/** HTML elements a spec description uses as markup; anything else in angle brackets is text. */\nconst SPEC_MARKUP = new Set(\n [...INLINE_TAGS, ...\"br p div ul ol li dl dt dd table thead tbody tfoot tr td th caption h1 h2 h3 h4 h5 h6 blockquote hr img pre section\".split(\" \")],\n);\n\n/**\n * A spec's text (CommonMark) as search text. The page renders it with raw\n * HTML off, so every `<…>` shows as written: `List<String>`, `Map<K, V>` and\n * `<T extends Foo>` are words a reader sees and searches for. Only tags\n * naming an HTML element are dropped (`<br>`, `<code>`: noise in a\n * snippet), outside backtick code, and entities decoded. Linear: a tag\n * match cannot run past the next `<` or `>`.\n */\nfunction specText(v: unknown): string {\n const s = str(v);\n if (!s.includes(\"<\") && !s.includes(\"&\")) return s;\n return s\n .split(/(`+[^`]*`+)/)\n .map((part, i) =>\n i % 2\n ? part\n : decode(part.replace(/<\\/?([a-z][a-z0-9]*)\\b[^<>]*>/gi, (m, name: string) => (SPEC_MARKUP.has(name.toLowerCase()) ? (INLINE_TAGS.has(name.toLowerCase()) ? \"\" : \" \") : m))),\n )\n .join(\"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Text for indexing: headings separated from body, code blocks skipped.\n *\n * API reference nodes keep their text in props, so they are read here:\n * an OpenAPI operation (`endpoint`) by its method and path, summary,\n * description, parameters, request body and responses; autodoc symbols by\n * their names (as headings, as the page shows them) and docstrings. Their\n * code (signatures, source, samples) is skipped like code blocks. An\n * embedded spec's descriptions (`prose` with HTML) count as body, and their\n * headings as headings.\n */\nexport function extractText(tree: TreeNode[]): { headings: string[]; body: string } {\n const headings: string[] = [];\n const body: string[] = [];\n const inl = (a?: InlineNode[]): string => {\n if (!a) return \"\";\n let s = \"\";\n for (const n of a) {\n if (n.type === \"text\" || n.type === \"code\") s += n.text;\n else if (n.type === \"link\" || n.type === \"highlight\") s += inl(n.children);\n else if (n.type === \"kbd\") s += n.keys.join(\" \");\n else if (n.type === \"badge\") s += n.text;\n else if (n.type === \"image\") s += n.alt ?? \"\";\n else if (n.type === \"html-inline\") s += \" \";\n }\n return s;\n };\n const walk = (nodes?: TreeNode[]) => {\n for (const n of nodes ?? []) {\n if (n.type === \"code\" || n.type === \"api-source\") continue;\n const ap = (n.props ?? {}) as Json;\n switch (n.type) {\n case \"endpoint\":\n body.push(\n `${str(ap.method).toUpperCase()} ${str(ap.path)}`,\n specText(ap.summary),\n specText(ap.description),\n ...propertyText(ap.parameters),\n ...propertyText(ap.requestBody),\n ...objects(ap.responses).flatMap((r) => [specText(r.description), ...propertyText(r.schema)]),\n );\n continue;\n case \"api-class\":\n case \"api-member\":\n headings.push(str(ap.name));\n walk(n.children);\n continue;\n case \"api-doc\":\n body.push(htmlText(str(ap.html)));\n walk(n.children);\n continue;\n case \"api-params\":\n for (const p of objects(ap.params)) body.push(str(p.name), htmlText(str(p.docHtml)) || str(p.doc) || str(p.description));\n continue;\n case \"prose\":\n if (!n.inline && typeof n.html === \"string\") {\n const parts = htmlParts(n.html);\n headings.push(...parts.headings);\n body.push(parts.text);\n continue;\n }\n break;\n }\n if (n.type === \"heading\") {\n headings.push(inl(n.inline));\n continue;\n }\n if (n.type === \"html\" && n.html) body.push(htmlText(n.html));\n if (n.inline) body.push(inl(n.inline));\n const p = n.props as Record<string, unknown> | undefined;\n for (const k of [\"title\", \"label\", \"caption\", \"description\"]) if (typeof p?.[k] === \"string\") body.push(p[k] as string);\n walk(n.children);\n }\n };\n walk(tree);\n return { headings, body: body.join(\" \").replace(/\\s+/g, \" \").trim() };\n}\n"],"mappings":"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","names":["end","p"]}
|
package/dist/query.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/query.ts"],"sourcesContent":["/**\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":"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","names":["i","k","key"]}
|
package/dist/site.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/site.ts"],"sourcesContent":["/**\n * Node: index a folder of built HTML, from any site generator, and write\n * the index files. The indexing itself (extractHtml, SearchIndexBuilder)\n * is Worker-safe; only the file walking here needs Node.\n */\nimport { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { join, relative, resolve, sep } from \"node:path\";\nimport { extractHtml, folderName } from \"./html.js\";\nimport { SearchIndexBuilder } from \"./index.js\";\n\nexport interface IndexSiteOptions {\n /** Where to write the index, relative to `dir` (default `_search`). */\n out?: string;\n /**\n * The site's language, for the query-time stemmer; default: the first page's `<html lang>`.\n * One index has one language: index each language's folder separately.\n */\n lang?: string;\n}\n\nexport function indexSite(dir: string, opts: IndexSiteOptions = {}): { pages: number; skipped: number; files: number; out: string } {\n const root = resolve(dir);\n const out = resolve(root, opts.out ?? \"_search\");\n const files: string[] = [];\n // Folders are not followed through symbolic links (they can loop); linked files are indexed.\n const walk = (d: string) => {\n for (const e of readdirSync(d, { withFileTypes: true })) {\n const full = join(d, e.name);\n if (e.name.startsWith(\".\") || full === out) continue;\n if (e.isDirectory()) walk(full);\n else if ((e.isFile() || (e.isSymbolicLink() && isFile(full))) && /\\.html?$/i.test(e.name)) files.push(full);\n }\n };\n walk(root);\n files.sort();\n\n let builder: SearchIndexBuilder | undefined;\n let pages = 0;\n let skipped = 0;\n for (const f of files) {\n const rel = relative(root, f).split(sep).join(\"/\");\n // Results link to this URL, so each path segment is percent-encoded (\"c#.html\" → \"c%23.html\").\n const url = rel.split(\"/\").map(encodeURIComponent).join(\"/\");\n const page = extractHtml(readFileSync(f, \"utf8\"), url, { section: folderName(rel) });\n if (!page) {\n skipped++;\n continue;\n }\n builder ??= new SearchIndexBuilder({ lang: opts.lang ?? page.lang ?? \"en\" });\n builder.add(page.doc, page.parts);\n pages++;\n }\n builder ??= new SearchIndexBuilder({ lang: opts.lang ?? \"en\" });\n mkdirSync(out, { recursive: true });\n let written = 0;\n for (const [name, content] of builder.emitFiles()) {\n writeFileSync(join(out, name), content);\n written++;\n }\n return { pages, skipped, files: written, out };\n}\n\nfunction isFile(p: string): boolean {\n try {\n return statSync(p).isFile();\n } catch {\n return false;\n }\n}\n\n/** True when `p` is a directory. */\nexport function isDir(p: string): boolean {\n try {\n return statSync(p).isDirectory();\n } catch {\n return false;\n }\n}\n"],"mappings":"AAKA,SAAS,WAAW,aAAa,cAAc,UAAU,qBAAqB;AAC9E,SAAS,MAAM,UAAU,SAAS,WAAW;AAC7C,SAAS,aAAa,kBAAkB;AACxC,SAAS,0BAA0B;AAY5B,SAAS,UAAU,KAAa,OAAyB,CAAC,GAAmE;AAClI,QAAM,OAAO,QAAQ,GAAG;AACxB,QAAM,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS;AAC/C,QAAM,QAAkB,CAAC;AAEzB,QAAM,OAAO,CAAC,MAAc;AAC1B,eAAW,KAAK,YAAY,GAAG,EAAE,eAAe,KAAK,CAAC,GAAG;AACvD,YAAM,OAAO,KAAK,GAAG,EAAE,IAAI;AAC3B,UAAI,EAAE,KAAK,WAAW,GAAG,KAAK,SAAS,IAAK;AAC5C,UAAI,EAAE,YAAY,EAAG,MAAK,IAAI;AAAA,gBACpB,EAAE,OAAO,KAAM,EAAE,eAAe,KAAK,OAAO,IAAI,MAAO,YAAY,KAAK,EAAE,IAAI,EAAG,OAAM,KAAK,IAAI;AAAA,IAC5G;AAAA,EACF;AACA,OAAK,IAAI;AACT,QAAM,KAAK;AAEX,MAAI;AACJ,MAAI,QAAQ;AACZ,MAAI,UAAU;AACd,aAAW,KAAK,OAAO;AACrB,UAAM,MAAM,SAAS,MAAM,CAAC,EAAE,MAAM,GAAG,EAAE,KAAK,GAAG;AAEjD,UAAM,MAAM,IAAI,MAAM,GAAG,EAAE,IAAI,kBAAkB,EAAE,KAAK,GAAG;AAC3D,UAAM,OAAO,YAAY,aAAa,GAAG,MAAM,GAAG,KAAK,EAAE,SAAS,WAAW,GAAG,EAAE,CAAC;AACnF,QAAI,CAAC,MAAM;AACT;AACA;AAAA,IACF;AACA,gBAAY,IAAI,mBAAmB,EAAE,MAAM,KAAK,QAAQ,KAAK,QAAQ,KAAK,CAAC;AAC3E,YAAQ,IAAI,KAAK,KAAK,KAAK,KAAK;AAChC;AAAA,EACF;AACA,cAAY,IAAI,mBAAmB,EAAE,MAAM,KAAK,QAAQ,KAAK,CAAC;AAC9D,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,MAAI,UAAU;AACd,aAAW,CAAC,MAAM,OAAO,KAAK,QAAQ,UAAU,GAAG;AACjD,kBAAc,KAAK,KAAK,IAAI,GAAG,OAAO;AACtC;AAAA,EACF;AACA,SAAO,EAAE,OAAO,SAAS,OAAO,SAAS,IAAI;AAC/C;AAEA,SAAS,OAAO,GAAoB;AAClC,MAAI;AACF,WAAO,SAAS,CAAC,EAAE,OAAO;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,MAAM,GAAoB;AACxC,MAAI;AACF,WAAO,SAAS,CAAC,EAAE,YAAY;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
package/dist/text.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/text.ts"],"sourcesContent":["/**\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"],"mappings":"AAIA,SAAS,eAAe;AACxB,MAAM,OAAO,IAAI;AAAA,EACf,wHAAwH,MAAM,GAAG;AACnI;AAGA,MAAM,MAAM;AAEZ,IAAI;AAQJ,SAAS,SAAS,KAAuB;AACvC,MAAI,cAAc,QAAW;AAC3B,QAAI;AACF,kBAAY,OAAO,SAAS,eAAe,eAAe,OAAO,IAAK,KAAa,UAAU,QAAW,EAAE,aAAa,OAAO,CAAC,IAAI;AAAA,IACrI,QAAQ;AACN,kBAAY;AAAA,IACd;AAAA,EACF;AACA,MAAI,UAAW,QAAO,CAAC,GAAG,UAAU,QAAQ,GAAG,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO;AAClG,QAAM,QAAQ,CAAC,GAAG,GAAG;AACrB,MAAI,MAAM,SAAS,EAAG,QAAO;AAC7B,QAAM,MAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,IAAK,KAAI,KAAK,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,CAAC;AAC3E,SAAO;AACT;AAGO,SAAS,SAAS,MAAwB;AAC/C,QAAM,MAAgB,CAAC;AACvB,aAAW,KAAK,KAAK,YAAY,EAAE,UAAU,MAAM,EAAE,QAAQ,UAAU,EAAE,EAAE,UAAU,KAAK,EAAE,SAAS,yDAAyD,GAAG;AAC/J,UAAM,MAAM,EAAE,CAAC;AAEf,UAAM,QAAQ,IAAI,MAAM,QAAQ,EAAE,OAAO,OAAO;AAChD,QAAI,MAAM,SAAS,EAAG,KAAI,KAAK,GAAG;AAElC,QAAI,MAAM,SAAS,KAAK,CAAC,OAAO,KAAK,GAAG,GAAG;AACzC,YAAM,SAAS,MAAM,KAAK,EAAE;AAC5B,UAAI,OAAO,UAAU,KAAK,CAAC,KAAK,IAAI,MAAM,EAAG,KAAI,KAAK,MAAM;AAAA,IAC9D;AACA,eAAW,KAAK,OAAO;AACrB,UAAI,IAAI,KAAK,CAAC,EAAG,KAAI,KAAK,GAAG,SAAS,CAAC,CAAC;AAAA,eAC/B,EAAE,UAAU,KAAK,CAAC,KAAK,IAAI,CAAC,EAAG,KAAI,KAAK,CAAC;AAAA,IACpD;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,SAAS,MAAc,OAAuB;AAC5D,MAAI,IAAI;AAER,aAAW,MAAM,KAAK,MAAM,GAAG,KAAK,EAAG,MAAK,WAAW,KAAK,EAAE,IAAI,KAAK,IAAI,GAAG,YAAY,CAAC,EAAG,SAAS,EAAE,CAAC;AAC1G,SAAO;AACT;AAOO,SAAS,QAAQ,MAAkE;AACxF,MAAI,CAAC,QAAQ,CAAC,YAAY,KAAK,IAAI,EAAG,QAAO;AAC7C,SAAO,CAAC,MAAO,cAAc,KAAK,CAAC,IAAI,QAAQ,CAAC,IAAI;AACtD;","names":[]}
|