@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/app.js
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
import { MANIFEST, DEFAULT_SECTION, findSection, getAdjacentSections, SITE_CONFIG, EXPORT_CONFIG } from './manifest.js';
|
|
2
|
+
import { updateMetaTags } from './seo.js';
|
|
3
|
+
import { escapeRegExp, searchContent, flattenManifest, findPreferredIndex } from './lib/search.js';
|
|
4
|
+
import { resolveTarget as resolveTargetFn, resolveEntry as resolveEntryFn } from './lib/router.js';
|
|
5
|
+
import { composeExportDocument, collectExportableSections } from './lib/export.js';
|
|
6
|
+
import { renderMermaidBlocks } from './mermaid-init.js';
|
|
7
|
+
import { highlightCodeBlocks } from './syntax-highlight.js';
|
|
8
|
+
|
|
9
|
+
const app = document.getElementById('app');
|
|
10
|
+
const nav = document.getElementById('nav');
|
|
11
|
+
const yearMarker = document.getElementById('year');
|
|
12
|
+
const exportBtn = document.getElementById('exportBtn');
|
|
13
|
+
const commandToggle = document.getElementById('commandToggle');
|
|
14
|
+
const commandPalette = document.getElementById('commandPalette');
|
|
15
|
+
const commandInput = document.getElementById('commandInput');
|
|
16
|
+
const commandList = document.getElementById('commandList');
|
|
17
|
+
const mobileMenuToggle = document.getElementById('mobileMenuToggle');
|
|
18
|
+
const sidebar = document.querySelector('.sidebar');
|
|
19
|
+
|
|
20
|
+
const COMMAND_QUERY_KEY = 'docs-toolkit-command-query';
|
|
21
|
+
|
|
22
|
+
const rendered = new Map();
|
|
23
|
+
const navButtons = new Map();
|
|
24
|
+
const navGroups = new Map();
|
|
25
|
+
const expandedGroups = new Set();
|
|
26
|
+
let commandEntries = [];
|
|
27
|
+
let commandIndex = 0;
|
|
28
|
+
let paletteOpen = false;
|
|
29
|
+
let highlightQuery = (localStorage.getItem(COMMAND_QUERY_KEY) || '').trim();
|
|
30
|
+
let pendingHighlightScroll = false;
|
|
31
|
+
|
|
32
|
+
function createExternalLink(item, className) {
|
|
33
|
+
const link = document.createElement('a');
|
|
34
|
+
link.href = item.url;
|
|
35
|
+
link.target = '_blank';
|
|
36
|
+
link.rel = 'noopener noreferrer';
|
|
37
|
+
link.className = `${className} nav-external`;
|
|
38
|
+
link.title = item.summary || item.title;
|
|
39
|
+
link.innerHTML = `
|
|
40
|
+
<span class="nav-title">${item.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>
|
|
41
|
+
${item.summary ? `<span class="nav-summary">${item.summary}</span>` : ''}
|
|
42
|
+
`;
|
|
43
|
+
return link;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function initNav() {
|
|
47
|
+
nav.innerHTML = '';
|
|
48
|
+
navButtons.clear();
|
|
49
|
+
navGroups.clear();
|
|
50
|
+
let groupApplied = expandedGroups.size > 0;
|
|
51
|
+
MANIFEST.forEach((section, index) => {
|
|
52
|
+
// Handle external links at top level
|
|
53
|
+
if (section.url) {
|
|
54
|
+
const link = createExternalLink(section, 'nav-leaf');
|
|
55
|
+
nav.appendChild(link);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (section.subsections && section.subsections.length) {
|
|
60
|
+
const group = document.createElement('div');
|
|
61
|
+
group.className = 'nav-group';
|
|
62
|
+
|
|
63
|
+
// Check if section has content (module) in addition to subsections
|
|
64
|
+
const hasContent = Boolean(section.module);
|
|
65
|
+
|
|
66
|
+
const parentBtn = document.createElement('button');
|
|
67
|
+
parentBtn.type = 'button';
|
|
68
|
+
parentBtn.className = 'nav-parent' + (hasContent ? ' nav-parent-with-content' : '');
|
|
69
|
+
parentBtn.dataset.section = section.id;
|
|
70
|
+
parentBtn.title = section.summary;
|
|
71
|
+
|
|
72
|
+
if (hasContent) {
|
|
73
|
+
// Section has content - title navigates, arrow toggles
|
|
74
|
+
parentBtn.innerHTML = `
|
|
75
|
+
<span class="nav-title-link">${section.title}</span>
|
|
76
|
+
<span class="nav-expand-toggle" aria-label="Expand"></span>
|
|
77
|
+
${section.summary ? `<span class="nav-summary">${section.summary}</span>` : ''}
|
|
78
|
+
`;
|
|
79
|
+
// Title click navigates to content
|
|
80
|
+
parentBtn.querySelector('.nav-title-link').addEventListener('click', (e) => {
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
navigate(section.id, { scrollToHighlight: Boolean(highlightQuery) });
|
|
83
|
+
});
|
|
84
|
+
// Arrow click toggles expansion
|
|
85
|
+
parentBtn.querySelector('.nav-expand-toggle').addEventListener('click', (e) => {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
const next = !expandedGroups.has(section.id);
|
|
88
|
+
setGroupExpanded(section.id, next);
|
|
89
|
+
});
|
|
90
|
+
// Button itself does nothing (handled by children)
|
|
91
|
+
parentBtn.addEventListener('click', (e) => {
|
|
92
|
+
// Only toggle if clicking the button background, not title or arrow
|
|
93
|
+
if (e.target === parentBtn) {
|
|
94
|
+
const next = !expandedGroups.has(section.id);
|
|
95
|
+
setGroupExpanded(section.id, next);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
// No content - whole button toggles expansion
|
|
100
|
+
parentBtn.innerHTML = `
|
|
101
|
+
<span class="nav-title">${section.title}</span>
|
|
102
|
+
${section.summary ? `<span class="nav-summary">${section.summary}</span>` : ''}
|
|
103
|
+
`;
|
|
104
|
+
parentBtn.addEventListener('click', () => {
|
|
105
|
+
const next = !expandedGroups.has(section.id);
|
|
106
|
+
setGroupExpanded(section.id, next);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const list = document.createElement('div');
|
|
111
|
+
list.className = 'nav-sublist';
|
|
112
|
+
section.subsections.forEach((sub) => {
|
|
113
|
+
// Handle external links in subsections
|
|
114
|
+
if (sub.url) {
|
|
115
|
+
const link = createExternalLink(sub, 'nav-item');
|
|
116
|
+
list.appendChild(link);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle nested subsections (3-level nav)
|
|
121
|
+
if (sub.subsections && sub.subsections.length) {
|
|
122
|
+
const nestedGroup = document.createElement('div');
|
|
123
|
+
nestedGroup.className = 'nav-group nav-group-nested';
|
|
124
|
+
|
|
125
|
+
const nestedParentBtn = document.createElement('button');
|
|
126
|
+
nestedParentBtn.type = 'button';
|
|
127
|
+
nestedParentBtn.className = 'nav-parent nav-parent-nested';
|
|
128
|
+
nestedParentBtn.dataset.section = sub.id;
|
|
129
|
+
nestedParentBtn.title = sub.summary || sub.title;
|
|
130
|
+
nestedParentBtn.innerHTML = `<span class="nav-title">${sub.title}</span>`;
|
|
131
|
+
nestedParentBtn.addEventListener('click', () => {
|
|
132
|
+
const next = !expandedGroups.has(sub.id);
|
|
133
|
+
setGroupExpanded(sub.id, next);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const nestedList = document.createElement('div');
|
|
137
|
+
nestedList.className = 'nav-sublist nav-sublist-nested';
|
|
138
|
+
sub.subsections.forEach((nested) => {
|
|
139
|
+
if (nested.url) {
|
|
140
|
+
const link = createExternalLink(nested, 'nav-item');
|
|
141
|
+
nestedList.appendChild(link);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle 4th level of nesting (deeply nested subsections)
|
|
146
|
+
if (nested.subsections && nested.subsections.length) {
|
|
147
|
+
const deepGroup = document.createElement('div');
|
|
148
|
+
deepGroup.className = 'nav-group nav-group-deep';
|
|
149
|
+
|
|
150
|
+
const deepParentBtn = document.createElement('button');
|
|
151
|
+
deepParentBtn.type = 'button';
|
|
152
|
+
deepParentBtn.className = 'nav-parent nav-parent-deep';
|
|
153
|
+
deepParentBtn.dataset.section = nested.id;
|
|
154
|
+
deepParentBtn.title = nested.summary || nested.title;
|
|
155
|
+
deepParentBtn.innerHTML = `<span class="nav-title">${nested.title}</span>`;
|
|
156
|
+
deepParentBtn.addEventListener('click', () => {
|
|
157
|
+
const next = !expandedGroups.has(nested.id);
|
|
158
|
+
setGroupExpanded(nested.id, next);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const deepList = document.createElement('div');
|
|
162
|
+
deepList.className = 'nav-sublist nav-sublist-deep';
|
|
163
|
+
nested.subsections.forEach((deep) => {
|
|
164
|
+
if (deep.url) {
|
|
165
|
+
const link = createExternalLink(deep, 'nav-item');
|
|
166
|
+
deepList.appendChild(link);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle 5th level of nesting (ultra-deep subsections)
|
|
171
|
+
if (deep.subsections && deep.subsections.length) {
|
|
172
|
+
const ultraGroup = document.createElement('div');
|
|
173
|
+
ultraGroup.className = 'nav-group nav-group-ultra';
|
|
174
|
+
|
|
175
|
+
const ultraParentBtn = document.createElement('button');
|
|
176
|
+
ultraParentBtn.type = 'button';
|
|
177
|
+
ultraParentBtn.className = 'nav-parent nav-parent-ultra';
|
|
178
|
+
ultraParentBtn.dataset.section = deep.id;
|
|
179
|
+
ultraParentBtn.title = deep.summary || deep.title;
|
|
180
|
+
ultraParentBtn.innerHTML = `<span class="nav-title">${deep.title}</span>`;
|
|
181
|
+
ultraParentBtn.addEventListener('click', () => {
|
|
182
|
+
const next = !expandedGroups.has(deep.id);
|
|
183
|
+
setGroupExpanded(deep.id, next);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const ultraList = document.createElement('div');
|
|
187
|
+
ultraList.className = 'nav-sublist nav-sublist-ultra';
|
|
188
|
+
deep.subsections.forEach((ultra) => {
|
|
189
|
+
if (ultra.url) {
|
|
190
|
+
const link = createExternalLink(ultra, 'nav-item');
|
|
191
|
+
ultraList.appendChild(link);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const ultraBtn = document.createElement('button');
|
|
195
|
+
ultraBtn.type = 'button';
|
|
196
|
+
ultraBtn.className = 'nav-item nav-item-ultra' + (ultra.type ? ` nav-type-${ultra.type}` : '');
|
|
197
|
+
ultraBtn.dataset.section = ultra.id;
|
|
198
|
+
ultraBtn.title = ultra.summary || ultra.title;
|
|
199
|
+
ultraBtn.innerHTML = `
|
|
200
|
+
<span class="nav-title">${ultra.title}</span>
|
|
201
|
+
<span class="nav-summary">${ultra.summary || ''}</span>
|
|
202
|
+
`;
|
|
203
|
+
ultraBtn.addEventListener('click', () => navigate(ultra.id, { scrollToHighlight: Boolean(highlightQuery) }));
|
|
204
|
+
ultraList.appendChild(ultraBtn);
|
|
205
|
+
navButtons.set(ultra.id, ultraBtn);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
ultraGroup.append(ultraParentBtn, ultraList);
|
|
209
|
+
deepList.appendChild(ultraGroup);
|
|
210
|
+
navButtons.set(deep.id, ultraParentBtn);
|
|
211
|
+
navGroups.set(deep.id, { group: ultraGroup, button: ultraParentBtn, list: ultraList });
|
|
212
|
+
const ultraShouldExpand = expandedGroups.has(deep.id) && !deep.collapsed;
|
|
213
|
+
setGroupExpanded(deep.id, ultraShouldExpand);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const deepBtn = document.createElement('button');
|
|
218
|
+
deepBtn.type = 'button';
|
|
219
|
+
deepBtn.className = 'nav-item nav-item-deep' + (deep.type ? ` nav-type-${deep.type}` : '');
|
|
220
|
+
deepBtn.dataset.section = deep.id;
|
|
221
|
+
deepBtn.title = deep.summary || deep.title;
|
|
222
|
+
deepBtn.innerHTML = `
|
|
223
|
+
<span class="nav-title">${deep.title}${deep.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
|
|
224
|
+
<span class="nav-summary">${deep.summary || ''}</span>
|
|
225
|
+
`;
|
|
226
|
+
deepBtn.addEventListener('click', () => navigate(deep.id, { scrollToHighlight: Boolean(highlightQuery) }));
|
|
227
|
+
deepList.appendChild(deepBtn);
|
|
228
|
+
navButtons.set(deep.id, deepBtn);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
deepGroup.append(deepParentBtn, deepList);
|
|
232
|
+
nestedList.appendChild(deepGroup);
|
|
233
|
+
navButtons.set(nested.id, deepParentBtn);
|
|
234
|
+
navGroups.set(nested.id, { group: deepGroup, button: deepParentBtn, list: deepList });
|
|
235
|
+
const deepShouldExpand = expandedGroups.has(nested.id) && !nested.collapsed;
|
|
236
|
+
setGroupExpanded(nested.id, deepShouldExpand);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const nestedBtn = document.createElement('button');
|
|
241
|
+
nestedBtn.type = 'button';
|
|
242
|
+
nestedBtn.className = 'nav-item nav-item-nested' + (nested.type ? ` nav-type-${nested.type}` : '');
|
|
243
|
+
nestedBtn.dataset.section = nested.id;
|
|
244
|
+
nestedBtn.title = nested.summary || nested.title;
|
|
245
|
+
nestedBtn.innerHTML = `
|
|
246
|
+
<span class="nav-title">${nested.title}${nested.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
|
|
247
|
+
<span class="nav-summary">${nested.summary || ''}</span>
|
|
248
|
+
`;
|
|
249
|
+
nestedBtn.addEventListener('click', () => navigate(nested.id, { scrollToHighlight: Boolean(highlightQuery) }));
|
|
250
|
+
nestedList.appendChild(nestedBtn);
|
|
251
|
+
navButtons.set(nested.id, nestedBtn);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
nestedGroup.append(nestedParentBtn, nestedList);
|
|
255
|
+
list.appendChild(nestedGroup);
|
|
256
|
+
navButtons.set(sub.id, nestedParentBtn);
|
|
257
|
+
navGroups.set(sub.id, { group: nestedGroup, button: nestedParentBtn, list: nestedList });
|
|
258
|
+
// Initialize nested group expansion state (collapsed by default, or if marked collapsed)
|
|
259
|
+
const nestedShouldExpand = expandedGroups.has(sub.id) && !sub.collapsed;
|
|
260
|
+
setGroupExpanded(sub.id, nestedShouldExpand);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const childBtn = document.createElement('button');
|
|
265
|
+
childBtn.type = 'button';
|
|
266
|
+
childBtn.className = 'nav-item' + (sub.type ? ` nav-type-${sub.type}` : '');
|
|
267
|
+
childBtn.dataset.section = sub.id;
|
|
268
|
+
childBtn.title = sub.summary;
|
|
269
|
+
childBtn.innerHTML = `
|
|
270
|
+
<span class="nav-title">${sub.title}${sub.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
|
|
271
|
+
<span class="nav-summary">${sub.summary}</span>
|
|
272
|
+
`;
|
|
273
|
+
childBtn.addEventListener('click', () => navigate(sub.id, { scrollToHighlight: Boolean(highlightQuery) }));
|
|
274
|
+
list.appendChild(childBtn);
|
|
275
|
+
navButtons.set(sub.id, childBtn);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
group.append(parentBtn, list);
|
|
279
|
+
nav.appendChild(group);
|
|
280
|
+
navButtons.set(section.id, parentBtn);
|
|
281
|
+
navGroups.set(section.id, { group, button: parentBtn, list });
|
|
282
|
+
// Respect collapsed property - only expand if not marked collapsed AND (previously expanded OR first group)
|
|
283
|
+
const shouldExpand = !section.collapsed && (expandedGroups.has(section.id) || (!groupApplied && !expandedGroups.size));
|
|
284
|
+
setGroupExpanded(section.id, shouldExpand);
|
|
285
|
+
if (shouldExpand) groupApplied = true;
|
|
286
|
+
} else {
|
|
287
|
+
const button = document.createElement('button');
|
|
288
|
+
button.type = 'button';
|
|
289
|
+
button.className = 'nav-leaf' + (section.type ? ` nav-type-${section.type}` : '');
|
|
290
|
+
button.dataset.section = section.id;
|
|
291
|
+
button.title = section.summary;
|
|
292
|
+
button.innerHTML = `
|
|
293
|
+
<span class="nav-title">${section.title}${section.type === 'press-release' ? '<span class="nav-type-icon" aria-label="Press Release"></span>' : ''}</span>
|
|
294
|
+
<span class="nav-summary">${section.summary}</span>
|
|
295
|
+
`;
|
|
296
|
+
button.addEventListener('click', () => navigate(section.id, { scrollToHighlight: Boolean(highlightQuery) }));
|
|
297
|
+
nav.appendChild(button);
|
|
298
|
+
navButtons.set(section.id, button);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function navigate(id, options = {}) {
|
|
304
|
+
const { scrollToHighlight = false } = options;
|
|
305
|
+
const { targetId, parentId } = resolveTarget(id);
|
|
306
|
+
if (paletteOpen) closeCommandPalette();
|
|
307
|
+
if (parentId) {
|
|
308
|
+
setGroupExpanded(parentId, true);
|
|
309
|
+
}
|
|
310
|
+
pendingHighlightScroll = scrollToHighlight || Boolean(highlightQuery);
|
|
311
|
+
if (location.hash.replace('#', '') === targetId) {
|
|
312
|
+
handleRoute();
|
|
313
|
+
} else {
|
|
314
|
+
location.hash = `#${targetId}`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function currentSectionId() {
|
|
319
|
+
return location.hash.replace('#', '') || DEFAULT_SECTION;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function handleRoute() {
|
|
323
|
+
const currentId = currentSectionId();
|
|
324
|
+
const resolved = resolveEntry(currentId);
|
|
325
|
+
if (!resolved) return;
|
|
326
|
+
const { entry, targetId, parentId } = resolved;
|
|
327
|
+
if (targetId !== currentId) {
|
|
328
|
+
location.replace(`#${targetId}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (parentId) {
|
|
332
|
+
setGroupExpanded(parentId, true);
|
|
333
|
+
}
|
|
334
|
+
setActiveNav(entry.id, parentId);
|
|
335
|
+
await loadSection(entry, parentId);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function setActiveNav(activeId, parentId = null) {
|
|
339
|
+
navButtons.forEach((btn) => {
|
|
340
|
+
btn.setAttribute('aria-current', 'false');
|
|
341
|
+
});
|
|
342
|
+
const activeBtn = navButtons.get(activeId);
|
|
343
|
+
if (activeBtn) {
|
|
344
|
+
activeBtn.setAttribute('aria-current', 'page');
|
|
345
|
+
}
|
|
346
|
+
if (parentId) {
|
|
347
|
+
const parentBtn = navButtons.get(parentId);
|
|
348
|
+
if (parentBtn) parentBtn.setAttribute('aria-current', 'page');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function setGroupExpanded(id, expanded) {
|
|
353
|
+
if (!id) return;
|
|
354
|
+
const info = navGroups.get(id);
|
|
355
|
+
if (expanded) {
|
|
356
|
+
expandedGroups.add(id);
|
|
357
|
+
if (info) info.group.classList.add('expanded');
|
|
358
|
+
} else {
|
|
359
|
+
expandedGroups.delete(id);
|
|
360
|
+
if (info) info.group.classList.remove('expanded');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Wrapper functions that bind findSection to lib functions
|
|
365
|
+
function resolveTarget(id) {
|
|
366
|
+
return resolveTargetFn(id, findSection);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function resolveEntry(id) {
|
|
370
|
+
return resolveEntryFn(id, findSection);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function loadSection(entry) {
|
|
374
|
+
if (!entry) return;
|
|
375
|
+
const module = await import(entry.module);
|
|
376
|
+
const loader = module.load || module.default;
|
|
377
|
+
if (typeof loader !== 'function') {
|
|
378
|
+
app.innerHTML = `<article class="section"><p>Section failed to load.</p></article>`;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const payload = await loader();
|
|
382
|
+
app.innerHTML = payload.html || '';
|
|
383
|
+
|
|
384
|
+
// Render any mermaid diagrams in the content
|
|
385
|
+
await renderMermaidBlocks(app);
|
|
386
|
+
|
|
387
|
+
// Apply syntax highlighting to code blocks
|
|
388
|
+
await highlightCodeBlocks(app);
|
|
389
|
+
|
|
390
|
+
// Add bottom page navigation
|
|
391
|
+
renderBottomNav(entry.id);
|
|
392
|
+
|
|
393
|
+
// Scroll to top of content area
|
|
394
|
+
app.scrollTop = 0;
|
|
395
|
+
window.scrollTo(0, 0);
|
|
396
|
+
|
|
397
|
+
if (typeof payload.afterRender === 'function') {
|
|
398
|
+
payload.afterRender(app);
|
|
399
|
+
}
|
|
400
|
+
updateMetaTags({
|
|
401
|
+
title: entry.title,
|
|
402
|
+
description: entry.summary,
|
|
403
|
+
siteTitle: SITE_CONFIG.siteTitle,
|
|
404
|
+
siteUrl: SITE_CONFIG.siteUrl,
|
|
405
|
+
sectionId: entry.id
|
|
406
|
+
});
|
|
407
|
+
rendered.set(entry.id, Date.now());
|
|
408
|
+
const shouldScrollToHighlight = pendingHighlightScroll;
|
|
409
|
+
pendingHighlightScroll = false;
|
|
410
|
+
applyHighlight(shouldScrollToHighlight);
|
|
411
|
+
focusCanvas();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Render bottom page navigation (prev/next links)
|
|
416
|
+
* Visibility controlled by SITE_CONFIG.bottomNav: 'always' | 'mobile' | 'never'
|
|
417
|
+
* Per-section override via SITE_CONFIG.bottomNavSections array
|
|
418
|
+
*/
|
|
419
|
+
function renderBottomNav(currentId) {
|
|
420
|
+
// Remove existing bottom nav if present
|
|
421
|
+
const existing = app.querySelector('.bottom-nav');
|
|
422
|
+
if (existing) existing.remove();
|
|
423
|
+
|
|
424
|
+
// Check if bottom nav is disabled globally
|
|
425
|
+
if (SITE_CONFIG.bottomNav === 'never') return;
|
|
426
|
+
|
|
427
|
+
// Check for per-section override
|
|
428
|
+
const sectionOverrides = SITE_CONFIG.bottomNavSections || [];
|
|
429
|
+
const isSectionEnabled = sectionOverrides.some(prefix => currentId.startsWith(prefix));
|
|
430
|
+
const isMobileOnly = SITE_CONFIG.bottomNav === 'mobile' && !isSectionEnabled;
|
|
431
|
+
|
|
432
|
+
const { prev, next } = getAdjacentSections(currentId);
|
|
433
|
+
if (!prev && !next) return;
|
|
434
|
+
|
|
435
|
+
const nav = document.createElement('nav');
|
|
436
|
+
nav.className = 'bottom-nav';
|
|
437
|
+
if (isMobileOnly) {
|
|
438
|
+
nav.classList.add('mobile-only');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (prev) {
|
|
442
|
+
const prevWrapper = document.createElement('div');
|
|
443
|
+
prevWrapper.className = 'bottom-nav-item bottom-nav-prev';
|
|
444
|
+
prevWrapper.innerHTML = `<span class="bottom-nav-chevron">\u2039</span>`;
|
|
445
|
+
const prevLink = document.createElement('a');
|
|
446
|
+
prevLink.href = `#${prev.id}`;
|
|
447
|
+
prevLink.className = 'bottom-nav-link';
|
|
448
|
+
prevLink.title = `Previous: ${prev.title}`;
|
|
449
|
+
prevLink.textContent = prev.title;
|
|
450
|
+
prevLink.addEventListener('click', (e) => {
|
|
451
|
+
e.preventDefault();
|
|
452
|
+
navigate(prev.id);
|
|
453
|
+
});
|
|
454
|
+
prevWrapper.appendChild(prevLink);
|
|
455
|
+
nav.appendChild(prevWrapper);
|
|
456
|
+
} else {
|
|
457
|
+
// Empty spacer for alignment
|
|
458
|
+
const spacer = document.createElement('div');
|
|
459
|
+
spacer.className = 'bottom-nav-spacer';
|
|
460
|
+
nav.appendChild(spacer);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (next) {
|
|
464
|
+
const nextWrapper = document.createElement('div');
|
|
465
|
+
nextWrapper.className = 'bottom-nav-item bottom-nav-next';
|
|
466
|
+
const nextLink = document.createElement('a');
|
|
467
|
+
nextLink.href = `#${next.id}`;
|
|
468
|
+
nextLink.className = 'bottom-nav-link';
|
|
469
|
+
nextLink.title = `Next: ${next.title}`;
|
|
470
|
+
nextLink.textContent = next.title;
|
|
471
|
+
nextLink.addEventListener('click', (e) => {
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
navigate(next.id);
|
|
474
|
+
});
|
|
475
|
+
nextWrapper.appendChild(nextLink);
|
|
476
|
+
nextWrapper.innerHTML += `<span class="bottom-nav-chevron">\u203a</span>`;
|
|
477
|
+
nav.appendChild(nextWrapper);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Append to the section content
|
|
481
|
+
const section = app.querySelector('section') || app;
|
|
482
|
+
section.appendChild(nav);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function focusCanvas() {
|
|
486
|
+
requestAnimationFrame(() => app.focus());
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function boot() {
|
|
490
|
+
initNav();
|
|
491
|
+
if (highlightQuery) {
|
|
492
|
+
pendingHighlightScroll = true;
|
|
493
|
+
}
|
|
494
|
+
window.addEventListener('hashchange', () => {
|
|
495
|
+
if (highlightQuery) {
|
|
496
|
+
pendingHighlightScroll = true;
|
|
497
|
+
}
|
|
498
|
+
handleRoute();
|
|
499
|
+
});
|
|
500
|
+
yearMarker.textContent = new Date().getFullYear();
|
|
501
|
+
handleRoute();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
boot();
|
|
505
|
+
|
|
506
|
+
// Command palette
|
|
507
|
+
if (commandToggle && commandPalette && commandInput && commandList) {
|
|
508
|
+
commandToggle.addEventListener('click', () => {
|
|
509
|
+
if (paletteOpen) closeCommandPalette();
|
|
510
|
+
else openCommandPalette();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
commandInput.addEventListener('input', () => {
|
|
514
|
+
const value = commandInput.value;
|
|
515
|
+
setHighlightQuery(value, true);
|
|
516
|
+
updateCommandEntries(value);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
commandInput.addEventListener('keydown', (event) => {
|
|
520
|
+
const max = commandEntries.length - 1;
|
|
521
|
+
if (event.key === 'ArrowDown') {
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
commandIndex = Math.min(max, commandIndex + 1);
|
|
524
|
+
reflectCommandSelection();
|
|
525
|
+
} else if (event.key === 'ArrowUp') {
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
commandIndex = Math.max(0, commandIndex - 1);
|
|
528
|
+
reflectCommandSelection();
|
|
529
|
+
} else if (event.key === 'Enter') {
|
|
530
|
+
event.preventDefault();
|
|
531
|
+
const target = commandEntries[commandIndex];
|
|
532
|
+
if (target) {
|
|
533
|
+
setHighlightQuery(commandInput.value, true);
|
|
534
|
+
navigate(target.id, { scrollToHighlight: true });
|
|
535
|
+
closeCommandPalette();
|
|
536
|
+
}
|
|
537
|
+
} else if (event.key === 'Escape') {
|
|
538
|
+
event.preventDefault();
|
|
539
|
+
closeCommandPalette();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
commandList.addEventListener('click', (event) => {
|
|
544
|
+
const item = event.target.closest('[data-section]');
|
|
545
|
+
if (!item) return;
|
|
546
|
+
const targetId = item.dataset.section;
|
|
547
|
+
if (!targetId) return;
|
|
548
|
+
setHighlightQuery(commandInput.value, true);
|
|
549
|
+
navigate(targetId, { scrollToHighlight: true });
|
|
550
|
+
closeCommandPalette();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
commandPalette.addEventListener('click', (event) => {
|
|
554
|
+
if (event.target === commandPalette) {
|
|
555
|
+
closeCommandPalette();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
window.addEventListener('keydown', (event) => {
|
|
560
|
+
const target = event.target;
|
|
561
|
+
const isTypingContext = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
|
|
562
|
+
const isModifier = event.metaKey || event.ctrlKey;
|
|
563
|
+
if ((event.key.toLowerCase() === 'k' && isModifier) || (event.key === '/' && !isTypingContext)) {
|
|
564
|
+
event.preventDefault();
|
|
565
|
+
if (paletteOpen) closeCommandPalette();
|
|
566
|
+
else openCommandPalette();
|
|
567
|
+
} else if (event.key === 'Escape' && paletteOpen) {
|
|
568
|
+
event.preventDefault();
|
|
569
|
+
closeCommandPalette();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function openCommandPalette() {
|
|
575
|
+
if (!commandPalette || !commandInput) return;
|
|
576
|
+
paletteOpen = true;
|
|
577
|
+
commandPalette.hidden = false;
|
|
578
|
+
const initial = highlightQuery;
|
|
579
|
+
commandInput.value = initial;
|
|
580
|
+
updateCommandEntries(initial);
|
|
581
|
+
requestAnimationFrame(() => {
|
|
582
|
+
commandInput.focus();
|
|
583
|
+
if (initial) {
|
|
584
|
+
commandInput.select();
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function closeCommandPalette() {
|
|
590
|
+
if (!commandPalette || !commandInput) return;
|
|
591
|
+
paletteOpen = false;
|
|
592
|
+
commandPalette.hidden = true;
|
|
593
|
+
commandInput.blur();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let searchDebounce = null;
|
|
597
|
+
let isSearching = false;
|
|
598
|
+
|
|
599
|
+
async function updateCommandEntries(query) {
|
|
600
|
+
if (!commandList) return;
|
|
601
|
+
|
|
602
|
+
// Show loading state on first search
|
|
603
|
+
if (!isSearching && query.trim()) {
|
|
604
|
+
isSearching = true;
|
|
605
|
+
commandList.innerHTML = '<li class="cmd-item cmd-loading">Indexing content...</li>';
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Debounce to avoid excessive searches while typing
|
|
609
|
+
clearTimeout(searchDebounce);
|
|
610
|
+
searchDebounce = setTimeout(async () => {
|
|
611
|
+
commandEntries = await searchContent(MANIFEST, query);
|
|
612
|
+
const currentId = currentSectionId();
|
|
613
|
+
commandIndex = findPreferredIndex(commandEntries, currentId);
|
|
614
|
+
renderCommandList();
|
|
615
|
+
reflectCommandSelection();
|
|
616
|
+
isSearching = false;
|
|
617
|
+
}, query.trim() ? 150 : 0);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function renderCommandList() {
|
|
621
|
+
if (!commandList) return;
|
|
622
|
+
commandList.innerHTML = '';
|
|
623
|
+
if (!commandEntries.length) {
|
|
624
|
+
const empty = document.createElement('li');
|
|
625
|
+
empty.className = 'cmd-item';
|
|
626
|
+
empty.setAttribute('aria-selected', 'false');
|
|
627
|
+
empty.textContent = 'No matches.';
|
|
628
|
+
commandList.appendChild(empty);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
commandEntries.forEach((section) => {
|
|
632
|
+
const item = document.createElement('li');
|
|
633
|
+
item.className = 'cmd-item';
|
|
634
|
+
item.dataset.section = section.id;
|
|
635
|
+
item.setAttribute('role', 'option');
|
|
636
|
+
const title = document.createElement('span');
|
|
637
|
+
title.className = 'cmd-item-title';
|
|
638
|
+
title.textContent = section.title;
|
|
639
|
+
if (section.group) {
|
|
640
|
+
const group = document.createElement('span');
|
|
641
|
+
group.className = 'cmd-item-group';
|
|
642
|
+
group.textContent = section.group;
|
|
643
|
+
title.prepend(group);
|
|
644
|
+
}
|
|
645
|
+
const summary = document.createElement('span');
|
|
646
|
+
summary.className = 'cmd-item-summary';
|
|
647
|
+
summary.textContent = section.summary || '';
|
|
648
|
+
item.append(title, summary);
|
|
649
|
+
commandList.appendChild(item);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function reflectCommandSelection() {
|
|
654
|
+
if (!commandList) return;
|
|
655
|
+
Array.from(commandList.children).forEach((li, index) => {
|
|
656
|
+
const isActive = index === commandIndex && commandEntries.length;
|
|
657
|
+
li.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
658
|
+
if (isActive) {
|
|
659
|
+
li.scrollIntoView({ block: 'nearest' });
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Export handler
|
|
665
|
+
if (exportBtn) {
|
|
666
|
+
exportBtn.addEventListener('click', showExportOptions);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function showExportOptions() {
|
|
670
|
+
const overlay = document.createElement('div');
|
|
671
|
+
overlay.className = 'export-options-overlay';
|
|
672
|
+
overlay.innerHTML = `
|
|
673
|
+
<div class="export-options-modal">
|
|
674
|
+
<div class="export-options-header">EXPORT OPTIONS</div>
|
|
675
|
+
<div class="export-options-buttons">
|
|
676
|
+
<button type="button" class="export-option-btn" data-scope="page">
|
|
677
|
+
<span class="export-option-title">Current Page</span>
|
|
678
|
+
<span class="export-option-desc">Export only this section</span>
|
|
679
|
+
</button>
|
|
680
|
+
<button type="button" class="export-option-btn" data-scope="site">
|
|
681
|
+
<span class="export-option-title">Entire Site</span>
|
|
682
|
+
<span class="export-option-desc">Export all documentation</span>
|
|
683
|
+
</button>
|
|
684
|
+
</div>
|
|
685
|
+
<button type="button" class="export-cancel-btn">Cancel</button>
|
|
686
|
+
</div>
|
|
687
|
+
`;
|
|
688
|
+
document.body.appendChild(overlay);
|
|
689
|
+
setTimeout(() => overlay.classList.add('active'), 10);
|
|
690
|
+
|
|
691
|
+
const close = () => {
|
|
692
|
+
overlay.classList.remove('active');
|
|
693
|
+
setTimeout(() => overlay.remove(), 200);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
overlay.querySelector('.export-cancel-btn').addEventListener('click', close);
|
|
697
|
+
overlay.addEventListener('click', (e) => {
|
|
698
|
+
if (e.target === overlay) close();
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
overlay.querySelectorAll('.export-option-btn').forEach(btn => {
|
|
702
|
+
btn.addEventListener('click', () => {
|
|
703
|
+
const scope = btn.dataset.scope;
|
|
704
|
+
close();
|
|
705
|
+
handleExport(scope);
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function handleExport(scope = 'site') {
|
|
711
|
+
if (!exportBtn) return;
|
|
712
|
+
const originalMarkup = exportBtn.innerHTML;
|
|
713
|
+
|
|
714
|
+
// Test for popup blocking first
|
|
715
|
+
const testWindow = window.open('', '_blank', 'width=1,height=1,left=0,top=0');
|
|
716
|
+
if (!testWindow || testWindow.closed || typeof testWindow.closed === 'undefined') {
|
|
717
|
+
if (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?')) {
|
|
718
|
+
return; // User will try again after enabling popups
|
|
719
|
+
} else {
|
|
720
|
+
return; // User declined, cannot proceed
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
testWindow.close(); // Close test window
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
exportBtn.disabled = true;
|
|
727
|
+
|
|
728
|
+
// Create loading overlay
|
|
729
|
+
const loadingOverlay = document.createElement('div');
|
|
730
|
+
loadingOverlay.className = 'export-loading-overlay';
|
|
731
|
+
loadingOverlay.innerHTML = `
|
|
732
|
+
<div class="export-loading-modal">
|
|
733
|
+
<div class="export-loading-header">
|
|
734
|
+
<div class="export-loading-title">COMPILING DOCUMENTATION</div>
|
|
735
|
+
<div class="export-loading-subtitle">Assembling all sections into unified document</div>
|
|
736
|
+
</div>
|
|
737
|
+
<div class="export-loading-progress">
|
|
738
|
+
<div class="export-loading-bar">
|
|
739
|
+
<div class="export-loading-fill"></div>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="export-loading-status-container">
|
|
742
|
+
<div class="export-loading-status">Initializing...</div>
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
<div class="export-loading-scanner">
|
|
746
|
+
<div class="scanner-line"></div>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
`;
|
|
750
|
+
document.body.appendChild(loadingOverlay);
|
|
751
|
+
|
|
752
|
+
// Force layout and trigger animation
|
|
753
|
+
setTimeout(() => loadingOverlay.classList.add('active'), 10);
|
|
754
|
+
|
|
755
|
+
const progressFill = loadingOverlay.querySelector('.export-loading-fill');
|
|
756
|
+
const statusText = loadingOverlay.querySelector('.export-loading-status');
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
// Collect sections based on scope
|
|
760
|
+
let allSections;
|
|
761
|
+
if (scope === 'page') {
|
|
762
|
+
// Export only current page
|
|
763
|
+
const currentId = currentSectionId();
|
|
764
|
+
const allAvailable = collectExportableSections(MANIFEST);
|
|
765
|
+
const current = allAvailable.find(s => s.id === currentId);
|
|
766
|
+
allSections = current ? [current] : [];
|
|
767
|
+
} else {
|
|
768
|
+
// Export entire site
|
|
769
|
+
allSections = collectExportableSections(MANIFEST);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (allSections.length === 0) {
|
|
773
|
+
alert('No content available to export.');
|
|
774
|
+
loadingOverlay.remove();
|
|
775
|
+
exportBtn.disabled = false;
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const bundle = [];
|
|
780
|
+
const totalSections = allSections.length;
|
|
781
|
+
let processedSections = 0;
|
|
782
|
+
|
|
783
|
+
for (const section of allSections) {
|
|
784
|
+
// Update progress
|
|
785
|
+
processedSections++;
|
|
786
|
+
const progress = (processedSections / totalSections) * 100;
|
|
787
|
+
progressFill.style.width = `${progress}%`;
|
|
788
|
+
statusText.textContent = scope === 'page'
|
|
789
|
+
? `Exporting: ${section.title}`
|
|
790
|
+
: `Processing section ${processedSections} of ${totalSections}: ${section.title}`;
|
|
791
|
+
|
|
792
|
+
// Small delay to show progress animation
|
|
793
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
const mod = await import(section.module);
|
|
797
|
+
const loader = mod.load || mod.default;
|
|
798
|
+
if (typeof loader !== 'function') continue;
|
|
799
|
+
const payload = await loader();
|
|
800
|
+
const parsed = sanitizeExportMarkup(payload.html || '');
|
|
801
|
+
bundle.push({ section, html: parsed });
|
|
802
|
+
} catch (err) {
|
|
803
|
+
console.error('Failed to include section in export', section.id, err);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
statusText.textContent = 'Generating document...';
|
|
808
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
809
|
+
|
|
810
|
+
const htmlDoc = composeExportDocument(bundle, EXPORT_CONFIG);
|
|
811
|
+
|
|
812
|
+
statusText.textContent = 'Opening document viewer...';
|
|
813
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
814
|
+
|
|
815
|
+
// Open window only after content is ready
|
|
816
|
+
const printWindow = window.open('', '_blank', 'width=900,height=860,scrollbars=yes,resizable=yes');
|
|
817
|
+
if (!printWindow) {
|
|
818
|
+
alert('Please allow pop-ups to export the document.');
|
|
819
|
+
loadingOverlay.remove();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
printWindow.document.open();
|
|
824
|
+
printWindow.document.write(htmlDoc);
|
|
825
|
+
printWindow.document.close();
|
|
826
|
+
printWindow.focus();
|
|
827
|
+
|
|
828
|
+
// Fade out loading overlay
|
|
829
|
+
loadingOverlay.classList.remove('active');
|
|
830
|
+
setTimeout(() => loadingOverlay.remove(), 300);
|
|
831
|
+
|
|
832
|
+
} catch (err) {
|
|
833
|
+
console.error('Export failed', err);
|
|
834
|
+
alert('Export failed. Check console for details.');
|
|
835
|
+
loadingOverlay.remove();
|
|
836
|
+
} finally {
|
|
837
|
+
exportBtn.disabled = false;
|
|
838
|
+
exportBtn.innerHTML = originalMarkup;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function sanitizeExportMarkup(markup) {
|
|
843
|
+
const wrapper = document.createElement('div');
|
|
844
|
+
wrapper.innerHTML = markup;
|
|
845
|
+
wrapper.querySelectorAll('script').forEach((script) => script.remove());
|
|
846
|
+
wrapper.querySelectorAll('button').forEach((button) => button.removeAttribute('onclick'));
|
|
847
|
+
wrapper.querySelectorAll('mark.hl').forEach((mark) => {
|
|
848
|
+
const text = document.createTextNode(mark.textContent || '');
|
|
849
|
+
mark.replaceWith(text);
|
|
850
|
+
});
|
|
851
|
+
const firstSection = wrapper.querySelector('section');
|
|
852
|
+
return firstSection ? firstSection.innerHTML : wrapper.innerHTML;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function setHighlightQuery(value, persist = false) {
|
|
856
|
+
highlightQuery = value.trim();
|
|
857
|
+
if (persist) {
|
|
858
|
+
if (highlightQuery) {
|
|
859
|
+
localStorage.setItem(COMMAND_QUERY_KEY, highlightQuery);
|
|
860
|
+
} else {
|
|
861
|
+
localStorage.removeItem(COMMAND_QUERY_KEY);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
applyHighlight();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function applyHighlight(scrollToFirst = false) {
|
|
868
|
+
if (!app) return;
|
|
869
|
+
highlightContent(app, highlightQuery, { scrollToFirst });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function clearHighlights(root) {
|
|
873
|
+
if (!root) return;
|
|
874
|
+
root.querySelectorAll('mark.hl').forEach((mark) => {
|
|
875
|
+
const text = document.createTextNode(mark.textContent || '');
|
|
876
|
+
mark.replaceWith(text);
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function highlightContent(root, query, { scrollToFirst = false } = {}) {
|
|
881
|
+
if (!root) return;
|
|
882
|
+
clearHighlights(root);
|
|
883
|
+
if (!query) return;
|
|
884
|
+
const terms = query.split(/\s+/).map((term) => term.trim()).filter(Boolean);
|
|
885
|
+
if (!terms.length) return;
|
|
886
|
+
|
|
887
|
+
const lowerTerms = terms.map((term) => term.toLowerCase());
|
|
888
|
+
const skipTags = new Set(['SCRIPT', 'STYLE', 'CODE', 'PRE']);
|
|
889
|
+
const walker = document.createTreeWalker(
|
|
890
|
+
root,
|
|
891
|
+
NodeFilter.SHOW_TEXT,
|
|
892
|
+
{
|
|
893
|
+
acceptNode(node) {
|
|
894
|
+
if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
|
|
895
|
+
const parent = node.parentNode;
|
|
896
|
+
if (parent && skipTags.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
|
897
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
const matches = [];
|
|
903
|
+
let current;
|
|
904
|
+
while ((current = walker.nextNode())) {
|
|
905
|
+
const value = current.nodeValue.toLowerCase();
|
|
906
|
+
if (lowerTerms.some((term) => value.includes(term))) {
|
|
907
|
+
matches.push(current);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const termPattern = new RegExp(`(${terms.map(escapeRegExp).join('|')})`, 'gi');
|
|
912
|
+
matches.forEach((textNode) => {
|
|
913
|
+
const text = textNode.nodeValue;
|
|
914
|
+
const fragments = [];
|
|
915
|
+
let lastIndex = 0;
|
|
916
|
+
text.replace(termPattern, (match, _group, offset) => {
|
|
917
|
+
if (offset > lastIndex) {
|
|
918
|
+
fragments.push(document.createTextNode(text.slice(lastIndex, offset)));
|
|
919
|
+
}
|
|
920
|
+
const mark = document.createElement('mark');
|
|
921
|
+
mark.className = 'hl';
|
|
922
|
+
mark.textContent = match;
|
|
923
|
+
fragments.push(mark);
|
|
924
|
+
lastIndex = offset + match.length;
|
|
925
|
+
return match;
|
|
926
|
+
});
|
|
927
|
+
if (lastIndex < text.length) {
|
|
928
|
+
fragments.push(document.createTextNode(text.slice(lastIndex)));
|
|
929
|
+
}
|
|
930
|
+
const replacement = document.createDocumentFragment();
|
|
931
|
+
fragments.forEach((fragment) => replacement.appendChild(fragment));
|
|
932
|
+
textNode.parentNode.replaceChild(replacement, textNode);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
if (scrollToFirst) {
|
|
936
|
+
requestAnimationFrame(() => {
|
|
937
|
+
const first = root.querySelector('mark.hl');
|
|
938
|
+
if (first) {
|
|
939
|
+
first.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Mobile menu toggle
|
|
946
|
+
if (mobileMenuToggle && sidebar) {
|
|
947
|
+
mobileMenuToggle.addEventListener('click', () => {
|
|
948
|
+
const isOpen = sidebar.classList.contains('mobile-open');
|
|
949
|
+
if (isOpen) {
|
|
950
|
+
sidebar.classList.remove('mobile-open');
|
|
951
|
+
document.body.classList.remove('menu-open');
|
|
952
|
+
mobileMenuToggle.setAttribute('aria-expanded', 'false');
|
|
953
|
+
} else {
|
|
954
|
+
sidebar.classList.add('mobile-open');
|
|
955
|
+
document.body.classList.add('menu-open');
|
|
956
|
+
mobileMenuToggle.setAttribute('aria-expanded', 'true');
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Close menu when clicking on a nav item (but not parent sections)
|
|
961
|
+
nav.addEventListener('click', (e) => {
|
|
962
|
+
if (window.innerWidth <= 960) {
|
|
963
|
+
const clickedElement = e.target.closest('.nav-item, .nav-leaf, .nav-parent');
|
|
964
|
+
if (clickedElement) {
|
|
965
|
+
// Only close if it's a leaf node or nav-item (actual navigation)
|
|
966
|
+
// Don't close for nav-parent (section headers that expand/collapse)
|
|
967
|
+
if (clickedElement.classList.contains('nav-item') ||
|
|
968
|
+
clickedElement.classList.contains('nav-leaf')) {
|
|
969
|
+
sidebar.classList.remove('mobile-open');
|
|
970
|
+
document.body.classList.remove('menu-open');
|
|
971
|
+
mobileMenuToggle.setAttribute('aria-expanded', 'false');
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
// Close menu when clicking outside
|
|
978
|
+
document.addEventListener('click', (e) => {
|
|
979
|
+
if (window.innerWidth <= 960 &&
|
|
980
|
+
sidebar.classList.contains('mobile-open') &&
|
|
981
|
+
!sidebar.contains(e.target) &&
|
|
982
|
+
!mobileMenuToggle.contains(e.target)) {
|
|
983
|
+
sidebar.classList.remove('mobile-open');
|
|
984
|
+
document.body.classList.remove('menu-open');
|
|
985
|
+
mobileMenuToggle.setAttribute('aria-expanded', 'false');
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}
|