@pagenary/publisher 2026.6.0 → 2026.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/package.json +1 -1
- package/scripts/build-tenants.js +20 -3
- 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/manifest.js +9 -9
- 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();
|
|
@@ -2575,14 +2576,14 @@ async function materializeScannedSections(sections, context) {
|
|
|
2575
2576
|
title,
|
|
2576
2577
|
summary,
|
|
2577
2578
|
...metadata,
|
|
2578
|
-
module:
|
|
2579
|
+
module: `./sections/${outFile}`,
|
|
2579
2580
|
subsections: processedSubsections
|
|
2580
2581
|
};
|
|
2581
2582
|
if (type) entry.type = type;
|
|
2582
2583
|
if (collapsed) entry.collapsed = true;
|
|
2583
2584
|
processed.push(entry);
|
|
2584
2585
|
} else {
|
|
2585
|
-
const entry = { id, title, summary, ...metadata, module:
|
|
2586
|
+
const entry = { id, title, summary, ...metadata, module: `./sections/${outFile}` };
|
|
2586
2587
|
if (type) entry.type = type;
|
|
2587
2588
|
if (collapsed) entry.collapsed = true;
|
|
2588
2589
|
processed.push(entry);
|
|
@@ -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
|
|
|
@@ -2887,7 +2896,7 @@ async function materializeSectionModule(entry, context) {
|
|
|
2887
2896
|
return null;
|
|
2888
2897
|
}
|
|
2889
2898
|
|
|
2890
|
-
return
|
|
2899
|
+
return `./sections/${outFile}`;
|
|
2891
2900
|
}
|
|
2892
2901
|
|
|
2893
2902
|
function buildManifestModuleSource(manifestEntries, defaultSection, siteConfig = {}, exportConfig = {}) {
|
|
@@ -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:27:48.659Z" />
|
|
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/manifest.js
CHANGED
|
@@ -3,7 +3,7 @@ export const MANIFEST = [
|
|
|
3
3
|
"id": "welcome",
|
|
4
4
|
"title": "Welcome",
|
|
5
5
|
"summary": "What Pagenary is and how this dogfooded portal is built.",
|
|
6
|
-
"module": "
|
|
6
|
+
"module": "./sections/welcome.js"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"id": "getting-started",
|
|
@@ -14,7 +14,7 @@ export const MANIFEST = [
|
|
|
14
14
|
"id": "quickstart",
|
|
15
15
|
"title": "Quickstart",
|
|
16
16
|
"summary": "Install, build the default bundle, and serve it locally.",
|
|
17
|
-
"module": "
|
|
17
|
+
"module": "./sections/quickstart.js"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
},
|
|
@@ -27,19 +27,19 @@ export const MANIFEST = [
|
|
|
27
27
|
"id": "developer-guide",
|
|
28
28
|
"title": "Developer Guide",
|
|
29
29
|
"summary": "Project layout, scripts, and the content authoring workflow.",
|
|
30
|
-
"module": "
|
|
30
|
+
"module": "./sections/developer-guide.js"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"id": "tenant-config",
|
|
34
34
|
"title": "Tenant Configuration",
|
|
35
35
|
"summary": "Every config.json option: branding, theming, SEO, and export.",
|
|
36
|
-
"module": "
|
|
36
|
+
"module": "./sections/tenant-config.js"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"id": "extending",
|
|
40
40
|
"title": "Extending",
|
|
41
41
|
"summary": "Add section templates, content types, and build behaviors.",
|
|
42
|
-
"module": "
|
|
42
|
+
"module": "./sections/extending.js"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
},
|
|
@@ -52,25 +52,25 @@ export const MANIFEST = [
|
|
|
52
52
|
"id": "architecture",
|
|
53
53
|
"title": "Architecture",
|
|
54
54
|
"summary": "The static SPA pattern, build pipeline, and tenant content model.",
|
|
55
|
-
"module": "
|
|
55
|
+
"module": "./sections/architecture.js"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"id": "api",
|
|
59
59
|
"title": "API Reference",
|
|
60
60
|
"summary": "Module-level documentation for the publisher internals.",
|
|
61
|
-
"module": "
|
|
61
|
+
"module": "./sections/api.js"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"id": "deployment",
|
|
65
65
|
"title": "Deployment",
|
|
66
66
|
"summary": "Hosting the static output and multi-tenant domain routing.",
|
|
67
|
-
"module": "
|
|
67
|
+
"module": "./sections/deployment.js"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"id": "seo-strategy",
|
|
71
71
|
"title": "SEO Strategy",
|
|
72
72
|
"summary": "Metadata, hash-routing considerations, and discoverability.",
|
|
73
|
-
"module": "
|
|
73
|
+
"module": "./sections/seo-strategy.js"
|
|
74
74
|
}
|
|
75
75
|
]
|
|
76
76
|
}
|
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 {
|