@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.
Files changed (147) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +337 -0
  3. package/bin/pagenary.mjs +116 -0
  4. package/build.config.json +5 -0
  5. package/package.json +66 -0
  6. package/scripts/build-site.js +87 -0
  7. package/scripts/build-tenants.js +3569 -0
  8. package/scripts/build.js +99 -0
  9. package/scripts/generate-sections.js +41 -0
  10. package/scripts/lib/seo-generator.js +558 -0
  11. package/scripts/lint-content.js +62 -0
  12. package/scripts/seo-smoke.js +94 -0
  13. package/scripts/serve.js +142 -0
  14. package/site/app.js +1 -0
  15. package/site/index.html +57 -0
  16. package/site/lib/categories.js +1 -0
  17. package/site/lib/export.js +1 -0
  18. package/site/lib/manifest-utils.js +1 -0
  19. package/site/lib/router.js +1 -0
  20. package/site/lib/search.js +1 -0
  21. package/site/llms.txt +22 -0
  22. package/site/manifest.js +132 -0
  23. package/site/mermaid-init.js +1 -0
  24. package/site/pages/api.html +339 -0
  25. package/site/pages/architecture.html +303 -0
  26. package/site/pages/deployment.html +282 -0
  27. package/site/pages/developer-guide.html +157 -0
  28. package/site/pages/extending.html +135 -0
  29. package/site/pages/quickstart.html +318 -0
  30. package/site/pages/seo-strategy.html +121 -0
  31. package/site/pages/tenant-config.html +519 -0
  32. package/site/pages/welcome.html +116 -0
  33. package/site/robots.txt +10 -0
  34. package/site/sections/api.js +3 -0
  35. package/site/sections/architecture.js +3 -0
  36. package/site/sections/deployment.js +3 -0
  37. package/site/sections/developer-guide.js +3 -0
  38. package/site/sections/extending.js +3 -0
  39. package/site/sections/quickstart.js +3 -0
  40. package/site/sections/section-templates.js +1 -0
  41. package/site/sections/seo-strategy.js +3 -0
  42. package/site/sections/tenant-config.js +3 -0
  43. package/site/sections/welcome.js +3 -0
  44. package/site/seo.js +1 -0
  45. package/site/sitemap.xml +63 -0
  46. package/site/styles.css +1982 -0
  47. package/site/syntax-highlight.js +1 -0
  48. package/src/app.js +988 -0
  49. package/src/index.html +56 -0
  50. package/src/lib/categories.js +55 -0
  51. package/src/lib/export.js +195 -0
  52. package/src/lib/manifest-utils.js +69 -0
  53. package/src/lib/router.js +44 -0
  54. package/src/lib/search.js +151 -0
  55. package/src/manifest.js +246 -0
  56. package/src/mermaid-init.js +207 -0
  57. package/src/sections/archive-future-roadmap.js +7 -0
  58. package/src/sections/archive-initiative-alpha.js +7 -0
  59. package/src/sections/archive-milestone-records.js +7 -0
  60. package/src/sections/archive-timeline-overview.js +7 -0
  61. package/src/sections/core-technology-compliance-frameworks.js +7 -0
  62. package/src/sections/core-technology-coordination-model.js +7 -0
  63. package/src/sections/core-technology-data-definitions.js +7 -0
  64. package/src/sections/core-technology-hardware-integration.js +7 -0
  65. package/src/sections/core-technology-integrity-controls.js +7 -0
  66. package/src/sections/core-technology-network-topology.js +7 -0
  67. package/src/sections/core-technology-operator-requirements.js +7 -0
  68. package/src/sections/core-technology-overview.js +7 -0
  69. package/src/sections/core-technology-service-interfaces.js +7 -0
  70. package/src/sections/core-technology-synchronization-strategy.js +7 -0
  71. package/src/sections/core-technology-system-foundation.js +7 -0
  72. package/src/sections/developers-api-credentials.js +7 -0
  73. package/src/sections/developers-api-operations.js +7 -0
  74. package/src/sections/developers-api-reference.js +7 -0
  75. package/src/sections/developers-api-websocket.js +7 -0
  76. package/src/sections/developers-automation-blueprints.js +7 -0
  77. package/src/sections/developers-automation-modules.js +7 -0
  78. package/src/sections/developers-automation-patterns.js +7 -0
  79. package/src/sections/developers-deployment-playbook.js +7 -0
  80. package/src/sections/developers-overview.js +7 -0
  81. package/src/sections/developers-scheduling-patterns.js +7 -0
  82. package/src/sections/developers-sdk-go.js +7 -0
  83. package/src/sections/developers-sdk-javascript.js +7 -0
  84. package/src/sections/developers-sdk-python.js +7 -0
  85. package/src/sections/developers-sdk-rust.js +7 -0
  86. package/src/sections/developers-sdks.js +7 -0
  87. package/src/sections/developers-solution-examples.js +7 -0
  88. package/src/sections/developers-testing-framework.js +7 -0
  89. package/src/sections/getting-started-architecture-basics.js +7 -0
  90. package/src/sections/getting-started-introduction.js +7 -0
  91. package/src/sections/getting-started-performance-overview.js +7 -0
  92. package/src/sections/governance-community-initiatives.js +7 -0
  93. package/src/sections/governance-dao-overview.js +7 -0
  94. package/src/sections/governance-multi-token.js +7 -0
  95. package/src/sections/governance-overview.js +7 -0
  96. package/src/sections/governance-proposal-process.js +7 -0
  97. package/src/sections/governance-proposals.js +7 -0
  98. package/src/sections/governance-structure.js +7 -0
  99. package/src/sections/governance-token-distribution.js +7 -0
  100. package/src/sections/governance-treasury.js +7 -0
  101. package/src/sections/operations-environment-prep.js +7 -0
  102. package/src/sections/operations-getting-started.js +7 -0
  103. package/src/sections/operations-incentives-guide.js +7 -0
  104. package/src/sections/operations-incentives-strategies.js +7 -0
  105. package/src/sections/operations-incentives.js +7 -0
  106. package/src/sections/operations-infrastructure.js +7 -0
  107. package/src/sections/operations-monitoring.js +7 -0
  108. package/src/sections/operations-overview.js +7 -0
  109. package/src/sections/operations-performance.js +7 -0
  110. package/src/sections/operations-power-infrastructure.js +7 -0
  111. package/src/sections/operations-setup-guide.js +7 -0
  112. package/src/sections/operations-sync-setup.js +7 -0
  113. package/src/sections/products-flagship-solution.js +7 -0
  114. package/src/sections/products-solution-library.js +7 -0
  115. package/src/sections/resources-brand-assets.js +7 -0
  116. package/src/sections/resources-faq.js +7 -0
  117. package/src/sections/resources-glossary.js +7 -0
  118. package/src/sections/resources-research-papers.js +7 -0
  119. package/src/sections/section-templates.js +873 -0
  120. package/src/sections/security-audits.js +7 -0
  121. package/src/sections/security-best-practices.js +7 -0
  122. package/src/sections/security-bug-bounty.js +7 -0
  123. package/src/sections/security-incident-response.js +7 -0
  124. package/src/sections/security-overview.js +7 -0
  125. package/src/sections/technical-architecture.js +7 -0
  126. package/src/sections/technical-whitepaper.js +7 -0
  127. package/src/sections/tutorial-automation-bot.js +7 -0
  128. package/src/sections/tutorial-build-first-integration.js +7 -0
  129. package/src/sections/tutorial-deploy-automation.js +7 -0
  130. package/src/sections/tutorial-event-driven-experience.js +7 -0
  131. package/src/sections/tutorial-operations-onboarding.js +7 -0
  132. package/src/sections/tutorial-systems-integration.js +7 -0
  133. package/src/sections/tutorials-overview.js +7 -0
  134. package/src/sections/use-case-connected-devices.js +7 -0
  135. package/src/sections/use-case-digital-auctions.js +7 -0
  136. package/src/sections/use-case-financial-automation.js +7 -0
  137. package/src/sections/use-case-interactive-media.js +7 -0
  138. package/src/sections/use-case-realtime-execution.js +7 -0
  139. package/src/sections/use-case-research-analytics.js +7 -0
  140. package/src/sections/use-case-supply-operations.js +7 -0
  141. package/src/sections/use-cases-overview.js +7 -0
  142. package/src/sections/welcome-overview.js +7 -0
  143. package/src/seo.js +90 -0
  144. package/src/styles.css +1982 -0
  145. package/src/syntax-highlight.js +90 -0
  146. package/tenants.json.example +68 -0
  147. 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
+ }