@pagenary/publisher 2026.6.0 → 2026.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/package.json +1 -1
- package/scripts/build-tenants.js +17 -0
- package/scripts/lib/search-index-generator.js +111 -0
- package/site/app.js +1 -1
- package/site/index.html +1 -1
- package/site/lib/fortemi-corpus.js +1 -0
- package/site/lib/search.js +1 -1
- package/site/pages/api.html +50 -13
- package/site/pages/architecture.html +37 -15
- package/site/pages/deployment.html +1 -1
- package/site/pages/developer-guide.html +2 -2
- package/site/pages/extending.html +1 -1
- package/site/pages/quickstart.html +5 -2
- package/site/pages/seo-strategy.html +1 -1
- package/site/pages/tenant-config.html +1 -1
- package/site/pages/welcome.html +1 -1
- package/site/robots.txt +1 -1
- package/site/search-index/manifest.json +49 -0
- package/site/search-index/part-0000.json +358 -0
- package/site/sections/api.js +1 -1
- package/site/sections/architecture.js +1 -1
- package/site/sections/developer-guide.js +1 -1
- package/site/sections/quickstart.js +1 -1
- package/site/sitemap.xml +10 -10
- package/site/styles.css +30 -0
- package/site/vendor/fortemi-aiwg-index.d.ts +212 -0
- package/site/vendor/fortemi-aiwg-index.js +1 -0
- package/src/app.js +73 -3
- package/src/lib/fortemi-corpus.js +0 -0
- package/src/lib/search.js +291 -42
- package/src/styles.css +30 -0
- package/src/vendor/fortemi-aiwg-index.d.ts +212 -0
- package/src/vendor/fortemi-aiwg-index.js +564 -0
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
`@pagenary/publisher` is the static site generator behind Pagenary — it turns one shared template catalog into many branded, tenant-specific documentation sites. Zero runtime dependencies, hash-based routing, full-text search, and a Git-aware build pipeline. Install it as a dev dependency and drive it with the `pagenary` CLI.
|
|
8
8
|
|
|
9
|
+
Built with [AIWG](https://aiwg.io), the multi-agent AI framework used to plan,
|
|
10
|
+
audit, and ship this project.
|
|
11
|
+
|
|
9
12
|
```bash
|
|
10
13
|
npm install --save-dev @pagenary/publisher # add Pagenary to your project
|
|
11
14
|
npx pagenary build:tenants my-docs # build your docs tenant
|
|
@@ -17,6 +20,8 @@ npx pagenary serve # serve on http://localhost:5173
|
|
|
17
20
|
[](https://docs.pagenary.com)
|
|
18
21
|
[](../../LICENSE)
|
|
19
22
|
[](https://nodejs.org)
|
|
23
|
+
[](https://www.npmjs.com/package/@fortemi/core)
|
|
24
|
+
[](https://www.npmjs.com/package/aiwg)
|
|
20
25
|
|
|
21
26
|
[**Docs Site**](https://docs.pagenary.com) · [**Quick Start**](#quick-start) · [**Features**](#features) · [**Tenant Workflow**](#tenant-content-workflow) · [**Documentation**](#documentation)
|
|
22
27
|
|
|
@@ -53,6 +58,9 @@ npm run dev # build + serve with watch mode
|
|
|
53
58
|
npm run build # build default bundle to dist/
|
|
54
59
|
```
|
|
55
60
|
|
|
61
|
+
Pagenary development uses [AIWG](https://aiwg.io). On this host, maintainers can
|
|
62
|
+
inspect, build, or run the AIWG project from `~/dev/aiwg`.
|
|
63
|
+
|
|
56
64
|
---
|
|
57
65
|
|
|
58
66
|
## Features
|
|
@@ -78,7 +86,10 @@ npm run build # build default bundle to dist/
|
|
|
78
86
|
|
|
79
87
|
### Navigation & Search
|
|
80
88
|
- **Command Palette** — `Ctrl/Cmd+K` or `/` opens a global finder
|
|
81
|
-
- **
|
|
89
|
+
- **Fortemi-backed full-text search** — ranked results with snippets over a static
|
|
90
|
+
chunked index emitted at build time; lazy chunk fetch (precache) and offset
|
|
91
|
+
paging for infinite scroll, with a clean in-browser fallback. No server, no WASM.
|
|
92
|
+
See `docs/ARCHITECTURE.md` and `.aiwg/architecture/adr/ADR-015-fortemi-core-search-adapter.md`.
|
|
82
93
|
- **Manifest-Driven Nav** — declarative navigation structure
|
|
83
94
|
- **Keyboard Navigation** — arrow keys, Enter to select
|
|
84
95
|
|
package/package.json
CHANGED
package/scripts/build-tenants.js
CHANGED
|
@@ -9,6 +9,7 @@ import os from 'os';
|
|
|
9
9
|
import { generateSeoArtifacts, resolveBaseUrl, resolveOgImage } from './lib/seo-generator.js';
|
|
10
10
|
import { generateCollections } from './lib/collections-generator.js';
|
|
11
11
|
import { parseFrontmatter } from './lib/frontmatter.js';
|
|
12
|
+
import { generateSearchIndex } from './lib/search-index-generator.js';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
13
14
|
|
|
14
15
|
const root = process.cwd();
|
|
@@ -2811,6 +2812,14 @@ async function processNestedContent(sourceDir, distDir, tenantId, contentRoot, o
|
|
|
2811
2812
|
await fsp.writeFile(path.join(distDir, 'manifest.js'), manifestModule, 'utf8');
|
|
2812
2813
|
console.log(` ↳ applied nested content structure for ${tenantId} (${context.leafOrder.length} sections)`);
|
|
2813
2814
|
|
|
2815
|
+
// Emit the static Fortemi chunked search index. Failure here never breaks the
|
|
2816
|
+
// bundle — the runtime search adapter degrades to its legacy in-browser index.
|
|
2817
|
+
try {
|
|
2818
|
+
await generateSearchIndex(distDir, processedManifest, { tenantId });
|
|
2819
|
+
} catch (err) {
|
|
2820
|
+
console.warn(` ↳ search index generation skipped for ${tenantId}: ${err.message}`);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2814
2823
|
return { success: true, sectionsCount: context.leafOrder.length };
|
|
2815
2824
|
}
|
|
2816
2825
|
|
|
@@ -3111,6 +3120,14 @@ async function processTenantManifestLegacy(sourceDir, distDir, tenantId, options
|
|
|
3111
3120
|
const manifestModule = buildManifestModuleSource(processedManifest, defaultSection, context.siteConfig);
|
|
3112
3121
|
await fsp.writeFile(path.join(distDir, 'manifest.js'), manifestModule, 'utf8');
|
|
3113
3122
|
console.log(` ↳ applied manifest-driven content for ${tenantId}`);
|
|
3123
|
+
|
|
3124
|
+
// Emit the static Fortemi chunked search index (legacy manifest path).
|
|
3125
|
+
try {
|
|
3126
|
+
await generateSearchIndex(distDir, processedManifest, { tenantId });
|
|
3127
|
+
} catch (err) {
|
|
3128
|
+
console.warn(` ↳ search index generation skipped for ${tenantId}: ${err.message}`);
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3114
3131
|
return { success: true };
|
|
3115
3132
|
}
|
|
3116
3133
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time Fortemi search-index generator.
|
|
3
|
+
*
|
|
4
|
+
* After a tenant's manifest.js + sections/ are materialized, this emits a
|
|
5
|
+
* deterministic chunked static index that the runtime adapter (src/lib/search.js)
|
|
6
|
+
* loads through the vendored @fortemi/core controller:
|
|
7
|
+
*
|
|
8
|
+
* dist/<tenant>/search-index/manifest.json (AiwgFortemiChunkManifest)
|
|
9
|
+
* dist/<tenant>/search-index/part-0000.json (AiwgFortemiChunkPart)
|
|
10
|
+
* ...
|
|
11
|
+
*
|
|
12
|
+
* Text is extracted by importing each section module and calling load(), the
|
|
13
|
+
* same contract the renderer uses, then stripping HTML without a DOM. The corpus
|
|
14
|
+
* is deterministic (sorted by record id, content-hashed generated_at) so repeat
|
|
15
|
+
* builds are byte-identical and incremental rebuilds refresh stale entries.
|
|
16
|
+
*/
|
|
17
|
+
import fsp from 'fs/promises';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { pathToFileURL } from 'node:url';
|
|
21
|
+
import {
|
|
22
|
+
buildFortemiIndexExport,
|
|
23
|
+
chunkFortemiIndex,
|
|
24
|
+
stripHtml,
|
|
25
|
+
DEFAULT_PART_SIZE
|
|
26
|
+
} from '../../src/lib/fortemi-corpus.js';
|
|
27
|
+
import { flattenManifest } from '../../src/lib/search.js';
|
|
28
|
+
|
|
29
|
+
const SEARCH_INDEX_DIR = 'search-index';
|
|
30
|
+
|
|
31
|
+
async function pathExists(target) {
|
|
32
|
+
try {
|
|
33
|
+
await fsp.access(target, fs.constants.F_OK);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Import a materialized section module and return its plain-text content.
|
|
42
|
+
* Resilient: a module that fails to load degrades to title/summary text, exactly
|
|
43
|
+
* as the runtime search index does.
|
|
44
|
+
* @param {object} section - Flattened section with a `module` path (./sections/x.js)
|
|
45
|
+
* @param {string} distDir - Tenant output directory
|
|
46
|
+
* @returns {Promise<string>}
|
|
47
|
+
*/
|
|
48
|
+
async function extractSectionText(section, distDir) {
|
|
49
|
+
if (!section.module) return '';
|
|
50
|
+
const rel = section.module.replace(/^\.?\//, '');
|
|
51
|
+
const abs = path.resolve(distDir, rel);
|
|
52
|
+
if (!(await pathExists(abs))) return '';
|
|
53
|
+
try {
|
|
54
|
+
const mod = await import(pathToFileURL(abs).href);
|
|
55
|
+
const loader = mod.load || mod.default;
|
|
56
|
+
if (typeof loader !== 'function') return '';
|
|
57
|
+
const payload = await loader();
|
|
58
|
+
return stripHtml(payload && payload.html ? payload.html : '');
|
|
59
|
+
} catch {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate and write the chunked Fortemi index for one tenant bundle.
|
|
66
|
+
* @param {string} distDir - Tenant output directory (contains manifest.js, sections/)
|
|
67
|
+
* @param {Array} processedManifest - The nested manifest array written to manifest.js
|
|
68
|
+
* @param {object} [options]
|
|
69
|
+
* @param {string} [options.tenantId='pagenary']
|
|
70
|
+
* @param {number} [options.partSize=DEFAULT_PART_SIZE]
|
|
71
|
+
* @returns {Promise<{ total: number, parts: number, buildHash: string }>}
|
|
72
|
+
*/
|
|
73
|
+
export async function generateSearchIndex(distDir, processedManifest, options = {}) {
|
|
74
|
+
const tenantId = options.tenantId || 'pagenary';
|
|
75
|
+
const partSize = options.partSize || DEFAULT_PART_SIZE;
|
|
76
|
+
const outDir = path.join(distDir, SEARCH_INDEX_DIR);
|
|
77
|
+
|
|
78
|
+
const flat = flattenManifest(processedManifest || []);
|
|
79
|
+
const entries = [];
|
|
80
|
+
for (const section of flat) {
|
|
81
|
+
if (!section || !section.id) continue;
|
|
82
|
+
const content = await extractSectionText(section, distDir);
|
|
83
|
+
const text = `${section.title || ''} ${section.summary || ''} ${section.group || ''} ${content}`.trim();
|
|
84
|
+
entries.push({ section, text });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { index, buildHash } = buildFortemiIndexExport(entries, { repo: tenantId });
|
|
88
|
+
const { manifest, parts } = chunkFortemiIndex(index, { partSize });
|
|
89
|
+
|
|
90
|
+
// Replace the directory wholesale so stale parts from a larger prior corpus
|
|
91
|
+
// never linger (incremental-build correctness).
|
|
92
|
+
await fsp.rm(outDir, { recursive: true, force: true });
|
|
93
|
+
await fsp.mkdir(outDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
await fsp.writeFile(
|
|
96
|
+
path.join(outDir, 'manifest.json'),
|
|
97
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
98
|
+
'utf8'
|
|
99
|
+
);
|
|
100
|
+
await Promise.all(parts.map((part, i) =>
|
|
101
|
+
fsp.writeFile(
|
|
102
|
+
path.join(outDir, manifest.parts[i].href),
|
|
103
|
+
`${JSON.stringify(part, null, 2)}\n`,
|
|
104
|
+
'utf8'
|
|
105
|
+
)));
|
|
106
|
+
|
|
107
|
+
console.log(` ↳ search index: ${manifest.total} record(s) in ${parts.length} part(s) [${buildHash.slice(0, 8)}]`);
|
|
108
|
+
return { total: manifest.total, parts: parts.length, buildHash };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { SEARCH_INDEX_DIR };
|
package/site/app.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{MANIFEST as e,DEFAULT_SECTION as t,findSection as n,getAdjacentSections as a,SITE_CONFIG as s,EXPORT_CONFIG as o}from"./manifest.js";import{updateMetaTags as i}from"./seo.js";import{escapeRegExp as l,searchContent as r,flattenManifest as c,findPreferredIndex as d}from"./lib/search.js";import{resolveTarget as m,resolveEntry as p}from"./lib/router.js";import{composeExportDocument as u,collectExportableSections as v}from"./lib/export.js";import{renderMermaidBlocks as h}from"./mermaid-init.js";import{highlightCodeBlocks as f}from"./syntax-highlight.js";const y=document.getElementById("app"),g=document.getElementById("nav"),b=document.getElementById("year"),E=document.getElementById("exportBtn"),L=document.getElementById("commandToggle"),T=document.getElementById("commandPalette"),x=document.getElementById("commandInput"),w=document.getElementById("commandList"),N=document.getElementById("mobileMenuToggle"),C=document.querySelector(".sidebar"),k="docs-toolkit-command-query",$=new Map,H=new Map,I=new Map,M=new Set;let S=[],A=0,P=!1,q=(localStorage.getItem(k)||"").trim(),B=!1;function D(e,t){const n=document.createElement("a");return n.href=e.url,n.target="_blank",n.rel="noopener noreferrer",n.className=`${t} nav-external`,n.title=e.summary||e.title,n.innerHTML=`\n <span class="nav-title">${e.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,n}function R(e,t={}){const{scrollToHighlight:a=!1}=t,{targetId:s,parentId:o}=function(e){return m(e,n)}(e);P&&W(),o&&O(o,!0),B=a||Boolean(q),location.hash.replace("#","")===s?j():location.hash=`#${s}`}function F(){return location.hash.replace("#","")||t}async function j(){const e=F(),t=p(e,n);if(!t)return;const{entry:o,targetId:l,parentId:r}=t;l===e?(r&&O(r,!0),function(e,t=null){H.forEach(e=>{e.setAttribute("aria-current","false")});const n=H.get(e);if(n&&n.setAttribute("aria-current","page"),t){const e=H.get(t);e&&e.setAttribute("aria-current","page")}}(o.id,r),await async function(e){if(!e)return;const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)return void(y.innerHTML='<article class="section"><p>Section failed to load.</p></article>');const o=await n();y.innerHTML=o.html||"",function(e){if(!e||!e.showDate&&!e.showReadingTime&&!e.showSummary)return;const t=(y.querySelector(".doc-content")||y.querySelector("article, section")||y).querySelector("h1");if(!t)return;const n=[];e.showDate&&e.date&&n.push(function(e){const t=String(e||"").trim();if(!t)return"";const n=/^\d{4}-\d{2}-\d{2}$/.test(t)?`${t}T00:00:00Z`:t,a=new Date(n);return Number.isNaN(a.getTime())?t:new Intl.DateTimeFormat(void 0,{year:"numeric",month:"long",day:"numeric",timeZone:"UTC"}).format(a)}(e.date)),e.showReadingTime&&e.reading_time&&n.push(`${e.reading_time} min read`);let a=t;if(n.length>0){const e=document.createElement("p");e.className="doc-meta",e.textContent=n.join(" · "),t.after(e),a=e}if(e.showSummary&&e.summary){const t=document.createElement("p");t.className="doc-summary",t.textContent=e.summary,a.after(t)}}(e),await h(y),await f(y),function(e){const t=y.querySelector(".bottom-nav");if(t&&t.remove(),"never"===s.bottomNav)return;const n=(s.bottomNavSections||[]).some(t=>e.startsWith(t)),o="mobile"===s.bottomNav&&!n,{prev:i,next:l}=a(e);if(!i&&!l)return;const r=document.createElement("nav");if(r.className="bottom-nav",o&&r.classList.add("mobile-only"),i){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-prev",e.innerHTML='<span class="bottom-nav-chevron">‹</span>';const t=document.createElement("a");t.href=`#${i.id}`,t.className="bottom-nav-link",t.title=`Previous: ${i.title}`,t.textContent=i.title,t.addEventListener("click",e=>{e.preventDefault(),R(i.id)}),e.appendChild(t),r.appendChild(e)}else{const e=document.createElement("div");e.className="bottom-nav-spacer",r.appendChild(e)}if(l){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-next";const t=document.createElement("a");t.href=`#${l.id}`,t.className="bottom-nav-link",t.title=`Next: ${l.title}`,t.textContent=l.title,t.addEventListener("click",e=>{e.preventDefault(),R(l.id)}),e.appendChild(t),e.innerHTML+='<span class="bottom-nav-chevron">›</span>',r.appendChild(e)}(y.querySelector("section")||y).appendChild(r)}(e.id),y.scrollTop=0,window.scrollTo(0,0),"function"==typeof o.afterRender&&o.afterRender(y),i({title:e.title,description:e.summary,siteTitle:s.siteTitle,siteUrl:s.siteUrl,sectionId:e.id,ogImage:e.ogImage||s.ogImage}),$.set(e.id,Date.now());const l=B;B=!1,K(l),requestAnimationFrame(()=>y.focus())}(o)):location.replace(`#${l}`)}function O(e,t){if(!e)return;const n=I.get(e);t?(M.add(e),n&&n.group.classList.add("expanded")):(M.delete(e),n&&n.group.classList.remove("expanded"))}function _(){if(!T||!x)return;P=!0,T.hidden=!1;const e=q;x.value=e,z(e),requestAnimationFrame(()=>{x.focus(),e&&x.select()})}function W(){T&&x&&(P=!1,T.hidden=!0,x.blur())}!function(){g.innerHTML="",H.clear(),I.clear();let t=M.size>0;e.forEach((e,n)=>{if(e.url){const t=D(e,"nav-leaf");return void g.appendChild(t)}if(e.subsections&&e.subsections.length){const n=document.createElement("div");n.className="nav-group";const a=Boolean(e.module),s=document.createElement("button");s.type="button",s.className="nav-parent"+(a?" nav-parent-with-content":""),s.dataset.section=e.id,s.title=e.summary,a?(s.innerHTML=`\n <span class="nav-title-link">${e.title}</span>\n <span class="nav-expand-toggle" aria-label="Expand"></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.querySelector(".nav-title-link").addEventListener("click",t=>{t.stopPropagation(),R(e.id,{scrollToHighlight:Boolean(q)})}),s.querySelector(".nav-expand-toggle").addEventListener("click",t=>{t.stopPropagation();const n=!M.has(e.id);O(e.id,n)}),s.addEventListener("click",t=>{if(t.target===s){const t=!M.has(e.id);O(e.id,t)}})):(s.innerHTML=`\n <span class="nav-title">${e.title}</span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)}));const o=document.createElement("div");o.className="nav-sublist",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void o.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-nested";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-nested",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-nested",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void a.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-deep";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-deep",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const s=document.createElement("div");s.className="nav-sublist nav-sublist-deep",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void s.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-ultra";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-ultra",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-ultra",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void a.appendChild(t)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-ultra"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),s.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const o=M.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-deep"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),s.appendChild(t),H.set(e.id,t)}),t.append(n,s),a.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:s});const o=M.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-nested"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),o.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const s=M.has(e.id)&&!e.collapsed;return void O(e.id,s)}const t=document.createElement("button");t.type="button",t.className="nav-item"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),o.appendChild(t),H.set(e.id,t)}),n.append(s,o),g.appendChild(n),H.set(e.id,s),I.set(e.id,{group:n,button:s,list:o});const i=!e.collapsed&&(M.has(e.id)||!t&&!M.size);O(e.id,i),i&&(t=!0)}else{const t=document.createElement("button");t.type="button",t.className="nav-leaf"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),g.appendChild(t),H.set(e.id,t)}})}(),q&&(B=!0),window.addEventListener("hashchange",()=>{q&&(B=!0),j()}),b.textContent=(new Date).getFullYear(),j(),L&&T&&x&&w&&(L.addEventListener("click",()=>{P?W():_()}),x.addEventListener("input",()=>{const e=x.value;J(e,!0),z(e)}),x.addEventListener("keydown",e=>{const t=S.length-1;if("ArrowDown"===e.key)e.preventDefault(),A=Math.min(t,A+1),X();else if("ArrowUp"===e.key)e.preventDefault(),A=Math.max(0,A-1),X();else if("Enter"===e.key){e.preventDefault();const t=S[A];t&&(J(x.value,!0),R(t.id,{scrollToHighlight:!0}),W())}else"Escape"===e.key&&(e.preventDefault(),W())}),w.addEventListener("click",e=>{const t=e.target.closest("[data-section]");if(!t)return;const n=t.dataset.section;n&&(J(x.value,!0),R(n,{scrollToHighlight:!0}),W())}),T.addEventListener("click",e=>{e.target===T&&W()}),window.addEventListener("keydown",e=>{const t=e.target,n=t&&("INPUT"===t.tagName||"TEXTAREA"===t.tagName||t.isContentEditable),a=e.metaKey||e.ctrlKey;"k"===e.key.toLowerCase()&&a||"/"===e.key&&!n?(e.preventDefault(),P?W():_()):"Escape"===e.key&&P&&(e.preventDefault(),W())}));let U=null,V=!1;async function z(t){w&&(!V&&t.trim()&&(V=!0,w.innerHTML='<li class="cmd-item cmd-loading">Indexing content...</li>'),clearTimeout(U),U=setTimeout(async()=>{S=await r(e,t);const n=F();A=d(S,n),function(){if(w){if(w.innerHTML="",!S.length){const e=document.createElement("li");return e.className="cmd-item",e.setAttribute("aria-selected","false"),e.textContent="No matches.",void w.appendChild(e)}S.forEach(e=>{const t=document.createElement("li");t.className="cmd-item",t.dataset.section=e.id,t.setAttribute("role","option");const n=document.createElement("span");if(n.className="cmd-item-title",n.textContent=e.title,e.group){const t=document.createElement("span");t.className="cmd-item-group",t.textContent=e.group,n.prepend(t)}const a=document.createElement("span");a.className="cmd-item-summary",a.textContent=e.summary||"",t.append(n,a),w.appendChild(t)})}}(),X(),V=!1},t.trim()?150:0))}function X(){w&&Array.from(w.children).forEach((e,t)=>{const n=t===A&&S.length;e.setAttribute("aria-selected",n?"true":"false"),n&&e.scrollIntoView({block:"nearest"})})}function G(e){const t=document.createElement("div");t.innerHTML=e,t.querySelectorAll("script").forEach(e=>e.remove()),t.querySelectorAll("button").forEach(e=>e.removeAttribute("onclick")),t.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)});const n=t.querySelector("section");return n?n.innerHTML:t.innerHTML}function J(e,t=!1){q=e.trim(),t&&(q?localStorage.setItem(k,q):localStorage.removeItem(k)),K()}function K(e=!1){y&&function(e,t,{scrollToFirst:n=!1}={}){if(!e)return;if(function(e){e&&e.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)})}(e),!t)return;const a=t.split(/\s+/).map(e=>e.trim()).filter(Boolean);if(!a.length)return;const s=a.map(e=>e.toLowerCase()),o=new Set(["SCRIPT","STYLE","CODE","PRE"]),i=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,{acceptNode(e){if(!e.nodeValue||!e.nodeValue.trim())return NodeFilter.FILTER_REJECT;const t=e.parentNode;return t&&o.has(t.tagName)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}),r=[];let c;for(;c=i.nextNode();){const e=c.nodeValue.toLowerCase();s.some(t=>e.includes(t))&&r.push(c)}const d=new RegExp(`(${a.map(l).join("|")})`,"gi");r.forEach(e=>{const t=e.nodeValue,n=[];let a=0;t.replace(d,(e,s,o)=>{o>a&&n.push(document.createTextNode(t.slice(a,o)));const i=document.createElement("mark");return i.className="hl",i.textContent=e,n.push(i),a=o+e.length,e}),a<t.length&&n.push(document.createTextNode(t.slice(a)));const s=document.createDocumentFragment();n.forEach(e=>s.appendChild(e)),e.parentNode.replaceChild(s,e)}),n&&requestAnimationFrame(()=>{const t=e.querySelector("mark.hl");t&&t.scrollIntoView({behavior:"smooth",block:"center"})})}(y,q,{scrollToFirst:e})}E&&E.addEventListener("click",function(){const t=document.createElement("div");t.className="export-options-overlay",t.innerHTML='\n <div class="export-options-modal">\n <div class="export-options-header">EXPORT OPTIONS</div>\n <div class="export-options-buttons">\n <button type="button" class="export-option-btn" data-scope="page">\n <span class="export-option-title">Current Page</span>\n <span class="export-option-desc">Export only this section</span>\n </button>\n <button type="button" class="export-option-btn" data-scope="site">\n <span class="export-option-title">Entire Site</span>\n <span class="export-option-desc">Export all documentation</span>\n </button>\n </div>\n <button type="button" class="export-cancel-btn">Cancel</button>\n </div>\n ',document.body.appendChild(t),setTimeout(()=>t.classList.add("active"),10);const n=()=>{t.classList.remove("active"),setTimeout(()=>t.remove(),200)};t.querySelector(".export-cancel-btn").addEventListener("click",n),t.addEventListener("click",e=>{e.target===t&&n()}),t.querySelectorAll(".export-option-btn").forEach(t=>{t.addEventListener("click",()=>{const a=t.dataset.scope;n(),async function(t="site"){if(!E)return;const n=E.innerHTML,a=window.open("","_blank","width=1,height=1,left=0,top=0");if(!a||a.closed||void 0===a.closed)return void confirm("Pop-ups are blocked. Please allow pop-ups for this site to export the document.\n\nWould you like to try again after enabling pop-ups?");a.close(),E.disabled=!0;const s=document.createElement("div");s.className="export-loading-overlay",s.innerHTML='\n <div class="export-loading-modal">\n <div class="export-loading-header">\n <div class="export-loading-title">COMPILING DOCUMENTATION</div>\n <div class="export-loading-subtitle">Assembling all sections into unified document</div>\n </div>\n <div class="export-loading-progress">\n <div class="export-loading-bar">\n <div class="export-loading-fill"></div>\n </div>\n <div class="export-loading-status-container">\n <div class="export-loading-status">Initializing...</div>\n </div>\n </div>\n <div class="export-loading-scanner">\n <div class="scanner-line"></div>\n </div>\n </div>\n ',document.body.appendChild(s),setTimeout(()=>s.classList.add("active"),10);const i=s.querySelector(".export-loading-fill"),l=s.querySelector(".export-loading-status");try{let n;if("page"===t){const t=F(),a=v(e).find(e=>e.id===t);n=a?[a]:[]}else n=v(e);if(0===n.length)return alert("No content available to export."),s.remove(),void(E.disabled=!1);const a=[],r=n.length;let c=0;for(const e of n){c++;const n=c/r*100;i.style.width=`${n}%`,l.textContent="page"===t?`Exporting: ${e.title}`:`Processing section ${c} of ${r}: ${e.title}`,await new Promise(e=>setTimeout(e,50));try{const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)continue;const s=G((await n()).html||"");a.push({section:e,html:s})}catch(t){console.error("Failed to include section in export",e.id,t)}}l.textContent="Generating document...",await new Promise(e=>setTimeout(e,200));const d=u(a,o);l.textContent="Opening document viewer...",await new Promise(e=>setTimeout(e,100));const m=window.open("","_blank","width=900,height=860,scrollbars=yes,resizable=yes");if(!m)return alert("Please allow pop-ups to export the document."),void s.remove();m.document.open(),m.document.write(d),m.document.close(),m.focus(),s.classList.remove("active"),setTimeout(()=>s.remove(),300)}catch(e){console.error("Export failed",e),alert("Export failed. Check console for details."),s.remove()}finally{E.disabled=!1,E.innerHTML=n}}(a)})})}),N&&C&&(N.addEventListener("click",()=>{C.classList.contains("mobile-open")?(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false")):(C.classList.add("mobile-open"),document.body.classList.add("menu-open"),N.setAttribute("aria-expanded","true"))}),g.addEventListener("click",e=>{if(window.innerWidth<=960){const t=e.target.closest(".nav-item, .nav-leaf, .nav-parent");t&&(t.classList.contains("nav-item")||t.classList.contains("nav-leaf"))&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false"))}}),document.addEventListener("click",e=>{window.innerWidth<=960&&C.classList.contains("mobile-open")&&!C.contains(e.target)&&!N.contains(e.target)&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false"))}));
|
|
1
|
+
import{MANIFEST as e,DEFAULT_SECTION as t,findSection as n,getAdjacentSections as a,SITE_CONFIG as s,EXPORT_CONFIG as o}from"./manifest.js";import{updateMetaTags as i}from"./seo.js";import{escapeRegExp as l,searchContentPage as c,flattenManifest as r,findPreferredIndex as d}from"./lib/search.js";import{resolveTarget as m,resolveEntry as p}from"./lib/router.js";import{composeExportDocument as u,collectExportableSections as v}from"./lib/export.js";import{renderMermaidBlocks as h}from"./mermaid-init.js";import{highlightCodeBlocks as f}from"./syntax-highlight.js";const g=document.getElementById("app"),y=document.getElementById("nav"),b=document.getElementById("year"),E=document.getElementById("exportBtn"),L=document.getElementById("commandToggle"),T=document.getElementById("commandPalette"),x=document.getElementById("commandInput"),N=document.getElementById("commandList"),w=document.getElementById("mobileMenuToggle"),C=document.querySelector(".sidebar"),k="docs-toolkit-command-query",$=new Map,H=new Map,S=new Map,I=new Set;let M=[],A=0,q=!1,P=(localStorage.getItem(k)||"").trim(),R=!1;function B(e,t){const n=document.createElement("a");return n.href=e.url,n.target="_blank",n.rel="noopener noreferrer",n.className=`${t} nav-external`,n.title=e.summary||e.title,n.innerHTML=`\n <span class="nav-title">${e.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,n}function D(e,t={}){const{scrollToHighlight:a=!1}=t,{targetId:s,parentId:o}=function(e){return m(e,n)}(e);q&&W(),o&&O(o,!0),R=a||Boolean(P),location.hash.replace("#","")===s?j():location.hash=`#${s}`}function F(){return location.hash.replace("#","")||t}async function j(){const e=F(),t=p(e,n);if(!t)return;const{entry:o,targetId:l,parentId:c}=t;l===e?(c&&O(c,!0),function(e,t=null){H.forEach(e=>{e.setAttribute("aria-current","false")});const n=H.get(e);if(n&&n.setAttribute("aria-current","page"),t){const e=H.get(t);e&&e.setAttribute("aria-current","page")}}(o.id,c),await async function(e){if(!e)return;const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)return void(g.innerHTML='<article class="section"><p>Section failed to load.</p></article>');const o=await n();g.innerHTML=o.html||"",function(e){if(!e||!e.showDate&&!e.showReadingTime&&!e.showSummary)return;const t=(g.querySelector(".doc-content")||g.querySelector("article, section")||g).querySelector("h1");if(!t)return;const n=[];e.showDate&&e.date&&n.push(function(e){const t=String(e||"").trim();if(!t)return"";const n=/^\d{4}-\d{2}-\d{2}$/.test(t)?`${t}T00:00:00Z`:t,a=new Date(n);return Number.isNaN(a.getTime())?t:new Intl.DateTimeFormat(void 0,{year:"numeric",month:"long",day:"numeric",timeZone:"UTC"}).format(a)}(e.date)),e.showReadingTime&&e.reading_time&&n.push(`${e.reading_time} min read`);let a=t;if(n.length>0){const e=document.createElement("p");e.className="doc-meta",e.textContent=n.join(" · "),t.after(e),a=e}if(e.showSummary&&e.summary){const t=document.createElement("p");t.className="doc-summary",t.textContent=e.summary,a.after(t)}}(e),await h(g),await f(g),function(e){const t=g.querySelector(".bottom-nav");if(t&&t.remove(),"never"===s.bottomNav)return;const n=(s.bottomNavSections||[]).some(t=>e.startsWith(t)),o="mobile"===s.bottomNav&&!n,{prev:i,next:l}=a(e);if(!i&&!l)return;const c=document.createElement("nav");if(c.className="bottom-nav",o&&c.classList.add("mobile-only"),i){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-prev",e.innerHTML='<span class="bottom-nav-chevron">‹</span>';const t=document.createElement("a");t.href=`#${i.id}`,t.className="bottom-nav-link",t.title=`Previous: ${i.title}`,t.textContent=i.title,t.addEventListener("click",e=>{e.preventDefault(),D(i.id)}),e.appendChild(t),c.appendChild(e)}else{const e=document.createElement("div");e.className="bottom-nav-spacer",c.appendChild(e)}if(l){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-next";const t=document.createElement("a");t.href=`#${l.id}`,t.className="bottom-nav-link",t.title=`Next: ${l.title}`,t.textContent=l.title,t.addEventListener("click",e=>{e.preventDefault(),D(l.id)}),e.appendChild(t),e.innerHTML+='<span class="bottom-nav-chevron">›</span>',c.appendChild(e)}(g.querySelector("section")||g).appendChild(c)}(e.id),g.scrollTop=0,window.scrollTo(0,0),"function"==typeof o.afterRender&&o.afterRender(g),i({title:e.title,description:e.summary,siteTitle:s.siteTitle,siteUrl:s.siteUrl,sectionId:e.id,ogImage:e.ogImage||s.ogImage}),$.set(e.id,Date.now());const l=R;R=!1,Q(l),requestAnimationFrame(()=>g.focus())}(o)):location.replace(`#${l}`)}function O(e,t){if(!e)return;const n=S.get(e);t?(I.add(e),n&&n.group.classList.add("expanded")):(I.delete(e),n&&n.group.classList.remove("expanded"))}function _(){if(!T||!x)return;q=!0,T.hidden=!1;const e=P;x.value=e,G(e),requestAnimationFrame(()=>{x.focus(),e&&x.select()})}function W(){T&&x&&(q=!1,T.hidden=!0,x.blur())}!function(){y.innerHTML="",H.clear(),S.clear();let t=I.size>0;e.forEach((e,n)=>{if(e.url){const t=B(e,"nav-leaf");return void y.appendChild(t)}if(e.subsections&&e.subsections.length){const n=document.createElement("div");n.className="nav-group";const a=Boolean(e.module),s=document.createElement("button");s.type="button",s.className="nav-parent"+(a?" nav-parent-with-content":""),s.dataset.section=e.id,s.title=e.summary,a?(s.innerHTML=`\n <span class="nav-title-link">${e.title}</span>\n <span class="nav-expand-toggle" aria-label="Expand"></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.querySelector(".nav-title-link").addEventListener("click",t=>{t.stopPropagation(),D(e.id,{scrollToHighlight:Boolean(P)})}),s.querySelector(".nav-expand-toggle").addEventListener("click",t=>{t.stopPropagation();const n=!I.has(e.id);O(e.id,n)}),s.addEventListener("click",t=>{if(t.target===s){const t=!I.has(e.id);O(e.id,t)}})):(s.innerHTML=`\n <span class="nav-title">${e.title}</span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)}));const o=document.createElement("div");o.className="nav-sublist",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void o.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-nested";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-nested",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-nested",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void a.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-deep";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-deep",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)});const s=document.createElement("div");s.className="nav-sublist nav-sublist-deep",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void s.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-ultra";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-ultra",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-ultra",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void a.appendChild(t)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-ultra"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),s.appendChild(t),H.set(e.id,n),S.set(e.id,{group:t,button:n,list:a});const o=I.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-deep"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),s.appendChild(t),H.set(e.id,t)}),t.append(n,s),a.appendChild(t),H.set(e.id,n),S.set(e.id,{group:t,button:n,list:s});const o=I.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-nested"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),o.appendChild(t),H.set(e.id,n),S.set(e.id,{group:t,button:n,list:a});const s=I.has(e.id)&&!e.collapsed;return void O(e.id,s)}const t=document.createElement("button");t.type="button",t.className="nav-item"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),o.appendChild(t),H.set(e.id,t)}),n.append(s,o),y.appendChild(n),H.set(e.id,s),S.set(e.id,{group:n,button:s,list:o});const i=!e.collapsed&&(I.has(e.id)||!t&&!I.size);O(e.id,i),i&&(t=!0)}else{const t=document.createElement("button");t.type="button",t.className="nav-leaf"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),y.appendChild(t),H.set(e.id,t)}})}(),P&&(R=!0),window.addEventListener("hashchange",()=>{P&&(R=!0),j()}),b.textContent=(new Date).getFullYear(),j(),L&&T&&x&&N&&(L.addEventListener("click",()=>{q?W():_()}),x.addEventListener("input",()=>{const e=x.value;Z(e,!0),G(e)}),x.addEventListener("keydown",e=>{const t=M.length-1;if("ArrowDown"===e.key)e.preventDefault(),A=Math.min(t,A+1),K();else if("ArrowUp"===e.key)e.preventDefault(),A=Math.max(0,A-1),K();else if("Enter"===e.key){e.preventDefault();const t=M[A];t&&(Z(x.value,!0),D(t.id,{scrollToHighlight:!0}),W())}else"Escape"===e.key&&(e.preventDefault(),W())}),N.addEventListener("click",e=>{const t=e.target.closest("[data-section]");if(!t)return;const n=t.dataset.section;n&&(Z(x.value,!0),D(n,{scrollToHighlight:!0}),W())}),T.addEventListener("click",e=>{e.target===T&&W()}),N.addEventListener("scroll",()=>{X.loading||X.complete||N.scrollTop+N.clientHeight>=N.scrollHeight-48&&J(!1)}),window.addEventListener("keydown",e=>{const t=e.target,n=t&&("INPUT"===t.tagName||"TEXTAREA"===t.tagName||t.isContentEditable),a=e.metaKey||e.ctrlKey;"k"===e.key.toLowerCase()&&a||"/"===e.key&&!n?(e.preventDefault(),q?W():_()):"Escape"===e.key&&q&&(e.preventDefault(),W())}));let U=null,V=!1;const z=25;let X={query:"",offset:0,total:0,complete:!0,loading:!1};async function G(e){N&&(X={query:e,offset:0,total:0,complete:!1,loading:!1},M=[],!V&&e.trim()&&(V=!0,N.innerHTML='<li class="cmd-item cmd-loading">Indexing content...</li>'),clearTimeout(U),U=setTimeout(async()=>{await J(!0);const e=F();A=d(M,e),K(),V=!1},e.trim()?150:0))}async function J(t=!1){if(X.loading)return;if(!t&&X.complete)return;const n=X.query;let a;X.loading=!0;try{a=await c(e,n,{offset:X.offset,limit:z})}catch{return void(X.loading=!1)}n===X.query?(M=t?a.items:M.concat(a.items),X.offset=M.length,X.total=a.total,X.complete=a.complete||0===a.items.length,X.loading=!1,function(){if(N){if(N.innerHTML="",!M.length){const e=document.createElement("li");return e.className="cmd-item",e.setAttribute("aria-selected","false"),e.textContent="No matches.",void N.appendChild(e)}if(M.forEach(e=>{const t=document.createElement("li");t.className="cmd-item",t.dataset.section=e.id,t.setAttribute("role","option");const n=document.createElement("span");if(n.className="cmd-item-title",n.textContent=e.title,e.group){const t=document.createElement("span");t.className="cmd-item-group",t.textContent=e.group,n.prepend(t)}const a=document.createElement("span");if(a.className="cmd-item-summary",a.textContent=e.summary||"",t.append(n,a),e.searchSnippet&&e.searchSnippet!==e.summary){const n=document.createElement("span");n.className="cmd-item-snippet",n.textContent=e.searchSnippet,t.appendChild(n)}if("number"==typeof e.searchRank&&e.searchRank>0){const n=document.createElement("span");n.className="cmd-item-score",n.textContent=`Rank ${e.searchRank}`,t.appendChild(n)}N.appendChild(t)}),!X.complete&&X.total>M.length){const e=document.createElement("li");e.className="cmd-item cmd-more",e.setAttribute("aria-selected","false"),e.setAttribute("role","presentation"),e.textContent=`Showing ${M.length} of ${X.total} — scroll for more`,N.appendChild(e)}}}()):X.loading=!1}function K(){N&&Array.from(N.children).forEach((e,t)=>{const n=t===A&&M.length;e.setAttribute("aria-selected",n?"true":"false"),n&&e.scrollIntoView({block:"nearest"})})}function Y(e){const t=document.createElement("div");t.innerHTML=e,t.querySelectorAll("script").forEach(e=>e.remove()),t.querySelectorAll("button").forEach(e=>e.removeAttribute("onclick")),t.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)});const n=t.querySelector("section");return n?n.innerHTML:t.innerHTML}function Z(e,t=!1){P=e.trim(),t&&(P?localStorage.setItem(k,P):localStorage.removeItem(k)),Q()}function Q(e=!1){g&&function(e,t,{scrollToFirst:n=!1}={}){if(!e)return;if(function(e){e&&e.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)})}(e),!t)return;const a=t.split(/\s+/).map(e=>e.trim()).filter(Boolean);if(!a.length)return;const s=a.map(e=>e.toLowerCase()),o=new Set(["SCRIPT","STYLE","CODE","PRE"]),i=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,{acceptNode(e){if(!e.nodeValue||!e.nodeValue.trim())return NodeFilter.FILTER_REJECT;const t=e.parentNode;return t&&o.has(t.tagName)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}),c=[];let r;for(;r=i.nextNode();){const e=r.nodeValue.toLowerCase();s.some(t=>e.includes(t))&&c.push(r)}const d=new RegExp(`(${a.map(l).join("|")})`,"gi");c.forEach(e=>{const t=e.nodeValue,n=[];let a=0;t.replace(d,(e,s,o)=>{o>a&&n.push(document.createTextNode(t.slice(a,o)));const i=document.createElement("mark");return i.className="hl",i.textContent=e,n.push(i),a=o+e.length,e}),a<t.length&&n.push(document.createTextNode(t.slice(a)));const s=document.createDocumentFragment();n.forEach(e=>s.appendChild(e)),e.parentNode.replaceChild(s,e)}),n&&requestAnimationFrame(()=>{const t=e.querySelector("mark.hl");t&&t.scrollIntoView({behavior:"smooth",block:"center"})})}(g,P,{scrollToFirst:e})}E&&E.addEventListener("click",function(){const t=document.createElement("div");t.className="export-options-overlay",t.innerHTML='\n <div class="export-options-modal">\n <div class="export-options-header">EXPORT OPTIONS</div>\n <div class="export-options-buttons">\n <button type="button" class="export-option-btn" data-scope="page">\n <span class="export-option-title">Current Page</span>\n <span class="export-option-desc">Export only this section</span>\n </button>\n <button type="button" class="export-option-btn" data-scope="site">\n <span class="export-option-title">Entire Site</span>\n <span class="export-option-desc">Export all documentation</span>\n </button>\n </div>\n <button type="button" class="export-cancel-btn">Cancel</button>\n </div>\n ',document.body.appendChild(t),setTimeout(()=>t.classList.add("active"),10);const n=()=>{t.classList.remove("active"),setTimeout(()=>t.remove(),200)};t.querySelector(".export-cancel-btn").addEventListener("click",n),t.addEventListener("click",e=>{e.target===t&&n()}),t.querySelectorAll(".export-option-btn").forEach(t=>{t.addEventListener("click",()=>{const a=t.dataset.scope;n(),async function(t="site"){if(!E)return;const n=E.innerHTML,a=window.open("","_blank","width=1,height=1,left=0,top=0");if(!a||a.closed||void 0===a.closed)return void confirm("Pop-ups are blocked. Please allow pop-ups for this site to export the document.\n\nWould you like to try again after enabling pop-ups?");a.close(),E.disabled=!0;const s=document.createElement("div");s.className="export-loading-overlay",s.innerHTML='\n <div class="export-loading-modal">\n <div class="export-loading-header">\n <div class="export-loading-title">COMPILING DOCUMENTATION</div>\n <div class="export-loading-subtitle">Assembling all sections into unified document</div>\n </div>\n <div class="export-loading-progress">\n <div class="export-loading-bar">\n <div class="export-loading-fill"></div>\n </div>\n <div class="export-loading-status-container">\n <div class="export-loading-status">Initializing...</div>\n </div>\n </div>\n <div class="export-loading-scanner">\n <div class="scanner-line"></div>\n </div>\n </div>\n ',document.body.appendChild(s),setTimeout(()=>s.classList.add("active"),10);const i=s.querySelector(".export-loading-fill"),l=s.querySelector(".export-loading-status");try{let n;if("page"===t){const t=F(),a=v(e).find(e=>e.id===t);n=a?[a]:[]}else n=v(e);if(0===n.length)return alert("No content available to export."),s.remove(),void(E.disabled=!1);const a=[],c=n.length;let r=0;for(const e of n){r++;const n=r/c*100;i.style.width=`${n}%`,l.textContent="page"===t?`Exporting: ${e.title}`:`Processing section ${r} of ${c}: ${e.title}`,await new Promise(e=>setTimeout(e,50));try{const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)continue;const s=Y((await n()).html||"");a.push({section:e,html:s})}catch(t){console.error("Failed to include section in export",e.id,t)}}l.textContent="Generating document...",await new Promise(e=>setTimeout(e,200));const d=u(a,o);l.textContent="Opening document viewer...",await new Promise(e=>setTimeout(e,100));const m=window.open("","_blank","width=900,height=860,scrollbars=yes,resizable=yes");if(!m)return alert("Please allow pop-ups to export the document."),void s.remove();m.document.open(),m.document.write(d),m.document.close(),m.focus(),s.classList.remove("active"),setTimeout(()=>s.remove(),300)}catch(e){console.error("Export failed",e),alert("Export failed. Check console for details."),s.remove()}finally{E.disabled=!1,E.innerHTML=n}}(a)})})}),w&&C&&(w.addEventListener("click",()=>{C.classList.contains("mobile-open")?(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false")):(C.classList.add("mobile-open"),document.body.classList.add("menu-open"),w.setAttribute("aria-expanded","true"))}),y.addEventListener("click",e=>{if(window.innerWidth<=960){const t=e.target.closest(".nav-item, .nav-leaf, .nav-parent");t&&(t.classList.contains("nav-item")||t.classList.contains("nav-leaf"))&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false"))}}),document.addEventListener("click",e=>{window.innerWidth<=960&&C.classList.contains("mobile-open")&&!C.contains(e.target)&&!w.contains(e.target)&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false"))}));
|
package/site/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<meta name="description" content="Pagenary developer documentation — building, configuring, deploying, and extending the multi-tenant documentation publisher, published with Pagenary itself." />
|
|
8
8
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
9
9
|
<link rel="stylesheet" href="/styles.css" />
|
|
10
|
-
<meta name="x-build" content="2026-06-
|
|
10
|
+
<meta name="x-build" content="2026-06-15T18:50:34.132Z" />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<a class="skip-link" href="#app">Skip to content</a>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const FORTEMI_INDEX_SCHEMA="aiwg.fortemi.index.export.v1";export const FORTEMI_RECORD_SCHEMA="aiwg.fortemi.index.record.v1";export const FORTEMI_CHUNK_MANIFEST_SCHEMA="aiwg.fortemi.index.chunk-manifest.v1";export const FORTEMI_CHUNK_PART_SCHEMA="aiwg.fortemi.index.chunk.v1";export const DEFAULT_PART_SIZE=100;export function stableHash(e){let t=0xcbf29ce484222325n;for(let r=0;r<e.length;r+=1)t^=BigInt(255&e.charCodeAt(r)),t=1099511628211n*t&18446744073709551615n;return t.toString(16).padStart(16,"0")}export function stripHtml(e){return e?String(e).replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi," ").replace(/<[^>]+>/g," ").replace(/ /gi," ").replace(/&/gi,"&").replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'").replace(/—/gi,"—").replace(/…/gi,"…").replace(/\s+/g," ").trim():""}export function normalizeFacetValue(e){return String(e||"").trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")||"general"}export function recordToSectionId(e){if(!e)return null;const t=e.source?.locator||"",r=/^#\/?(.+)$/.exec(t);if(r)return r[1];const o=e.facets?.section?.[0];return o||e.id?.replace(/^docs:page:/,"")||null}export function sectionToFortemiRecord(e,t,r){const o=String(e.group||e.title||"Documentation").split(">").map(e=>e.trim()).filter(Boolean),i=e.module||"",a=i?i.replace(/^\.?\//,"").replace(/^sections\//,"sections/"):`${e.id}.md`,n=Array.from(new Set([...o.map(normalizeFacetValue),e.type?normalizeFacetValue(e.type):null].filter(Boolean))),c=t&&t.trim()?t.trim():`${e.title||""} ${e.summary||""}`.trim();return{schema_version:FORTEMI_RECORD_SCHEMA,id:`docs:page:${e.id}`,type:"docs.page",source:{path:a,repo_relative_path:a,locator:`#/${e.id}`},title:e.title||e.id,text:c,facets:{section:[e.id],group:o.length?o.map(normalizeFacetValue):["documentation"]},tags:e.type?[normalizeFacetValue(e.type)]:[],concepts:n,relationships:[],provenance:[{field:"text",source:a,path:"$.text",confidence:"source",privacy:"public"}],privacy:{classification:"public",pii:!1},updated_at:r}}export function buildFortemiIndexExport(e,t={}){const r=t.repo||"pagenary",o=new Map;for(const{section:t,text:r}of e)t&&t.id&&(o.has(t.id)||o.set(t.id,{section:t,text:r||""}));const i=Array.from(o.values()).sort((e,t)=>`docs:page:${e.section.id}`.localeCompare(`docs:page:${t.section.id}`)),a=stableHash(`${r}${i.map(({section:e,text:t})=>`${e.id}\0${e.title||""}\0${t||""}`).join("")}`),n=Number(BigInt(`0x${a.slice(0,8)}`)),c=new Date(1e3*n).toISOString(),s=i.map(({section:e,text:t})=>sectionToFortemiRecord(e,t,e.date||c));return{index:{schema_version:FORTEMI_INDEX_SCHEMA,generated_at:c,source:{repo:r,privacy:"public",build_hash:a},items:s},buildHash:a,generatedAt:c}}export function computeFacetCounts(e){const t={},r=(e,r)=>{null!=r&&(t[e]||={},t[e][r]=(t[e][r]||0)+1)};for(const t of e)r("type",t.type),r("privacy",t.privacy?.classification),(t.tags||[]).forEach(e=>r("tag",e)),(t.concepts||[]).forEach(e=>r("concept",e)),Object.entries(t.facets||{}).forEach(([e,t])=>(t||[]).forEach(t=>r(e,t)));return t}export function chunkFortemiIndex(e,t={}){const r=Math.max(1,t.partSize||100),o=t.partHref||(e=>`part-${String(e).padStart(4,"0")}.json`),i=e.items||[],a=[],n=[];let c=0,s=0;do{const e=i.slice(c,c+r),t=o(s);a.push({schema_version:FORTEMI_CHUNK_PART_SCHEMA,manifest_schema_version:FORTEMI_CHUNK_MANIFEST_SCHEMA,offset:c,items:e}),n.push({href:t,offset:c,count:e.length}),c+=e.length||r,s+=1}while(c<i.length);return{manifest:{schema_version:FORTEMI_CHUNK_MANIFEST_SCHEMA,generated_at:e.generated_at,source:e.source,total:i.length,part_size:r,facets:computeFacetCounts(i),parts:n},parts:a}}
|
package/site/lib/search.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
let
|
|
1
|
+
import{queryAiwgFortemiIndex as t,getAiwgFortemiFacets as e,createAiwgIndexController as n,createAiwgFetchChunkLoader as r,aiwgFortemiIndexToCommunityGraph as i,validateAiwgFortemiChunkManifest as o,validateAiwgFortemiIndexExport as a}from"../vendor/fortemi-aiwg-index.js";import{buildFortemiIndexExport as s,recordToSectionId as l}from"./fortemi-corpus.js";export{t as queryAiwgFortemiIndex,e as getAiwgFortemiFacets,n as createAiwgIndexController,r as createAiwgFetchChunkLoader,i as aiwgFortemiIndexToCommunityGraph};let u=null,c=null,f=!1,m=null,p=null,d=null,h=null;export function escapeRegExp(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export function flattenManifest(t,e=""){const n=[];for(const r of t){const t=e?`${e} > ${r.title}`:r.title,i=Array.isArray(r.subsections)&&r.subsections.length>0;if(!r.module&&i||n.push({...r,group:e||r.title}),i){const e=flattenManifest(r.subsections,t);n.push(...e)}}return n}function y(t,e){const n=l(t.item);return n?{...e.get(n)||{id:n,title:t.item.title,summary:""},searchRank:t.rank,searchSnippet:t.snippet,searchMatches:t.matches||[]}:null}export function filterSections(t,e){const n=flattenManifest(t),r=e.trim().toLowerCase();return r?n.filter(t=>`${t.title||""} ${t.summary||""} ${t.group||""}`.toLowerCase().includes(r)):n}export async function searchContentPage(e,i,a={}){const l=Math.max(0,a.offset||0),g=Math.max(1,a.limit||25),x=function(t){if(d&&h===t)return d;d=new Map;for(const e of flattenManifest(t))e&&e.id&&!d.has(e.id)&&d.set(e.id,e);return h=t,d}(e),w={types:["docs.page"],rank:!0,snippets:!0,includeMatches:!0,snippetLength:140,limit:g,offset:l},M=await async function(){if(u)return u;if(f)return null;if(c)return c;c=(async()=>{const t=function(){if("undefined"==typeof document||"undefined"==typeof URL)return null;try{return new URL("search-index/",document.baseURI).toString()}catch{return null}}();if(!t||"function"!=typeof fetch)return null;try{const e=await fetch(new URL("manifest.json",t).toString());if(!e.ok)return null;const i=await e.json();if(!o(i).valid)return null;const a=n();return a.loadChunkedIndex(i,r(t),{maxCachedParts:4}),u=a,a}catch{return null}})();const t=await c;return t||(f=!0),c=null,t}();if(M)try{const t=await M.queryChunked(i,{...w,onProgress:a.onProgress}),e=(t.rankedItems||[]).map(t=>y(t,x)).filter(Boolean);return{items:e,total:t.total,offset:l,limit:g,complete:l+e.length>=t.total,source:"static"}}catch{}const $=await async function(t){return m||p||(p=(async()=>{const e=flattenManifest(t),n=await Promise.all(e.map(async t=>{let e="";try{if(t.module){const n=t.module.replace("./","../"),r=await import(n);r.load&&(e=function(t){if("undefined"==typeof document)return"";const e=document.createElement("div");return e.innerHTML=t,e.textContent||e.innerText||""}((await r.load()).html||""))}}catch{}return{section:t,text:`${t.title||""} ${t.summary||""} ${t.group||""} ${e}`.trim()}})),{index:r}=s(n,{repo:"pagenary"}),i=new Map(e.map(t=>[t.id,t]));return m={index:r,byId:i},m})(),p)}(e);if(!i.trim()){const t=Array.from($.byId.values()),e=t.slice(l,l+g);return{items:e,total:t.length,offset:l,limit:g,complete:l+e.length>=t.length,source:"legacy"}}const C=t($.index,i,w),I=(C.rankedItems||[]).map(t=>y(t,$.byId)).filter(Boolean);return{items:I,total:C.total,offset:l,limit:g,complete:l+I.length>=C.total,source:"legacy"}}export async function searchContent(t,e,n={}){return e.trim()?(await searchContentPage(t,e,{limit:25,...n})).items:(await searchContentPage(t,"",{offset:0,limit:Number.MAX_SAFE_INTEGER,...n})).items}export function buildCommunityGraph(t,e={}){const n=flattenManifest(t).map(t=>({section:t,text:`${t.title||""} ${t.summary||""}`.trim()})),{index:r}=s(n,{repo:"pagenary"});return a(r).valid?i(r,e):{nodes:[],edges:[],communities:[]}}export function parseSearchTerms(t){return t.split(/\s+/).map(t=>t.trim()).filter(Boolean)}export function findPreferredIndex(t,e){const n=t.findIndex(t=>t.id===e);return n>=0?n:0}export function resetSearchState(){u=null,c=null,f=!1,m=null,p=null,d=null,h=null}
|
package/site/pages/api.html
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"headline": "API Reference",
|
|
28
28
|
"description": "Module-level documentation for the publisher internals.",
|
|
29
29
|
"url": "https://docs.pagenary.com/pages/api.html",
|
|
30
|
-
"dateModified": "2026-06-
|
|
30
|
+
"dateModified": "2026-06-15",
|
|
31
31
|
"mainEntityOfPage": {
|
|
32
32
|
"@type": "WebPage",
|
|
33
33
|
"@id": "https://docs.pagenary.com/pages/api.html"
|
|
@@ -164,8 +164,19 @@ await loadSection(section);</code></pre>
|
|
|
164
164
|
});</code></pre>
|
|
165
165
|
<hr>
|
|
166
166
|
<h2 id="library-modules">Library Modules</h2>
|
|
167
|
-
<h3 id="libsearchjs-
|
|
168
|
-
<p>Search
|
|
167
|
+
<h3 id="libsearchjs-fortemi-backed-search">lib/search.js - Fortemi-backed search</h3>
|
|
168
|
+
<p>Search runs on the <strong>real, vendored `@fortemi/core` static-index engine</strong></p>
|
|
169
|
+
<p>(`src/vendor/fortemi-aiwg-index.js`). At build time, `scripts/build-tenants.js`</p>
|
|
170
|
+
<p>emits a deterministic <strong>chunked</strong> index per tenant under `dist/<tenant>/search-index/`</p>
|
|
171
|
+
<p>(`manifest.json` + `part-NNNN.json`, the `aiwg.fortemi.index.*.v1` contract). At</p>
|
|
172
|
+
<p>runtime the adapter loads that index through an index controller +</p>
|
|
173
|
+
<p>fetch chunk-loader: parts are fetched lazily and cached (<strong>precache</strong>), results</p>
|
|
174
|
+
<p>are ranked with snippets, and pages are returned by offset for <strong>infinite scroll</strong>.</p>
|
|
175
|
+
<p>If the static index is missing or invalid, the adapter falls back to an</p>
|
|
176
|
+
<p>in-browser index built from section modules — same ranking engine, same result</p>
|
|
177
|
+
<p>shape. See `.aiwg/architecture/adr/ADR-015-fortemi-core-search-adapter.md`.</p>
|
|
178
|
+
<p>Build-time and fallback share the deterministic corpus builder in</p>
|
|
179
|
+
<p>`lib/fortemi-corpus.js`.</p>
|
|
169
180
|
<h4 id="functions-3">Functions</h4>
|
|
170
181
|
<p><strong>`escapeRegExp(value: string): string`</strong></p>
|
|
171
182
|
<p>Escape special regex characters.</p>
|
|
@@ -174,19 +185,34 @@ await loadSection(section);</code></pre>
|
|
|
174
185
|
<p>Flatten nested manifest into searchable sections.</p>
|
|
175
186
|
<pre><code class="language-javascript">const flat = flattenManifest(MANIFEST);
|
|
176
187
|
// Returns all navigable sections with group info</code></pre>
|
|
177
|
-
<p><strong>`buildSearchIndex(manifest: SectionEntry[]): Promise<IndexedSection[]>`</strong></p>
|
|
178
|
-
<p>Build search index by loading all section modules. Cached after first call.</p>
|
|
179
|
-
<pre><code class="language-javascript">const index = await buildSearchIndex(MANIFEST);
|
|
180
|
-
// Each entry has searchContent: lowercase text for matching</code></pre>
|
|
181
188
|
<p><strong>`filterSections(manifest: SectionEntry[], query: string): FlatSection[]`</strong></p>
|
|
182
189
|
<p>Synchronous title/summary search (no content).</p>
|
|
183
190
|
<pre><code class="language-javascript">const results = filterSections(MANIFEST, 'setup');</code></pre>
|
|
184
|
-
<p><strong>`
|
|
185
|
-
<p>
|
|
186
|
-
<pre><code class="language-javascript">const
|
|
187
|
-
//
|
|
191
|
+
<p><strong>`searchContentPage(manifest, query, options?): Promise<SearchPage>`</strong></p>
|
|
192
|
+
<p>Paged full-text search — the primary entry point, used for infinite scroll.</p>
|
|
193
|
+
<pre><code class="language-javascript">const page = await searchContentPage(MANIFEST, 'auth', { offset: 0, limit: 25 });
|
|
194
|
+
// page = { items, total, offset, limit, complete, source: 'static' | 'legacy' }
|
|
195
|
+
// items: section objects with searchRank, searchSnippet, searchMatches</code></pre>
|
|
196
|
+
<p><strong>`searchContent(manifest, query, options?): Promise<Section[]>`</strong></p>
|
|
197
|
+
<p>First-page convenience wrapper (back-compatible array). Empty query returns all</p>
|
|
198
|
+
<p>sections.</p>
|
|
199
|
+
<pre><code class="language-javascript">const results = await searchContent(MANIFEST, 'authentication');</code></pre>
|
|
200
|
+
<p><strong>`buildCommunityGraph(manifest, options?): { nodes, edges, communities }`</strong></p>
|
|
201
|
+
<p>Project the corpus into a Fortemi community graph (relationships/facets) — the</p>
|
|
202
|
+
<p>"graph" capability, no full-text required.</p>
|
|
203
|
+
<pre><code class="language-javascript">const graph = buildCommunityGraph(MANIFEST);</code></pre>
|
|
188
204
|
<p><strong>`findPreferredIndex(entries: Section[], currentId: string): number`</strong></p>
|
|
189
205
|
<p>Find index of current section in filtered results.</p>
|
|
206
|
+
<p>Re-exported from the vendored engine for advanced use:</p>
|
|
207
|
+
<p>`queryAiwgFortemiIndex`, `getAiwgFortemiFacets`, `createAiwgIndexController`,</p>
|
|
208
|
+
<p>`createAiwgFetchChunkLoader`, `aiwgFortemiIndexToCommunityGraph`.</p>
|
|
209
|
+
<hr>
|
|
210
|
+
<h3 id="libfortemi-corpusjs-deterministic-corpus-builder">lib/fortemi-corpus.js - Deterministic corpus builder</h3>
|
|
211
|
+
<p>Pure, DOM-free, `Date.now()`-free helpers shared by the build-time generator and</p>
|
|
212
|
+
<p>the runtime fallback: `buildFortemiIndexExport(entries, { repo })` (records sorted</p>
|
|
213
|
+
<p>by id, deduped, content-hashed `generated_at` + `source.build_hash`),</p>
|
|
214
|
+
<p>`chunkFortemiIndex(index, { partSize })`, `sectionToFortemiRecord`, `stripHtml`,</p>
|
|
215
|
+
<p>`recordToSectionId`, `stableHash`.</p>
|
|
190
216
|
<hr>
|
|
191
217
|
<h3 id="librouterjs-hash-routing">lib/router.js - Hash Routing</h3>
|
|
192
218
|
<p>URL hash parsing and resolution.</p>
|
|
@@ -291,8 +317,19 @@ interface FlatSection extends SectionEntry {
|
|
|
291
317
|
group?: string; // Parent group title
|
|
292
318
|
}
|
|
293
319
|
|
|
294
|
-
interface
|
|
295
|
-
|
|
320
|
+
interface SearchResultSection extends FlatSection {
|
|
321
|
+
searchRank?: number; // Relevance rank from the Fortemi engine
|
|
322
|
+
searchSnippet?: string; // Highlighted-context snippet
|
|
323
|
+
searchMatches?: { field: string; value: string }[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface SearchPage {
|
|
327
|
+
items: SearchResultSection[];
|
|
328
|
+
total: number;
|
|
329
|
+
offset: number;
|
|
330
|
+
limit: number;
|
|
331
|
+
complete: boolean; // true when no further pages remain
|
|
332
|
+
source: 'static' | 'legacy';
|
|
296
333
|
}
|
|
297
334
|
|
|
298
335
|
interface SiteConfig {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"headline": "Architecture",
|
|
28
28
|
"description": "The static SPA pattern, build pipeline, and tenant content model.",
|
|
29
29
|
"url": "https://docs.pagenary.com/pages/architecture.html",
|
|
30
|
-
"dateModified": "2026-06-
|
|
30
|
+
"dateModified": "2026-06-15",
|
|
31
31
|
"mainEntityOfPage": {
|
|
32
32
|
"@type": "WebPage",
|
|
33
33
|
"@id": "https://docs.pagenary.com/pages/architecture.html"
|
|
@@ -170,6 +170,25 @@
|
|
|
170
170
|
<ul>
|
|
171
171
|
<li>optional `feed.xml` when `feed: true`</li>
|
|
172
172
|
</ul>
|
|
173
|
+
<h3 id="search-index-generation">Search Index Generation</h3>
|
|
174
|
+
<p>After `manifest.js` and `sections/` are materialized (both the nested-content and</p>
|
|
175
|
+
<p>legacy-manifest paths), `scripts/lib/search-index-generator.js` emits a static</p>
|
|
176
|
+
<p><strong>Fortemi</strong> search index per tenant. It imports each section module, extracts</p>
|
|
177
|
+
<p>plain text, and writes a deterministic chunked index under</p>
|
|
178
|
+
<p>`dist/<tenant>/search-index/`:</p>
|
|
179
|
+
<ul>
|
|
180
|
+
<li>`manifest.json` — `aiwg.fortemi.index.chunk-manifest.v1` (totals, facet counts,</li>
|
|
181
|
+
</ul>
|
|
182
|
+
<p>contiguous part refs, and a content-derived `source.build_hash`)</p>
|
|
183
|
+
<ul>
|
|
184
|
+
<li>`part-NNNN.json` — `aiwg.fortemi.index.chunk.v1` record pages</li>
|
|
185
|
+
</ul>
|
|
186
|
+
<p>The corpus contract and chunking live in the pure, DOM-free</p>
|
|
187
|
+
<p>`src/lib/fortemi-corpus.js` (shared with the runtime fallback); the validated</p>
|
|
188
|
+
<p>record/index shape comes from the vendored `@fortemi/core` engine</p>
|
|
189
|
+
<p>(`src/vendor/fortemi-aiwg-index.js`). Generation is deterministic — repeat builds</p>
|
|
190
|
+
<p>are byte-identical — and failure is non-fatal (search degrades to the in-browser</p>
|
|
191
|
+
<p>fallback). See `.aiwg/architecture/adr/ADR-015-fortemi-core-search-adapter.md`.</p>
|
|
173
192
|
<h3 id="build-flow">Build Flow</h3>
|
|
174
193
|
<pre><code class="language-text">resolve source
|
|
175
194
|
-> run base build
|
|
@@ -177,6 +196,7 @@
|
|
|
177
196
|
-> apply overrides
|
|
178
197
|
-> apply branding/theme/navigation/welcome
|
|
179
198
|
-> copy .public assets
|
|
199
|
+
-> generate Fortemi search index (search-index/)
|
|
180
200
|
-> generate SEO artifacts
|
|
181
201
|
-> generate collection manifests/feeds
|
|
182
202
|
-> copy or sync to target</code></pre>
|
|
@@ -202,7 +222,8 @@
|
|
|
202
222
|
├── mermaid-init.js # Diagram rendering
|
|
203
223
|
├── syntax-highlight.js # Code highlighting
|
|
204
224
|
└── lib/
|
|
205
|
-
├── search.js #
|
|
225
|
+
├── search.js # Fortemi-backed search adapter (ranking, paging, fallback)
|
|
226
|
+
├── fortemi-corpus.js # Deterministic corpus builder (shared with build)
|
|
206
227
|
├── router.js # Hash routing utilities
|
|
207
228
|
└── export.js # Document export
|
|
208
229
|
|
|
@@ -232,18 +253,19 @@ function handleRoute() {
|
|
|
232
253
|
await loadSection(section);
|
|
233
254
|
}</code></pre>
|
|
234
255
|
<h3 id="search-libsearchjs">Search (lib/search.js)</h3>
|
|
235
|
-
<p>
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
256
|
+
<p>Ranked full-text search on the vendored `@fortemi/core` static-index engine. The</p>
|
|
257
|
+
<p>primary path loads the build-time chunked index from `search-index/` through an</p>
|
|
258
|
+
<p>index controller + fetch chunk-loader (lazy part fetch, in-memory part cache),</p>
|
|
259
|
+
<p>ranks with snippets, and returns results by offset for infinite scroll. If the</p>
|
|
260
|
+
<p>static index is missing/invalid it falls back to an in-browser index built from</p>
|
|
261
|
+
<p>section modules — same engine, same result shape.</p>
|
|
262
|
+
<pre><code class="language-javascript">// Primary: fetch the chunk manifest once (precache), then page through results.
|
|
263
|
+
const page = await searchContentPage(MANIFEST, query, { offset, limit: 25 });
|
|
264
|
+
// page = { items, total, offset, limit, complete, source: 'static' | 'legacy' }
|
|
265
|
+
// items: section objects carrying searchRank, searchSnippet, searchMatches</code></pre>
|
|
266
|
+
<p>The chunked index is emitted at build time (see "Search Index Generation"); the</p>
|
|
267
|
+
<p>runtime engine and the build-time corpus builder (`lib/fortemi-corpus.js`) share</p>
|
|
268
|
+
<p>the `@fortemi/core` `aiwg.fortemi.index.*.v1` contract.</p>
|
|
247
269
|
<h3 id="mermaid-integration-mermaid-initjs">Mermaid Integration (mermaid-init.js)</h3>
|
|
248
270
|
<p>Lazy-loaded diagram rendering:</p>
|
|
249
271
|
<pre><code class="language-javascript">export async function renderMermaidBlocks(container) {
|
|
@@ -312,7 +334,7 @@ tenant-b.example.com → dist/tenant-b/</code></pre>
|
|
|
312
334
|
<p>1. <strong>Critical Path</strong> - Shell + manifest + first section</p>
|
|
313
335
|
<p>2. <strong>Lazy Load</strong> - Other sections on navigation</p>
|
|
314
336
|
<p>3. <strong>On-Demand</strong> - Mermaid/Prism when needed</p>
|
|
315
|
-
<p>4. <strong>
|
|
337
|
+
<p>4. <strong>Precached</strong> - Search index chunks fetched on first palette open and cached in memory</p>
|
|
316
338
|
<h2 id="extensibility-points">Extensibility Points</h2>
|
|
317
339
|
<h3 id="custom-page-types">Custom Page Types</h3>
|
|
318
340
|
<p>Add to `section-templates.js`:</p>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"headline": "Deployment",
|
|
28
28
|
"description": "Hosting the static output and multi-tenant domain routing.",
|
|
29
29
|
"url": "https://docs.pagenary.com/pages/deployment.html",
|
|
30
|
-
"dateModified": "2026-06-
|
|
30
|
+
"dateModified": "2026-06-15",
|
|
31
31
|
"mainEntityOfPage": {
|
|
32
32
|
"@type": "WebPage",
|
|
33
33
|
"@id": "https://docs.pagenary.com/pages/deployment.html"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"headline": "Developer Guide",
|
|
28
28
|
"description": "Project layout, scripts, and the content authoring workflow.",
|
|
29
29
|
"url": "https://docs.pagenary.com/pages/developer-guide.html",
|
|
30
|
-
"dateModified": "2026-06-
|
|
30
|
+
"dateModified": "2026-06-15",
|
|
31
31
|
"mainEntityOfPage": {
|
|
32
32
|
"@type": "WebPage",
|
|
33
33
|
"@id": "https://docs.pagenary.com/pages/developer-guide.html"
|
|
@@ -118,7 +118,7 @@ npm run dev</code></pre>
|
|
|
118
118
|
<p>2. <strong>External Links</strong> - Links to external URLs (http/https) automatically open in new tab with security attributes. Use `url` property in manifest for external nav links.</p>
|
|
119
119
|
<p>3. <strong>Internal Linking</strong> - Link between sections with `#section-id` syntax. Build validates that all internal links reference existing sections.</p>
|
|
120
120
|
<p>4. <strong>Bottom Navigation</strong> - Configurable via `bottomNav` in root manifest. Options: "mobile" (default), "always", or "never".</p>
|
|
121
|
-
<p>5. <strong>Command Palette</strong> - Press Ctrl+K (or Cmd+K) to search and navigate sections.
|
|
121
|
+
<p>5. <strong>Command Palette</strong> - Press Ctrl+K (or Cmd+K) to search and navigate sections. Ranked full-text search (titles, summaries, and rendered content) via the vendored `@fortemi/core` static index, with snippets and infinite scroll.</p>
|
|
122
122
|
<p>See TENANT-CONFIG.md for full configuration details and examples.</p>
|
|
123
123
|
<h2 id="common-tasks">Common Tasks</h2>
|
|
124
124
|
<ul>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"headline": "Extending",
|
|
28
28
|
"description": "Add section templates, content types, and build behaviors.",
|
|
29
29
|
"url": "https://docs.pagenary.com/pages/extending.html",
|
|
30
|
-
"dateModified": "2026-06-
|
|
30
|
+
"dateModified": "2026-06-15",
|
|
31
31
|
"mainEntityOfPage": {
|
|
32
32
|
"@type": "WebPage",
|
|
33
33
|
"@id": "https://docs.pagenary.com/pages/extending.html"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"headline": "Quickstart",
|
|
28
28
|
"description": "Install, build the default bundle, and serve it locally.",
|
|
29
29
|
"url": "https://docs.pagenary.com/pages/quickstart.html",
|
|
30
|
-
"dateModified": "2026-06-
|
|
30
|
+
"dateModified": "2026-06-15",
|
|
31
31
|
"mainEntityOfPage": {
|
|
32
32
|
"@type": "WebPage",
|
|
33
33
|
"@id": "https://docs.pagenary.com/pages/quickstart.html"
|
|
@@ -310,7 +310,10 @@ npx pagenary serve
|
|
|
310
310
|
<p>1. Verify `config.json` is valid JSON</p>
|
|
311
311
|
<p>2. Check color values are valid hex codes (e.g., `#3B82F6`)</p>
|
|
312
312
|
<h3 id="search-not-working">Search not working?</h3>
|
|
313
|
-
<p>
|
|
313
|
+
<p>The command palette loads a prebuilt static index (`dist/<tenant>/search-index/`)</p>
|
|
314
|
+
<p>on first open — wait a moment for "Indexing content…" to clear. If that directory</p>
|
|
315
|
+
<p>is missing (e.g., an older build), search falls back to indexing in the browser on</p>
|
|
316
|
+
<p>first use. Rebuild with `npm run build:tenants` to regenerate the static index.</p>
|
|
314
317
|
<h2 id="resources">Resources</h2>
|
|
315
318
|
<ul>
|
|
316
319
|
<li><a href="#tenant-config">Tenant Configuration</a> - All config options</li>
|