@pagenary/publisher 2026.5.0
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/LICENSE +661 -0
- package/README.md +337 -0
- package/bin/pagenary.mjs +116 -0
- package/build.config.json +5 -0
- package/package.json +66 -0
- package/scripts/build-site.js +87 -0
- package/scripts/build-tenants.js +3569 -0
- package/scripts/build.js +99 -0
- package/scripts/generate-sections.js +41 -0
- package/scripts/lib/seo-generator.js +558 -0
- package/scripts/lint-content.js +62 -0
- package/scripts/seo-smoke.js +94 -0
- package/scripts/serve.js +142 -0
- package/site/app.js +1 -0
- package/site/index.html +57 -0
- package/site/lib/categories.js +1 -0
- package/site/lib/export.js +1 -0
- package/site/lib/manifest-utils.js +1 -0
- package/site/lib/router.js +1 -0
- package/site/lib/search.js +1 -0
- package/site/llms.txt +22 -0
- package/site/manifest.js +132 -0
- package/site/mermaid-init.js +1 -0
- package/site/pages/api.html +339 -0
- package/site/pages/architecture.html +303 -0
- package/site/pages/deployment.html +282 -0
- package/site/pages/developer-guide.html +157 -0
- package/site/pages/extending.html +135 -0
- package/site/pages/quickstart.html +318 -0
- package/site/pages/seo-strategy.html +121 -0
- package/site/pages/tenant-config.html +519 -0
- package/site/pages/welcome.html +116 -0
- package/site/robots.txt +10 -0
- package/site/sections/api.js +3 -0
- package/site/sections/architecture.js +3 -0
- package/site/sections/deployment.js +3 -0
- package/site/sections/developer-guide.js +3 -0
- package/site/sections/extending.js +3 -0
- package/site/sections/quickstart.js +3 -0
- package/site/sections/section-templates.js +1 -0
- package/site/sections/seo-strategy.js +3 -0
- package/site/sections/tenant-config.js +3 -0
- package/site/sections/welcome.js +3 -0
- package/site/seo.js +1 -0
- package/site/sitemap.xml +63 -0
- package/site/styles.css +1982 -0
- package/site/syntax-highlight.js +1 -0
- package/src/app.js +988 -0
- package/src/index.html +56 -0
- package/src/lib/categories.js +55 -0
- package/src/lib/export.js +195 -0
- package/src/lib/manifest-utils.js +69 -0
- package/src/lib/router.js +44 -0
- package/src/lib/search.js +151 -0
- package/src/manifest.js +246 -0
- package/src/mermaid-init.js +207 -0
- package/src/sections/archive-future-roadmap.js +7 -0
- package/src/sections/archive-initiative-alpha.js +7 -0
- package/src/sections/archive-milestone-records.js +7 -0
- package/src/sections/archive-timeline-overview.js +7 -0
- package/src/sections/core-technology-compliance-frameworks.js +7 -0
- package/src/sections/core-technology-coordination-model.js +7 -0
- package/src/sections/core-technology-data-definitions.js +7 -0
- package/src/sections/core-technology-hardware-integration.js +7 -0
- package/src/sections/core-technology-integrity-controls.js +7 -0
- package/src/sections/core-technology-network-topology.js +7 -0
- package/src/sections/core-technology-operator-requirements.js +7 -0
- package/src/sections/core-technology-overview.js +7 -0
- package/src/sections/core-technology-service-interfaces.js +7 -0
- package/src/sections/core-technology-synchronization-strategy.js +7 -0
- package/src/sections/core-technology-system-foundation.js +7 -0
- package/src/sections/developers-api-credentials.js +7 -0
- package/src/sections/developers-api-operations.js +7 -0
- package/src/sections/developers-api-reference.js +7 -0
- package/src/sections/developers-api-websocket.js +7 -0
- package/src/sections/developers-automation-blueprints.js +7 -0
- package/src/sections/developers-automation-modules.js +7 -0
- package/src/sections/developers-automation-patterns.js +7 -0
- package/src/sections/developers-deployment-playbook.js +7 -0
- package/src/sections/developers-overview.js +7 -0
- package/src/sections/developers-scheduling-patterns.js +7 -0
- package/src/sections/developers-sdk-go.js +7 -0
- package/src/sections/developers-sdk-javascript.js +7 -0
- package/src/sections/developers-sdk-python.js +7 -0
- package/src/sections/developers-sdk-rust.js +7 -0
- package/src/sections/developers-sdks.js +7 -0
- package/src/sections/developers-solution-examples.js +7 -0
- package/src/sections/developers-testing-framework.js +7 -0
- package/src/sections/getting-started-architecture-basics.js +7 -0
- package/src/sections/getting-started-introduction.js +7 -0
- package/src/sections/getting-started-performance-overview.js +7 -0
- package/src/sections/governance-community-initiatives.js +7 -0
- package/src/sections/governance-dao-overview.js +7 -0
- package/src/sections/governance-multi-token.js +7 -0
- package/src/sections/governance-overview.js +7 -0
- package/src/sections/governance-proposal-process.js +7 -0
- package/src/sections/governance-proposals.js +7 -0
- package/src/sections/governance-structure.js +7 -0
- package/src/sections/governance-token-distribution.js +7 -0
- package/src/sections/governance-treasury.js +7 -0
- package/src/sections/operations-environment-prep.js +7 -0
- package/src/sections/operations-getting-started.js +7 -0
- package/src/sections/operations-incentives-guide.js +7 -0
- package/src/sections/operations-incentives-strategies.js +7 -0
- package/src/sections/operations-incentives.js +7 -0
- package/src/sections/operations-infrastructure.js +7 -0
- package/src/sections/operations-monitoring.js +7 -0
- package/src/sections/operations-overview.js +7 -0
- package/src/sections/operations-performance.js +7 -0
- package/src/sections/operations-power-infrastructure.js +7 -0
- package/src/sections/operations-setup-guide.js +7 -0
- package/src/sections/operations-sync-setup.js +7 -0
- package/src/sections/products-flagship-solution.js +7 -0
- package/src/sections/products-solution-library.js +7 -0
- package/src/sections/resources-brand-assets.js +7 -0
- package/src/sections/resources-faq.js +7 -0
- package/src/sections/resources-glossary.js +7 -0
- package/src/sections/resources-research-papers.js +7 -0
- package/src/sections/section-templates.js +873 -0
- package/src/sections/security-audits.js +7 -0
- package/src/sections/security-best-practices.js +7 -0
- package/src/sections/security-bug-bounty.js +7 -0
- package/src/sections/security-incident-response.js +7 -0
- package/src/sections/security-overview.js +7 -0
- package/src/sections/technical-architecture.js +7 -0
- package/src/sections/technical-whitepaper.js +7 -0
- package/src/sections/tutorial-automation-bot.js +7 -0
- package/src/sections/tutorial-build-first-integration.js +7 -0
- package/src/sections/tutorial-deploy-automation.js +7 -0
- package/src/sections/tutorial-event-driven-experience.js +7 -0
- package/src/sections/tutorial-operations-onboarding.js +7 -0
- package/src/sections/tutorial-systems-integration.js +7 -0
- package/src/sections/tutorials-overview.js +7 -0
- package/src/sections/use-case-connected-devices.js +7 -0
- package/src/sections/use-case-digital-auctions.js +7 -0
- package/src/sections/use-case-financial-automation.js +7 -0
- package/src/sections/use-case-interactive-media.js +7 -0
- package/src/sections/use-case-realtime-execution.js +7 -0
- package/src/sections/use-case-research-analytics.js +7 -0
- package/src/sections/use-case-supply-operations.js +7 -0
- package/src/sections/use-cases-overview.js +7 -0
- package/src/sections/welcome-overview.js +7 -0
- package/src/seo.js +90 -0
- package/src/styles.css +1982 -0
- package/src/syntax-highlight.js +90 -0
- package/tenants.json.example +68 -0
- package/tenants.schema.json +231 -0
package/src/index.html
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Docs Toolkit</title>
|
|
7
|
+
<meta name="description" content="Reusable documentation toolkit for multi-tenant services." />
|
|
8
|
+
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
9
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<a class="skip-link" href="#app">Skip to content</a>
|
|
13
|
+
<header class="shell topbar">
|
|
14
|
+
<button type="button" id="mobileMenuToggle" class="mobile-menu-toggle" aria-label="Toggle navigation" aria-expanded="false">
|
|
15
|
+
<span class="menu-icon"></span>
|
|
16
|
+
</button>
|
|
17
|
+
<div class="brand">
|
|
18
|
+
<span class="brand-mark">Docs</span>
|
|
19
|
+
<span class="brand-sub">Toolkit</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="top-actions">
|
|
22
|
+
<button type="button" id="commandToggle" class="ghost-button" aria-haspopup="dialog" aria-controls="commandPalette">
|
|
23
|
+
<span class="ghost-icon">⌘K</span>
|
|
24
|
+
<span class="ghost-label">Quick Find</span>
|
|
25
|
+
</button>
|
|
26
|
+
<button type="button" id="exportBtn" class="ghost-button">
|
|
27
|
+
<span class="ghost-icon">⇣</span>
|
|
28
|
+
<span class="ghost-label">Export</span>
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
</header>
|
|
32
|
+
<div class="layout">
|
|
33
|
+
<aside class="sidebar" aria-label="Section navigation">
|
|
34
|
+
<div class="sidebar-inner">
|
|
35
|
+
<nav id="nav" class="nav" aria-label="Primary"></nav>
|
|
36
|
+
</div>
|
|
37
|
+
</aside>
|
|
38
|
+
<main id="app" class="canvas" tabindex="-1" aria-live="polite"></main>
|
|
39
|
+
</div>
|
|
40
|
+
<footer class="shell footnote">
|
|
41
|
+
<span>© <span id="year"></span> Modular Documentation Toolkit</span>
|
|
42
|
+
<span class="divider"></span>
|
|
43
|
+
<span>Reusable patterns for multi-tenant services.</span>
|
|
44
|
+
</footer>
|
|
45
|
+
<div id="commandPalette" class="cmd" role="dialog" aria-modal="true" aria-labelledby="commandLabel" hidden>
|
|
46
|
+
<div class="cmd-surface" role="document">
|
|
47
|
+
<div class="cmd-header">
|
|
48
|
+
<label id="commandLabel" class="cmd-title" for="commandInput">Jump to section</label>
|
|
49
|
+
<input id="commandInput" class="cmd-input" type="search" name="query" autocomplete="off" placeholder="Start typing…" />
|
|
50
|
+
</div>
|
|
51
|
+
<ul id="commandList" class="cmd-list" role="listbox"></ul>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<script type="module" src="./app.js"></script>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category inference and title formatting utilities.
|
|
3
|
+
* Pure functions - no DOM dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const WORD_OVERRIDES = {
|
|
7
|
+
api: 'API',
|
|
8
|
+
apis: 'APIs',
|
|
9
|
+
faq: 'FAQ',
|
|
10
|
+
rpc: 'RPC',
|
|
11
|
+
sdk: 'SDK',
|
|
12
|
+
sdks: 'SDKs',
|
|
13
|
+
ui: 'UI'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const CATEGORY_RULES = [
|
|
17
|
+
{ category: 'welcome', test: (id) => id.startsWith('welcome') },
|
|
18
|
+
{ category: 'guide', test: (id) => id.startsWith('getting-started') },
|
|
19
|
+
{ category: 'reference', test: (id) => id.startsWith('core-technology') },
|
|
20
|
+
{ category: 'technical', test: (id) => id.startsWith('technical') },
|
|
21
|
+
{ category: 'developer', test: (id) => id.startsWith('developers') },
|
|
22
|
+
{ category: 'tutorial-overview', test: (id) => id === 'tutorials-overview' },
|
|
23
|
+
{ category: 'tutorial', test: (id) => id.startsWith('tutorial-') },
|
|
24
|
+
{ category: 'tutorial', test: (id) => id.startsWith('tutorials-') },
|
|
25
|
+
{ category: 'use-case', test: (id) => id.startsWith('use-case') },
|
|
26
|
+
{ category: 'product', test: (id) => id.startsWith('products') },
|
|
27
|
+
{ category: 'governance', test: (id) => id.startsWith('governance') },
|
|
28
|
+
{ category: 'resource', test: (id) => id.startsWith('resources') },
|
|
29
|
+
{ category: 'security', test: (id) => id.startsWith('security') },
|
|
30
|
+
{ category: 'operations', test: (id) => id.startsWith('operations') },
|
|
31
|
+
{ category: 'archive', test: (id) => id.startsWith('archive') },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function formatWord(word) {
|
|
35
|
+
if (!word) return '';
|
|
36
|
+
if (WORD_OVERRIDES[word]) return WORD_OVERRIDES[word];
|
|
37
|
+
if (/^\d+$/.test(word)) return word;
|
|
38
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeId(id) {
|
|
42
|
+
return id.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function inferCategory(id) {
|
|
46
|
+
const normalized = normalizeId(id);
|
|
47
|
+
const match = CATEGORY_RULES.find((rule) => rule.test(normalized));
|
|
48
|
+
return match ? match.category : 'default';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function titleFromId(id) {
|
|
52
|
+
const normalized = normalizeId(id).replace(/_/g, '-');
|
|
53
|
+
const words = normalized.split('-').filter(Boolean).map((word) => formatWord(word));
|
|
54
|
+
return words.join(' ');
|
|
55
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export document composition utilities.
|
|
3
|
+
* Pure functions - no DOM dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compose a complete HTML export document from chapters.
|
|
8
|
+
* @param {Array<{section: {title: string, summary?: string}, html: string}>} chapters - Array of chapter objects
|
|
9
|
+
* @param {object} [config={}] - Export branding configuration
|
|
10
|
+
* @param {string} [config.title] - Document title
|
|
11
|
+
* @param {string} [config.brandMark] - Primary brand text
|
|
12
|
+
* @param {string} [config.brandSub] - Secondary brand text
|
|
13
|
+
* @param {string} [config.tagline] - Brand tagline
|
|
14
|
+
* @param {string|null} [config.logo] - Logo as data URI or null
|
|
15
|
+
* @param {boolean} [config.showTagline=true] - Whether to show tagline
|
|
16
|
+
* @param {boolean} [config.showDate=true] - Whether to show generation date
|
|
17
|
+
* @returns {string} Complete HTML document
|
|
18
|
+
*/
|
|
19
|
+
export function composeExportDocument(chapters, config = {}) {
|
|
20
|
+
const {
|
|
21
|
+
title = 'Documentation Export',
|
|
22
|
+
brandMark = 'Docs',
|
|
23
|
+
brandSub = 'Toolkit',
|
|
24
|
+
tagline = '',
|
|
25
|
+
logo = null,
|
|
26
|
+
showTagline = true,
|
|
27
|
+
showDate = true
|
|
28
|
+
} = config;
|
|
29
|
+
|
|
30
|
+
const date = new Date().toLocaleString();
|
|
31
|
+
|
|
32
|
+
// Build brand name with optional sub
|
|
33
|
+
const brandName = brandSub
|
|
34
|
+
? `<span class="brand-mark">${brandMark}</span><span class="brand-sub">${brandSub}</span>`
|
|
35
|
+
: brandMark;
|
|
36
|
+
|
|
37
|
+
// Build header components
|
|
38
|
+
const logoHtml = logo
|
|
39
|
+
? `<img src="${logo}" alt="" class="export-logo" aria-hidden="true" />`
|
|
40
|
+
: '';
|
|
41
|
+
|
|
42
|
+
const taglineHtml = showTagline && tagline
|
|
43
|
+
? `<p class="tagline">${tagline}</p>`
|
|
44
|
+
: '';
|
|
45
|
+
|
|
46
|
+
const dateHtml = showDate
|
|
47
|
+
? `<p class="meta">Generated ${date}</p>`
|
|
48
|
+
: '';
|
|
49
|
+
|
|
50
|
+
// Determine header class based on content
|
|
51
|
+
const headerClass = logo ? 'export-header--logo-text' : 'export-header--text-only';
|
|
52
|
+
|
|
53
|
+
const toc = chapters.map((chapter, index) => `
|
|
54
|
+
<li>${index + 1}. ${chapter.section.title}</li>`).join('');
|
|
55
|
+
const body = chapters.map((chapter, index) => `
|
|
56
|
+
<section>
|
|
57
|
+
<h2>${index + 1}. ${chapter.section.title}</h2>
|
|
58
|
+
<p class="section-summary">${chapter.section.summary || ''}</p>
|
|
59
|
+
<div class="section-body">
|
|
60
|
+
${chapter.html}
|
|
61
|
+
</div>
|
|
62
|
+
</section>`).join('\n');
|
|
63
|
+
return `<!doctype html>
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charset="utf-8" />
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
68
|
+
<title>${title}</title>
|
|
69
|
+
<style>
|
|
70
|
+
:root { color-scheme: light; font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
|
71
|
+
body { margin: 0 auto; padding: 2.5rem; max-width: 820px; color: #111; line-height: 1.6; }
|
|
72
|
+
/* Export header styling */
|
|
73
|
+
header { text-align: center; margin-bottom: 3rem; }
|
|
74
|
+
.export-brand { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 0.5rem; }
|
|
75
|
+
.export-header--logo-text .export-brand { flex-direction: row; }
|
|
76
|
+
.export-header--text-only .export-brand { flex-direction: column; gap: 0; }
|
|
77
|
+
.export-logo { max-height: 48px; width: auto; }
|
|
78
|
+
h1 { font-size: 2.2rem; letter-spacing: 0.1em; text-transform: uppercase; margin: 0; line-height: 1; }
|
|
79
|
+
.brand-mark { font-weight: 700; }
|
|
80
|
+
.brand-sub { font-weight: 400; opacity: 0.85; }
|
|
81
|
+
.tagline { color: #666; font-size: 0.95rem; margin: 0.5rem 0 0 0; font-style: italic; }
|
|
82
|
+
.meta { color: #666; font-size: 0.9rem; margin: 0.5rem 0 0 0; }
|
|
83
|
+
.toc { border: 1px solid rgba(0,0,0,0.1); padding: 1.5rem; margin-bottom: 2.5rem; background: rgba(0,0,0,0.02); }
|
|
84
|
+
.toc h2 { margin-top: 0; letter-spacing: 0.12em; text-transform: uppercase; font-size: 0.95rem; }
|
|
85
|
+
.toc ul { margin: 0; padding-left: 1.2rem; }
|
|
86
|
+
.toc li { margin: 0.4rem 0; }
|
|
87
|
+
section { page-break-inside: avoid; margin-bottom: 2.75rem; }
|
|
88
|
+
h2 { font-size: 1.4rem; letter-spacing: 0.08em; text-transform: uppercase; border-bottom: 1px solid rgba(0,0,0,0.12); padding-bottom: 0.5rem; margin-bottom: 1rem; }
|
|
89
|
+
.section-summary { color: #5a5a5a; font-size: 0.95rem; margin-top: 0; }
|
|
90
|
+
.section-body { border-left: 1px solid rgba(0,0,0,0.1); padding-left: 1.25rem; }
|
|
91
|
+
.card, .card-grid, .card-grid > *, .section-body > * { break-inside: avoid; }
|
|
92
|
+
pre { background: rgba(0,0,0,0.05); padding: 1rem; border-radius: 4px; overflow-x: auto; }
|
|
93
|
+
code { font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
94
|
+
/* Table styling */
|
|
95
|
+
table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.9rem; }
|
|
96
|
+
th, td { padding: 0.75rem 1rem; text-align: left; border: 1px solid rgba(0,0,0,0.15); }
|
|
97
|
+
th { background: rgba(0,0,0,0.05); font-weight: 600; }
|
|
98
|
+
tbody tr:nth-child(even) { background: rgba(0,0,0,0.02); }
|
|
99
|
+
/* Spec table styling (legacy) */
|
|
100
|
+
.spec-table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: 0.9rem; }
|
|
101
|
+
.spec-table th, .spec-table td { padding: 0.75rem 1rem; text-align: left; border: 1px solid rgba(0,0,0,0.15); }
|
|
102
|
+
.spec-table th { background: rgba(0,0,0,0.05); font-weight: 600; }
|
|
103
|
+
.spec-table tr:nth-child(even) { background: rgba(0,0,0,0.02); }
|
|
104
|
+
.spec-table td:first-child { width: 40%; }
|
|
105
|
+
/* Layer stack styling */
|
|
106
|
+
.layer-stack { display: flex; flex-direction: column; margin: 1.5rem 0; border: 2px solid #111; border-radius: 6px; overflow: hidden; }
|
|
107
|
+
.layer-stack .layer { padding: 1rem 1.25rem; text-align: center; border-bottom: 1px solid rgba(0,0,0,0.15); }
|
|
108
|
+
.layer-stack .layer:last-child { border-bottom: none; }
|
|
109
|
+
.layer-stack .layer:first-child { background: rgba(0,0,0,0.02); }
|
|
110
|
+
.layer-stack .layer:nth-child(2) { background: rgba(0,0,0,0.04); }
|
|
111
|
+
.layer-stack .layer:nth-child(3) { background: rgba(0,0,0,0.06); }
|
|
112
|
+
.layer-stack .layer-title { font-weight: 600; font-size: 1rem; margin-bottom: 0.25rem; }
|
|
113
|
+
.layer-stack .layer-desc, .layer-stack .layer-detail { font-size: 0.85rem; color: #666; }
|
|
114
|
+
/* HTML block container */
|
|
115
|
+
.html-block { margin: 1.5rem 0; }
|
|
116
|
+
/* Box diagram styling */
|
|
117
|
+
.box-diagram { border: 2px solid #111; border-radius: 6px; padding: 1.25rem; margin: 1.5rem 0; background: rgba(0,0,0,0.02); font-family: 'IBM Plex Mono', monospace; font-size: 0.85rem; white-space: pre-wrap; }
|
|
118
|
+
.box-diagram .box-title { font-weight: 700; font-size: 1rem; margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid rgba(0,0,0,0.15); }
|
|
119
|
+
/* Syntax highlighting tokens */
|
|
120
|
+
.token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6a737d; font-style: italic; }
|
|
121
|
+
.token.punctuation { color: #24292e; }
|
|
122
|
+
.token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #d73a49; }
|
|
123
|
+
.token.boolean, .token.number { color: #005cc5; }
|
|
124
|
+
.token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #22863a; }
|
|
125
|
+
.token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #d73a49; }
|
|
126
|
+
.token.atrule, .token.attr-value, .token.keyword { color: #d73a49; }
|
|
127
|
+
.token.function, .token.class-name { color: #6f42c1; }
|
|
128
|
+
.token.regex, .token.important, .token.variable { color: #e36209; }
|
|
129
|
+
.token.important, .token.bold { font-weight: bold; }
|
|
130
|
+
.token.italic { font-style: italic; }
|
|
131
|
+
@media print {
|
|
132
|
+
@page { size: A4; margin: 1in; }
|
|
133
|
+
body { box-shadow: none; }
|
|
134
|
+
header, .toc { break-after: avoid; }
|
|
135
|
+
.export-logo { max-height: 36px; }
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<header class="${headerClass}">
|
|
141
|
+
<div class="export-brand">
|
|
142
|
+
${logoHtml}
|
|
143
|
+
<h1>${brandName}</h1>
|
|
144
|
+
</div>
|
|
145
|
+
${taglineHtml}
|
|
146
|
+
${dateHtml}
|
|
147
|
+
</header>
|
|
148
|
+
<div class="toc">
|
|
149
|
+
<h2>Table of Contents</h2>
|
|
150
|
+
<ul>${toc || '<li>No sections</li>'}</ul>
|
|
151
|
+
</div>
|
|
152
|
+
${body || '<p>No sections available.</p>'}
|
|
153
|
+
<script type="module">
|
|
154
|
+
import Prism from 'https://esm.sh/prismjs@1.29.0';
|
|
155
|
+
await Promise.all([
|
|
156
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-c'),
|
|
157
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-json'),
|
|
158
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-typescript'),
|
|
159
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-python'),
|
|
160
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-rust'),
|
|
161
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-go'),
|
|
162
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-bash'),
|
|
163
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-yaml'),
|
|
164
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-sql'),
|
|
165
|
+
import('https://esm.sh/prismjs@1.29.0/components/prism-solidity'),
|
|
166
|
+
]);
|
|
167
|
+
Prism.highlightAll();
|
|
168
|
+
window.focus();
|
|
169
|
+
</script>
|
|
170
|
+
</body>
|
|
171
|
+
</html>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Collect all exportable sections from a manifest.
|
|
176
|
+
* Returns leaf sections (those with module paths) in navigation order.
|
|
177
|
+
* @param {Array} manifest - Navigation manifest
|
|
178
|
+
* @returns {Array} Flat array of exportable sections
|
|
179
|
+
*/
|
|
180
|
+
export function collectExportableSections(manifest) {
|
|
181
|
+
const allSections = [];
|
|
182
|
+
for (const section of manifest) {
|
|
183
|
+
if (section.module) {
|
|
184
|
+
allSections.push(section);
|
|
185
|
+
}
|
|
186
|
+
if (section.subsections) {
|
|
187
|
+
for (const subsection of section.subsections) {
|
|
188
|
+
if (subsection.module) {
|
|
189
|
+
allSections.push(subsection);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return allSections;
|
|
195
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest building utilities.
|
|
3
|
+
* Pure functions - no DOM dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a section entry from input (string ID or object with overrides).
|
|
8
|
+
* @param {string|{id: string, title?: string, summary?: string}} input - Section ID or config object
|
|
9
|
+
* @param {Function} getSectionMetadata - Function to get metadata for a section ID
|
|
10
|
+
* @returns {{id: string, title: string, summary: string, module: string}} Section entry
|
|
11
|
+
*/
|
|
12
|
+
export function sectionEntry(input, getSectionMetadata) {
|
|
13
|
+
const sectionId = typeof input === 'string' ? input : input.id;
|
|
14
|
+
const overrides = typeof input === 'string' ? {} : { title: input.title, summary: input.summary };
|
|
15
|
+
const meta = getSectionMetadata(sectionId, overrides);
|
|
16
|
+
return {
|
|
17
|
+
id: sectionId,
|
|
18
|
+
title: meta.title,
|
|
19
|
+
summary: meta.summary,
|
|
20
|
+
module: `./sections/${sectionId}.js`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a group entry with subsections.
|
|
26
|
+
* @param {{id: string, title: string, summary: string, sections: Array}} config - Group config
|
|
27
|
+
* @param {Function} getSectionMetadata - Function to get metadata for section IDs
|
|
28
|
+
* @returns {{id: string, title: string, summary: string, subsections: Array}} Group entry
|
|
29
|
+
*/
|
|
30
|
+
export function groupEntry(config, getSectionMetadata) {
|
|
31
|
+
const { id, title, summary, sections } = config;
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
title,
|
|
35
|
+
summary,
|
|
36
|
+
subsections: sections.map((s) => sectionEntry(s, getSectionMetadata))
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a section index from a manifest array.
|
|
42
|
+
* @param {Array} manifest - Array of section/group entries
|
|
43
|
+
* @returns {Map} Map of section ID to entry (with parentId set on subsections)
|
|
44
|
+
*/
|
|
45
|
+
export function buildSectionIndex(manifest) {
|
|
46
|
+
const index = new Map();
|
|
47
|
+
|
|
48
|
+
function registerEntry(entry, parentId = null) {
|
|
49
|
+
if (parentId) entry.parentId = parentId;
|
|
50
|
+
index.set(entry.id, entry);
|
|
51
|
+
if (Array.isArray(entry.subsections)) {
|
|
52
|
+
entry.subsections.forEach((subsection) => registerEntry(subsection, entry.id));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
manifest.forEach((entry) => registerEntry(entry));
|
|
57
|
+
return index;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a findSection function from an index.
|
|
62
|
+
* @param {Map} index - Section index map
|
|
63
|
+
* @returns {Function} Function that takes ID and returns entry or null
|
|
64
|
+
*/
|
|
65
|
+
export function createFindSection(index) {
|
|
66
|
+
return function findSection(id) {
|
|
67
|
+
return index.get(id) || null;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router and navigation utilities.
|
|
3
|
+
* Pure functions - no DOM dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract current section ID from URL hash.
|
|
8
|
+
* @param {string} hash - URL hash (e.g., '#section-id' or '')
|
|
9
|
+
* @param {string} defaultSection - Default section ID if hash is empty
|
|
10
|
+
* @returns {string} Section ID
|
|
11
|
+
*/
|
|
12
|
+
export function currentSectionId(hash, defaultSection) {
|
|
13
|
+
return hash.replace('#', '') || defaultSection;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve target section and parent group from an ID.
|
|
18
|
+
* If ID is a group with subsections, redirects to first subsection.
|
|
19
|
+
* @param {string} id - Section ID to resolve
|
|
20
|
+
* @param {Function} findSection - Function to look up section by ID
|
|
21
|
+
* @returns {{targetId: string, parentId: string|null}} Resolved target and parent
|
|
22
|
+
*/
|
|
23
|
+
export function resolveTarget(id, findSection) {
|
|
24
|
+
const entry = findSection(id);
|
|
25
|
+
if (!entry) return { targetId: id, parentId: null };
|
|
26
|
+
// If it's a group with subsections but no module, redirect to first subsection
|
|
27
|
+
if (!entry.module && entry.subsections && entry.subsections.length) {
|
|
28
|
+
return { targetId: entry.subsections[0].id, parentId: entry.id };
|
|
29
|
+
}
|
|
30
|
+
return { targetId: entry.id, parentId: entry.parentId || null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve full entry information from an ID.
|
|
35
|
+
* @param {string} id - Section ID to resolve
|
|
36
|
+
* @param {Function} findSection - Function to look up section by ID
|
|
37
|
+
* @returns {{entry: object, targetId: string, parentId: string|null}|null} Resolved entry or null
|
|
38
|
+
*/
|
|
39
|
+
export function resolveEntry(id, findSection) {
|
|
40
|
+
const { targetId, parentId } = resolveTarget(id, findSection);
|
|
41
|
+
const entry = findSection(targetId);
|
|
42
|
+
if (!entry) return null;
|
|
43
|
+
return { entry, targetId, parentId };
|
|
44
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search and filtering utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Cache for loaded section content
|
|
6
|
+
let searchIndex = null;
|
|
7
|
+
let indexPromise = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Escape special regex characters in a string.
|
|
11
|
+
* @param {string} value - String to escape
|
|
12
|
+
* @returns {string} Escaped string safe for use in RegExp
|
|
13
|
+
*/
|
|
14
|
+
export function escapeRegExp(value) {
|
|
15
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Flatten manifest into searchable sections.
|
|
20
|
+
* Returns all navigable sections (those with module paths).
|
|
21
|
+
* Recursively handles deeply nested subsections.
|
|
22
|
+
* @param {Array} manifest - Nested manifest array
|
|
23
|
+
* @param {string} [parentGroup] - Parent group title for tracking hierarchy
|
|
24
|
+
* @returns {Array} Flat array of searchable sections
|
|
25
|
+
*/
|
|
26
|
+
export function flattenManifest(manifest, parentGroup = '') {
|
|
27
|
+
const flat = [];
|
|
28
|
+
for (const entry of manifest) {
|
|
29
|
+
const groupLabel = parentGroup ? `${parentGroup} > ${entry.title}` : entry.title;
|
|
30
|
+
const hasSubsections = Array.isArray(entry.subsections) && entry.subsections.length > 0;
|
|
31
|
+
// Include an entry as a searchable section when it has a module (a
|
|
32
|
+
// navigable leaf in production manifests) OR when it is a leaf with no
|
|
33
|
+
// subsections (flat manifests carry navigable sections without a module
|
|
34
|
+
// field). Group entries — those with subsections — are containers, not
|
|
35
|
+
// navigable targets, so they are not included directly; their children
|
|
36
|
+
// are picked up by the recursion below.
|
|
37
|
+
if (entry.module || !hasSubsections) {
|
|
38
|
+
flat.push({
|
|
39
|
+
...entry,
|
|
40
|
+
group: parentGroup || entry.title
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Recursively include all subsections
|
|
44
|
+
if (hasSubsections) {
|
|
45
|
+
const nested = flattenManifest(entry.subsections, groupLabel);
|
|
46
|
+
flat.push(...nested);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return flat;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract plain text from HTML string.
|
|
54
|
+
* @param {string} html - HTML content
|
|
55
|
+
* @returns {string} Plain text
|
|
56
|
+
*/
|
|
57
|
+
function extractText(html) {
|
|
58
|
+
const div = document.createElement('div');
|
|
59
|
+
div.innerHTML = html;
|
|
60
|
+
return div.textContent || div.innerText || '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build search index by loading all section modules.
|
|
65
|
+
* @param {Array} manifest - The manifest array
|
|
66
|
+
* @returns {Promise<Array>} Indexed sections with content
|
|
67
|
+
*/
|
|
68
|
+
export async function buildSearchIndex(manifest) {
|
|
69
|
+
if (searchIndex) return searchIndex;
|
|
70
|
+
if (indexPromise) return indexPromise;
|
|
71
|
+
|
|
72
|
+
indexPromise = (async () => {
|
|
73
|
+
const flat = flattenManifest(manifest);
|
|
74
|
+
const indexed = await Promise.all(
|
|
75
|
+
flat.map(async (section) => {
|
|
76
|
+
let content = '';
|
|
77
|
+
try {
|
|
78
|
+
if (section.module) {
|
|
79
|
+
// Adjust path: module paths are relative to root, but we're in lib/
|
|
80
|
+
const modulePath = section.module.replace('./', '../');
|
|
81
|
+
const mod = await import(modulePath);
|
|
82
|
+
if (mod.load) {
|
|
83
|
+
const result = await mod.load();
|
|
84
|
+
content = extractText(result.html || '');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
// Module failed to load, search by title/summary only
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...section,
|
|
92
|
+
searchContent: `${section.title || ''} ${section.summary || ''} ${section.group || ''} ${content}`.toLowerCase()
|
|
93
|
+
};
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
searchIndex = indexed;
|
|
97
|
+
return indexed;
|
|
98
|
+
})();
|
|
99
|
+
|
|
100
|
+
return indexPromise;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Filter sections by search query (title/summary only, synchronous).
|
|
105
|
+
* @param {Array} manifest - Array of section objects
|
|
106
|
+
* @param {string} query - Search query
|
|
107
|
+
* @returns {Array} Filtered sections
|
|
108
|
+
*/
|
|
109
|
+
export function filterSections(manifest, query) {
|
|
110
|
+
const flat = flattenManifest(manifest);
|
|
111
|
+
const q = query.trim().toLowerCase();
|
|
112
|
+
if (!q) return flat;
|
|
113
|
+
return flat.filter((section) => {
|
|
114
|
+
const haystack = `${section.title || ''} ${section.summary || ''} ${section.group || ''}`.toLowerCase();
|
|
115
|
+
return haystack.includes(q);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Full-text search across all section content.
|
|
121
|
+
* Loads all modules on first call (cached thereafter).
|
|
122
|
+
* @param {Array} manifest - The manifest array
|
|
123
|
+
* @param {string} query - Search query
|
|
124
|
+
* @returns {Promise<Array>} Matching sections
|
|
125
|
+
*/
|
|
126
|
+
export async function searchContent(manifest, query) {
|
|
127
|
+
const index = await buildSearchIndex(manifest);
|
|
128
|
+
const q = query.trim().toLowerCase();
|
|
129
|
+
if (!q) return index;
|
|
130
|
+
return index.filter((section) => section.searchContent.includes(q));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse search query into individual terms.
|
|
135
|
+
* @param {string} query - Search query
|
|
136
|
+
* @returns {Array<string>} Array of search terms
|
|
137
|
+
*/
|
|
138
|
+
export function parseSearchTerms(query) {
|
|
139
|
+
return query.split(/\s+/).map((term) => term.trim()).filter(Boolean);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Find the preferred index in filtered entries based on current section.
|
|
144
|
+
* @param {Array} entries - Filtered section entries
|
|
145
|
+
* @param {string} currentId - Current section ID
|
|
146
|
+
* @returns {number} Index to select (0 if current not found)
|
|
147
|
+
*/
|
|
148
|
+
export function findPreferredIndex(entries, currentId) {
|
|
149
|
+
const index = entries.findIndex((entry) => entry.id === currentId);
|
|
150
|
+
return index >= 0 ? index : 0;
|
|
151
|
+
}
|