@pagenary/publisher 2026.5.4 → 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 +114 -16
- package/scripts/lib/search-index-generator.js +111 -0
- package/site/app.js +1 -1
- package/site/index.html +4 -4
- package/site/lib/fortemi-corpus.js +1 -0
- package/site/lib/manifest-utils.js +1 -1
- 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 +43 -0
- package/site/vendor/fortemi-aiwg-index.d.ts +212 -0
- package/site/vendor/fortemi-aiwg-index.js +1 -0
- package/src/app.js +119 -3
- package/src/index.html +3 -3
- package/src/lib/fortemi-corpus.js +0 -0
- package/src/lib/manifest-utils.js +1 -1
- package/src/lib/search.js +291 -42
- package/src/manifest.js +1 -1
- package/src/styles.css +43 -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();
|
|
@@ -1672,6 +1673,35 @@ async function ensureJavascriptModule(sourcePath, targetPath) {
|
|
|
1672
1673
|
await fsp.copyFile(sourcePath, targetPath);
|
|
1673
1674
|
}
|
|
1674
1675
|
|
|
1676
|
+
async function readMarkdownMetadata(sourcePath) {
|
|
1677
|
+
const raw = await fsp.readFile(sourcePath, 'utf8');
|
|
1678
|
+
const { data, body } = parseFrontmatter(raw);
|
|
1679
|
+
return {
|
|
1680
|
+
title: data.title || firstHeadingFromMarkdown(body) || null,
|
|
1681
|
+
summary: data.summary || data.description || '',
|
|
1682
|
+
date: data.date || null,
|
|
1683
|
+
reading_time: estimateReadingTime(body)
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function firstHeadingFromMarkdown(body) {
|
|
1688
|
+
const match = body.match(/^#\s+(.+)$/m);
|
|
1689
|
+
return match ? match[1].trim() : null;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function estimateReadingTime(body) {
|
|
1693
|
+
const words = String(body || '').trim().split(/\s+/).filter(Boolean);
|
|
1694
|
+
return Math.max(1, Math.ceil(words.length / 200));
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
async function readContentMetadata(sourcePath) {
|
|
1698
|
+
const ext = path.extname(sourcePath).toLowerCase();
|
|
1699
|
+
if (ext === '.md' || ext === '.markdown') {
|
|
1700
|
+
return readMarkdownMetadata(sourcePath);
|
|
1701
|
+
}
|
|
1702
|
+
return {};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1675
1705
|
// ============================================================================
|
|
1676
1706
|
// Internal Link Transformation (ADR-011)
|
|
1677
1707
|
// ============================================================================
|
|
@@ -1992,6 +2022,46 @@ function deriveSectionId(parentId, name, isIndex = false) {
|
|
|
1992
2022
|
return stem;
|
|
1993
2023
|
}
|
|
1994
2024
|
|
|
2025
|
+
function routePath(value) {
|
|
2026
|
+
return String(value || '').replace(/^\/+|\/+$/g, '');
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function collectionForRelPath(relPath, collections = []) {
|
|
2030
|
+
const normalized = relPath.split(path.sep).join('/');
|
|
2031
|
+
return collections.find((collection) => {
|
|
2032
|
+
const collectionPath = routePath(collection.path);
|
|
2033
|
+
return collectionPath && (normalized === collectionPath || normalized.startsWith(`${collectionPath}/`));
|
|
2034
|
+
}) || null;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function decorateCollectionEntry(entry, metadata, collection) {
|
|
2038
|
+
if (!collection || !entry) return entry;
|
|
2039
|
+
entry.collection = routePath(collection.path);
|
|
2040
|
+
entry.showDate = collection.showDate === true;
|
|
2041
|
+
entry.showSummary = collection.showSummary === true;
|
|
2042
|
+
entry.showReadingTime = collection.showReadingTime !== false;
|
|
2043
|
+
if (metadata.date) entry.date = metadata.date;
|
|
2044
|
+
if (metadata.reading_time) entry.reading_time = metadata.reading_time;
|
|
2045
|
+
return entry;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function sortCollectionEntries(entries, collection) {
|
|
2049
|
+
if (!collection || !Array.isArray(entries)) return entries;
|
|
2050
|
+
const sortBy = collection.sortBy || 'date';
|
|
2051
|
+
const dir = (collection.order || 'desc').toLowerCase() === 'asc' ? 1 : -1;
|
|
2052
|
+
entries.sort((a, b) => {
|
|
2053
|
+
const av = a?.[sortBy];
|
|
2054
|
+
const bv = b?.[sortBy];
|
|
2055
|
+
if (av == null && bv == null) return 0;
|
|
2056
|
+
if (av == null) return 1;
|
|
2057
|
+
if (bv == null) return -1;
|
|
2058
|
+
if (av < bv) return -1 * dir;
|
|
2059
|
+
if (av > bv) return 1 * dir;
|
|
2060
|
+
return 0;
|
|
2061
|
+
});
|
|
2062
|
+
return entries;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1995
2065
|
/**
|
|
1996
2066
|
* Encode section ID for use in output filename
|
|
1997
2067
|
* Replaces / with -- to create flat output structure
|
|
@@ -2107,19 +2177,21 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2107
2177
|
const indexFile = files.find(f => /^index\.(md|markdown|html|htm|js|mjs)$/i.test(f.name));
|
|
2108
2178
|
if (indexFile) {
|
|
2109
2179
|
const sectionId = parentId || path.basename(dirPath);
|
|
2110
|
-
const title = manifest?.title || humanizeTitle(path.basename(dirPath));
|
|
2111
|
-
const summary = manifest?.summary || '';
|
|
2112
|
-
|
|
2113
|
-
// Calculate relative path from content root
|
|
2114
2180
|
const relPath = path.relative(context.contentRoot, path.join(dirPath, indexFile.name));
|
|
2181
|
+
const metadata = await readContentMetadata(path.join(dirPath, indexFile.name));
|
|
2182
|
+
const collection = collectionForRelPath(relPath, context.collections);
|
|
2183
|
+
const title = manifest?.title || metadata.title || humanizeTitle(path.basename(dirPath));
|
|
2184
|
+
const summary = manifest?.summary || metadata.summary || '';
|
|
2115
2185
|
|
|
2116
|
-
|
|
2186
|
+
const sectionEntry = {
|
|
2117
2187
|
id: sectionId,
|
|
2118
2188
|
title,
|
|
2119
2189
|
summary,
|
|
2120
2190
|
file: relPath,
|
|
2121
2191
|
_isIndex: true
|
|
2122
|
-
}
|
|
2192
|
+
};
|
|
2193
|
+
decorateCollectionEntry(sectionEntry, metadata, collection);
|
|
2194
|
+
sections.push(sectionEntry);
|
|
2123
2195
|
}
|
|
2124
2196
|
|
|
2125
2197
|
// Process other content files
|
|
@@ -2131,10 +2203,12 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2131
2203
|
s.id === sectionId || s.file === file.name || s.id === file.name.replace(/\.[^.]+$/, '')
|
|
2132
2204
|
);
|
|
2133
2205
|
|
|
2134
|
-
const title = manifestEntry?.title || humanizeTitle(file.name);
|
|
2135
|
-
const summary = manifestEntry?.summary || '';
|
|
2136
|
-
const type = manifestEntry?.type || null;
|
|
2137
2206
|
const relPath = path.relative(context.contentRoot, path.join(dirPath, file.name));
|
|
2207
|
+
const metadata = await readContentMetadata(path.join(dirPath, file.name));
|
|
2208
|
+
const collection = collectionForRelPath(relPath, context.collections);
|
|
2209
|
+
const title = manifestEntry?.title || metadata.title || humanizeTitle(file.name);
|
|
2210
|
+
const summary = manifestEntry?.summary || metadata.summary || '';
|
|
2211
|
+
const type = manifestEntry?.type || null;
|
|
2138
2212
|
|
|
2139
2213
|
const sectionEntry = {
|
|
2140
2214
|
id: sectionId,
|
|
@@ -2142,6 +2216,7 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2142
2216
|
summary,
|
|
2143
2217
|
file: relPath
|
|
2144
2218
|
};
|
|
2219
|
+
decorateCollectionEntry(sectionEntry, metadata, collection);
|
|
2145
2220
|
if (type) sectionEntry.type = type;
|
|
2146
2221
|
sections.push(sectionEntry);
|
|
2147
2222
|
}
|
|
@@ -2170,6 +2245,7 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2170
2245
|
if (indexEntry) {
|
|
2171
2246
|
// Remove index from subsections, it becomes the group itself
|
|
2172
2247
|
const otherSections = subsections.filter(s => !s._isIndex);
|
|
2248
|
+
sortCollectionEntries(otherSections, collectionForRelPath(path.relative(context.contentRoot, subdirPath), context.collections));
|
|
2173
2249
|
const entry = {
|
|
2174
2250
|
id: subdirId,
|
|
2175
2251
|
title,
|
|
@@ -2185,7 +2261,7 @@ async function scanContentDirectory(dirPath, parentId, context, depth = 0) {
|
|
|
2185
2261
|
id: subdirId,
|
|
2186
2262
|
title,
|
|
2187
2263
|
summary,
|
|
2188
|
-
subsections
|
|
2264
|
+
subsections: sortCollectionEntries(subsections, collectionForRelPath(path.relative(context.contentRoot, subdirPath), context.collections))
|
|
2189
2265
|
};
|
|
2190
2266
|
if (collapsed) entry.collapsed = true;
|
|
2191
2267
|
sections.push(entry);
|
|
@@ -2441,7 +2517,7 @@ async function materializeScannedSections(sections, context) {
|
|
|
2441
2517
|
const processed = [];
|
|
2442
2518
|
|
|
2443
2519
|
for (const section of sections) {
|
|
2444
|
-
const { id, title, summary, file, subsections, url, type, collapsed } = section;
|
|
2520
|
+
const { id, title, summary, file, subsections, url, type, collapsed, _isIndex, ...metadata } = section;
|
|
2445
2521
|
|
|
2446
2522
|
// Pass through external links without processing
|
|
2447
2523
|
if (url) {
|
|
@@ -2499,6 +2575,7 @@ async function materializeScannedSections(sections, context) {
|
|
|
2499
2575
|
id,
|
|
2500
2576
|
title,
|
|
2501
2577
|
summary,
|
|
2578
|
+
...metadata,
|
|
2502
2579
|
module: `./sections/${outFile}`,
|
|
2503
2580
|
subsections: processedSubsections
|
|
2504
2581
|
};
|
|
@@ -2506,14 +2583,14 @@ async function materializeScannedSections(sections, context) {
|
|
|
2506
2583
|
if (collapsed) entry.collapsed = true;
|
|
2507
2584
|
processed.push(entry);
|
|
2508
2585
|
} else {
|
|
2509
|
-
const entry = { id, title, summary, module: `./sections/${outFile}` };
|
|
2586
|
+
const entry = { id, title, summary, ...metadata, module: `./sections/${outFile}` };
|
|
2510
2587
|
if (type) entry.type = type;
|
|
2511
2588
|
if (collapsed) entry.collapsed = true;
|
|
2512
2589
|
processed.push(entry);
|
|
2513
2590
|
}
|
|
2514
2591
|
} else if (processedSubsections && processedSubsections.length > 0) {
|
|
2515
2592
|
// Group without its own content
|
|
2516
|
-
const entry = { id, title, summary, subsections: processedSubsections };
|
|
2593
|
+
const entry = { id, title, summary, ...metadata, subsections: processedSubsections };
|
|
2517
2594
|
if (type) entry.type = type;
|
|
2518
2595
|
if (collapsed) entry.collapsed = true;
|
|
2519
2596
|
processed.push(entry);
|
|
@@ -2575,6 +2652,9 @@ function applyManifestHierarchy(scannedSections, rootManifest) {
|
|
|
2575
2652
|
title: mEntry.title || scanned?.title || mEntry.id,
|
|
2576
2653
|
summary: mEntry.summary || scanned?.summary || ''
|
|
2577
2654
|
};
|
|
2655
|
+
for (const key of ['collection', 'showDate', 'showSummary', 'showReadingTime', 'date', 'reading_time']) {
|
|
2656
|
+
if (scanned?.[key] !== undefined) node[key] = scanned[key];
|
|
2657
|
+
}
|
|
2578
2658
|
|
|
2579
2659
|
if (mEntry.collapsed) node.collapsed = true;
|
|
2580
2660
|
if (mEntry.type) node.type = mEntry.type;
|
|
@@ -2671,6 +2751,7 @@ async function processNestedContent(sourceDir, distDir, tenantId, contentRoot, o
|
|
|
2671
2751
|
keepFiles,
|
|
2672
2752
|
leafOrder: [],
|
|
2673
2753
|
siteConfig,
|
|
2754
|
+
collections: Array.isArray(config.collections) ? config.collections : [],
|
|
2674
2755
|
// Link transformation context (populated after scan)
|
|
2675
2756
|
sectionIndex: null,
|
|
2676
2757
|
linkWarnings,
|
|
@@ -2731,6 +2812,14 @@ async function processNestedContent(sourceDir, distDir, tenantId, contentRoot, o
|
|
|
2731
2812
|
await fsp.writeFile(path.join(distDir, 'manifest.js'), manifestModule, 'utf8');
|
|
2732
2813
|
console.log(` ↳ applied nested content structure for ${tenantId} (${context.leafOrder.length} sections)`);
|
|
2733
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
|
+
|
|
2734
2823
|
return { success: true, sectionsCount: context.leafOrder.length };
|
|
2735
2824
|
}
|
|
2736
2825
|
|
|
@@ -2779,7 +2868,7 @@ async function materializeSectionModule(entry, context) {
|
|
|
2779
2868
|
}
|
|
2780
2869
|
|
|
2781
2870
|
const ext = path.extname(sourcePath).toLowerCase();
|
|
2782
|
-
const outFile = `${id}.js`;
|
|
2871
|
+
const outFile = `${encodePathForFilename(id)}.js`;
|
|
2783
2872
|
const targetPath = path.join(context.sectionsDir, outFile);
|
|
2784
2873
|
context.keepFiles.add(outFile);
|
|
2785
2874
|
|
|
@@ -3031,6 +3120,14 @@ async function processTenantManifestLegacy(sourceDir, distDir, tenantId, options
|
|
|
3031
3120
|
const manifestModule = buildManifestModuleSource(processedManifest, defaultSection, context.siteConfig);
|
|
3032
3121
|
await fsp.writeFile(path.join(distDir, 'manifest.js'), manifestModule, 'utf8');
|
|
3033
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
|
+
|
|
3034
3131
|
return { success: true };
|
|
3035
3132
|
}
|
|
3036
3133
|
|
|
@@ -3339,7 +3436,7 @@ async function processIncrementalManifest(sourceDir, distDir, tenantId, changedF
|
|
|
3339
3436
|
const ext = path.extname(sourcePath).toLowerCase();
|
|
3340
3437
|
// Use manifest section ID if available, otherwise fall back to filename
|
|
3341
3438
|
const sectionId = fileToSectionId.get(relPath) || path.basename(relPath, ext);
|
|
3342
|
-
const targetPath = path.join(sectionsDir, `${sectionId}.js`);
|
|
3439
|
+
const targetPath = path.join(sectionsDir, `${encodePathForFilename(sectionId)}.js`);
|
|
3343
3440
|
|
|
3344
3441
|
try {
|
|
3345
3442
|
if (ext === '.md' || ext === '.markdown') {
|
|
@@ -3349,7 +3446,8 @@ async function processIncrementalManifest(sourceDir, distDir, tenantId, changedF
|
|
|
3349
3446
|
contentRoot: contentRoot.basePath || contentDir,
|
|
3350
3447
|
sectionIndex,
|
|
3351
3448
|
linkWarnings,
|
|
3352
|
-
strictLinks: options.strictLinks !== false
|
|
3449
|
+
strictLinks: options.strictLinks !== false,
|
|
3450
|
+
collections: Array.isArray(config.collections) ? config.collections : []
|
|
3353
3451
|
};
|
|
3354
3452
|
await ensureMarkdownModule(sourcePath, targetPath, linkContext);
|
|
3355
3453
|
console.log(` ↳ updated: ${sectionId} (markdown)`);
|
|
@@ -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 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 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"),N=document.getElementById("commandList"),w=document.getElementById("mobileMenuToggle"),C=document.querySelector(".sidebar"),k="docs-toolkit-command-query",$=new Map,H=new Map,I=new Map,M=new Set;let A=[],S=0,P=!1,q=(localStorage.getItem(k)||"").trim(),B=!1;function R(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 F(e,t={}){const{scrollToHighlight:a=!1}=t,{targetId:s,parentId:o}=function(e){return m(e,n)}(e);P&&_(),o&&j(o,!0),B=a||Boolean(q),location.hash.replace("#","")===s?O():location.hash=`#${s}`}function D(){return location.hash.replace("#","")||t}async function O(){const e=D(),t=p(e,n);if(!t)return;const{entry:o,targetId:l,parentId:c}=t;l===e?(c&&j(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(y.innerHTML='<article class="section"><p>Section failed to load.</p></article>');const o=await n();y.innerHTML=o.html||"",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 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(),F(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(),F(l.id)}),e.appendChild(t),e.innerHTML+='<span class="bottom-nav-chevron">›</span>',c.appendChild(e)}(y.querySelector("section")||y).appendChild(c)}(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 j(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 W(){if(!T||!x)return;P=!0,T.hidden=!1;const e=q;x.value=e,z(e),requestAnimationFrame(()=>{x.focus(),e&&x.select()})}function _(){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=R(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(),F(e.id,{scrollToHighlight:Boolean(q)})}),s.querySelector(".nav-expand-toggle").addEventListener("click",t=>{t.stopPropagation();const n=!M.has(e.id);j(e.id,n)}),s.addEventListener("click",t=>{if(t.target===s){const t=!M.has(e.id);j(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);j(e.id,t)}));const o=document.createElement("div");o.className="nav-sublist",e.subsections.forEach(e=>{if(e.url){const t=R(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);j(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-nested",e.subsections.forEach(e=>{if(e.url){const t=R(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);j(e.id,t)});const s=document.createElement("div");s.className="nav-sublist nav-sublist-deep",e.subsections.forEach(e=>{if(e.url){const t=R(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);j(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-ultra",e.subsections.forEach(e=>{if(e.url){const t=R(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",()=>F(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 j(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",()=>F(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 j(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",()=>F(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 j(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",()=>F(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);j(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",()=>F(e.id,{scrollToHighlight:Boolean(q)})),g.appendChild(t),H.set(e.id,t)}})}(),q&&(B=!0),window.addEventListener("hashchange",()=>{q&&(B=!0),O()}),b.textContent=(new Date).getFullYear(),O(),L&&T&&x&&N&&(L.addEventListener("click",()=>{P?_():W()}),x.addEventListener("input",()=>{const e=x.value;J(e,!0),z(e)}),x.addEventListener("keydown",e=>{const t=A.length-1;if("ArrowDown"===e.key)e.preventDefault(),S=Math.min(t,S+1),X();else if("ArrowUp"===e.key)e.preventDefault(),S=Math.max(0,S-1),X();else if("Enter"===e.key){e.preventDefault();const t=A[S];t&&(J(x.value,!0),F(t.id,{scrollToHighlight:!0}),_())}else"Escape"===e.key&&(e.preventDefault(),_())}),N.addEventListener("click",e=>{const t=e.target.closest("[data-section]");if(!t)return;const n=t.dataset.section;n&&(J(x.value,!0),F(n,{scrollToHighlight:!0}),_())}),T.addEventListener("click",e=>{e.target===T&&_()}),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(),_())}));let V=null,U=!1;async function z(t){N&&(!U&&t.trim()&&(U=!0,N.innerHTML='<li class="cmd-item cmd-loading">Indexing content...</li>'),clearTimeout(V),V=setTimeout(async()=>{A=await c(e,t);const n=D();S=d(A,n),function(){if(N){if(N.innerHTML="",!A.length){const e=document.createElement("li");return e.className="cmd-item",e.setAttribute("aria-selected","false"),e.textContent="No matches.",void N.appendChild(e)}A.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),N.appendChild(t)})}}(),X(),U=!1},t.trim()?150:0))}function X(){N&&Array.from(N.children).forEach((e,t)=>{const n=t===S&&A.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}}),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"})})}(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=D(),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=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)})})}),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"))}),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"),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"))}));
|
|
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
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
6
|
<title>Pagenary Docs</title>
|
|
7
7
|
<meta name="description" content="Pagenary developer documentation — building, configuring, deploying, and extending the multi-tenant documentation publisher, published with Pagenary itself." />
|
|
8
|
-
<link rel="icon" type="image/png" href="
|
|
9
|
-
<link rel="stylesheet" href="
|
|
10
|
-
<meta name="x-build" content="2026-
|
|
8
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
9
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
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>
|
|
@@ -52,6 +52,6 @@
|
|
|
52
52
|
<ul id="commandList" class="cmd-list" role="listbox"></ul>
|
|
53
53
|
</div>
|
|
54
54
|
</div>
|
|
55
|
-
<script type="module" src="
|
|
55
|
+
<script type="module" src="/app.js"></script>
|
|
56
56
|
</body>
|
|
57
57
|
</html>
|
|
@@ -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}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function sectionEntry(t,n){const e="string"==typeof t?t:t.id,r=n(e,"string"==typeof t?{}:{title:t.title,summary:t.summary});return{id:e,title:r.title,summary:r.summary,module
|
|
1
|
+
export function sectionEntry(t,n){const e="string"==typeof t?t:t.id,r=n(e,"string"==typeof t?{}:{title:t.title,summary:t.summary});return{id:e,title:r.title,summary:r.summary,module:`/sections/${e}.js`}}export function groupEntry(t,n){const{id:e,title:r,summary:i,sections:s}=t;return{id:e,title:r,summary:i,subsections:s.map(t=>sectionEntry(t,n))}}export function buildSectionIndex(t){const n=new Map;function e(t,r=null){r&&(t.parentId=r),n.set(t.id,t),Array.isArray(t.subsections)&&t.subsections.forEach(n=>e(n,t.id))}return t.forEach(t=>e(t)),n}export function createFindSection(t){return function(n){return t.get(n)||null}}
|
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}
|