docyard 0.6.0 → 0.8.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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +34 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +82 -50
  8. data/lib/docyard/builder.rb +20 -10
  9. data/lib/docyard/cli.rb +6 -3
  10. data/lib/docyard/components/aliases.rb +29 -0
  11. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  12. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  13. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  14. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  15. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  16. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  17. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  18. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  19. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  20. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  21. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  22. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  23. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  24. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  25. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  26. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  27. data/lib/docyard/components/support/code_detector.rb +61 -0
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  29. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  30. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  31. data/lib/docyard/config/branding_resolver.rb +183 -0
  32. data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
  33. data/lib/docyard/config/validator.rb +122 -99
  34. data/lib/docyard/config.rb +38 -36
  35. data/lib/docyard/initializer.rb +15 -76
  36. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  37. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
  38. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  39. data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
  40. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  41. data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
  42. data/lib/docyard/navigation/sidebar/item.rb +96 -0
  43. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  44. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  45. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  46. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  47. data/lib/docyard/navigation/sidebar/renderer.rb +144 -0
  48. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  49. data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
  50. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  51. data/lib/docyard/navigation/sidebar_builder.rb +159 -0
  52. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  53. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
  54. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
  55. data/lib/docyard/rendering/renderer.rb +163 -0
  56. data/lib/docyard/rendering/template_resolver.rb +172 -0
  57. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  58. data/lib/docyard/search/build_indexer.rb +74 -0
  59. data/lib/docyard/search/dev_indexer.rb +155 -0
  60. data/lib/docyard/search/pagefind_support.rb +33 -0
  61. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
  62. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  63. data/lib/docyard/server/pagefind_handler.rb +63 -0
  64. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
  65. data/lib/docyard/server/rack_application.rb +192 -0
  66. data/lib/docyard/server/resolution_result.rb +29 -0
  67. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  68. data/lib/docyard/templates/assets/css/code.css +18 -51
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  72. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  73. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  74. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  75. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  76. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  77. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  78. data/lib/docyard/templates/assets/css/components/search.css +561 -0
  79. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  80. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  81. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  82. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  83. data/lib/docyard/templates/assets/css/landing.css +815 -0
  84. data/lib/docyard/templates/assets/css/layout.css +503 -87
  85. data/lib/docyard/templates/assets/css/main.css +1 -3
  86. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  87. data/lib/docyard/templates/assets/css/reset.css +0 -3
  88. data/lib/docyard/templates/assets/css/typography.css +43 -41
  89. data/lib/docyard/templates/assets/css/variables.css +268 -208
  90. data/lib/docyard/templates/assets/favicon.svg +7 -8
  91. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  92. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  93. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  94. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  95. data/lib/docyard/templates/assets/js/components/search.js +610 -0
  96. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  97. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  98. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  99. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  100. data/lib/docyard/templates/assets/js/theme.js +0 -3
  101. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  102. data/lib/docyard/templates/assets/logo.svg +7 -4
  103. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  104. data/lib/docyard/templates/errors/404.html.erb +1 -1
  105. data/lib/docyard/templates/errors/500.html.erb +1 -1
  106. data/lib/docyard/templates/layouts/default.html.erb +19 -56
  107. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  108. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  109. data/lib/docyard/templates/partials/_code_block.html.erb +6 -4
  110. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  111. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  112. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  113. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  114. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  115. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  116. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  117. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  118. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  119. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  120. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  121. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_prev_next.html.erb +9 -3
  123. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  124. data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
  125. data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
  126. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  127. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  129. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  130. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  131. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  132. data/lib/docyard/utils/html_helpers.rb +14 -0
  133. data/lib/docyard/utils/path_resolver.rb +2 -1
  134. data/lib/docyard/utils/url_helpers.rb +20 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +22 -15
  137. metadata +89 -50
  138. data/lib/docyard/components/callout_processor.rb +0 -121
  139. data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
  140. data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
  141. data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
  142. data/lib/docyard/components/code_block_icon_detector.rb +0 -40
  143. data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
  144. data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
  145. data/lib/docyard/components/code_block_patterns.rb +0 -51
  146. data/lib/docyard/components/code_block_processor.rb +0 -176
  147. data/lib/docyard/components/code_detector.rb +0 -59
  148. data/lib/docyard/components/code_line_parser.rb +0 -80
  149. data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
  150. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  151. data/lib/docyard/components/icon_detector.rb +0 -57
  152. data/lib/docyard/components/icon_processor.rb +0 -51
  153. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  154. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  155. data/lib/docyard/components/tabs_parser.rb +0 -191
  156. data/lib/docyard/components/tabs_processor.rb +0 -44
  157. data/lib/docyard/components/tabs_range_finder.rb +0 -42
  158. data/lib/docyard/rack_application.rb +0 -172
  159. data/lib/docyard/renderer.rb +0 -120
  160. data/lib/docyard/routing/resolution_result.rb +0 -31
  161. data/lib/docyard/sidebar/config_parser.rb +0 -180
  162. data/lib/docyard/sidebar/item.rb +0 -58
  163. data/lib/docyard/sidebar/renderer.rb +0 -137
  164. data/lib/docyard/sidebar/tree_builder.rb +0 -59
  165. data/lib/docyard/sidebar_builder.rb +0 -102
  166. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  167. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  168. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  169. data/lib/docyard/templates/markdown/index.md.erb +0 -82
  170. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  171. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  172. /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
  173. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  174. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  175. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  176. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  177. /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
@@ -0,0 +1,610 @@
1
+ class SearchManager {
2
+ constructor() {
3
+ this.modal = document.querySelector('[data-search-modal]');
4
+ this.trigger = document.querySelector('[data-search-trigger]');
5
+ this.backdrop = document.querySelector('[data-search-backdrop]');
6
+ this.input = document.querySelector('[data-search-input]');
7
+ this.clearButton = document.querySelector('[data-search-clear]');
8
+ this.closeButton = document.querySelector('[data-search-close]');
9
+ this.body = document.querySelector('[data-search-body]');
10
+ this.resultsContainer = document.querySelector('[data-search-results]');
11
+ this.loadingState = document.querySelector('[data-search-loading]');
12
+ this.emptyState = document.querySelector('[data-search-empty]');
13
+
14
+ if (!this.modal) return;
15
+
16
+ this.pagefind = null;
17
+ this.isOpen = false;
18
+ this.selectedIndex = -1;
19
+ this.results = [];
20
+ this.searchTimeout = null;
21
+ this.DEBOUNCE_DELAY = 150;
22
+ this.RESULTS_PER_PAGE = 6;
23
+
24
+ this.allSearchResults = [];
25
+ this.displayedCount = 0;
26
+ this.currentQuery = '';
27
+ this.groupedResults = [];
28
+
29
+ this.handleKeyDown = this.handleKeyDown.bind(this);
30
+ this.handleInput = this.handleInput.bind(this);
31
+ this.handleResultClick = this.handleResultClick.bind(this);
32
+ this.handleLoadMore = this.handleLoadMore.bind(this);
33
+
34
+ this.init();
35
+ }
36
+
37
+ init() {
38
+ this.attachEventListeners();
39
+ this.updateShortcutHint();
40
+ }
41
+
42
+ attachEventListeners() {
43
+ document.addEventListener('keydown', this.handleKeyDown);
44
+
45
+ if (this.trigger) {
46
+ this.trigger.addEventListener('click', () => this.open());
47
+ }
48
+
49
+ if (this.backdrop) {
50
+ this.backdrop.addEventListener('click', () => this.close());
51
+ }
52
+
53
+ if (this.closeButton) {
54
+ this.closeButton.addEventListener('click', () => this.close());
55
+ }
56
+
57
+ if (this.clearButton) {
58
+ this.clearButton.addEventListener('click', () => this.clearSearch());
59
+ }
60
+
61
+ if (this.input) {
62
+ this.input.addEventListener('input', this.handleInput);
63
+ this.input.addEventListener('keydown', (e) => this.handleInputKeyDown(e));
64
+ }
65
+
66
+ if (this.resultsContainer) {
67
+ this.resultsContainer.addEventListener('click', this.handleResultClick);
68
+ }
69
+ }
70
+
71
+ updateShortcutHint() {
72
+ const shortcut = document.querySelector('[data-search-shortcut]');
73
+ if (shortcut) {
74
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0 ||
75
+ navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
76
+ if (!isMac) {
77
+ shortcut.setAttribute('data-os', 'windows');
78
+ }
79
+ }
80
+ }
81
+
82
+ handleKeyDown(event) {
83
+ if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
84
+ event.preventDefault();
85
+ this.toggle();
86
+ return;
87
+ }
88
+
89
+ if (event.key === 'Escape' && this.isOpen) {
90
+ event.preventDefault();
91
+ this.close();
92
+ return;
93
+ }
94
+
95
+ if (event.key === '/' && !this.isOpen && !this.isInputFocused()) {
96
+ event.preventDefault();
97
+ this.open();
98
+ }
99
+ }
100
+
101
+ handleInputKeyDown(event) {
102
+ switch (event.key) {
103
+ case 'ArrowDown':
104
+ event.preventDefault();
105
+ this.selectNext();
106
+ break;
107
+ case 'ArrowUp':
108
+ event.preventDefault();
109
+ this.selectPrevious();
110
+ break;
111
+ case 'Enter':
112
+ event.preventDefault();
113
+ this.navigateToSelected();
114
+ break;
115
+ }
116
+ }
117
+
118
+ handleInput(event) {
119
+ const query = event.target.value.trim();
120
+
121
+ if (this.clearButton) {
122
+ this.clearButton.hidden = query.length === 0;
123
+ }
124
+
125
+ if (this.searchTimeout) {
126
+ clearTimeout(this.searchTimeout);
127
+ }
128
+
129
+ if (query.length === 0) {
130
+ this.hideBody();
131
+ return;
132
+ }
133
+
134
+ this.searchTimeout = setTimeout(() => {
135
+ this.search(query);
136
+ }, this.DEBOUNCE_DELAY);
137
+ }
138
+
139
+ handleResultClick(event) {
140
+ const resultElement = event.target.closest('.search-result');
141
+ if (resultElement) {
142
+ const url = resultElement.getAttribute('href');
143
+ if (url) {
144
+ this.close();
145
+ window.location.href = url;
146
+ }
147
+ }
148
+ }
149
+
150
+ isInputFocused() {
151
+ const activeElement = document.activeElement;
152
+ return activeElement && (
153
+ activeElement.tagName === 'INPUT' ||
154
+ activeElement.tagName === 'TEXTAREA' ||
155
+ activeElement.isContentEditable
156
+ );
157
+ }
158
+
159
+ toggle() {
160
+ if (this.isOpen) {
161
+ this.close();
162
+ } else {
163
+ this.open();
164
+ }
165
+ }
166
+
167
+ async open() {
168
+ if (this.isOpen) return;
169
+
170
+ this.isOpen = true;
171
+ this.modal.hidden = false;
172
+ document.body.style.overflow = 'hidden';
173
+
174
+ requestAnimationFrame(() => {
175
+ this.modal.classList.add('is-open');
176
+ requestAnimationFrame(() => {
177
+ if (this.input) {
178
+ this.input.focus();
179
+ }
180
+ });
181
+ });
182
+
183
+ if (!this.pagefind) {
184
+ await this.initPagefind();
185
+ }
186
+ }
187
+
188
+ close() {
189
+ if (!this.isOpen) return;
190
+
191
+ this.isOpen = false;
192
+ this.modal.classList.remove('is-open');
193
+ document.body.style.overflow = '';
194
+ this.selectedIndex = -1;
195
+
196
+ setTimeout(() => {
197
+ if (!this.isOpen) {
198
+ this.modal.hidden = true;
199
+ if (this.body) {
200
+ this.body.hidden = true;
201
+ }
202
+ }
203
+ }, 200);
204
+
205
+ if (this.trigger) {
206
+ this.trigger.focus();
207
+ }
208
+ }
209
+
210
+ clearSearch() {
211
+ if (this.input) {
212
+ this.input.value = '';
213
+ this.input.focus();
214
+ }
215
+ if (this.clearButton) {
216
+ this.clearButton.hidden = true;
217
+ }
218
+ this.hideBody();
219
+ }
220
+
221
+ async initPagefind() {
222
+ try {
223
+ this.pagefind = await import('/pagefind/pagefind.js');
224
+ await this.pagefind.init();
225
+ } catch (error) {
226
+ console.warn('Pagefind not available:', error);
227
+ this.showErrorState('Search is not available. Run "docyard build" to generate the search index.');
228
+ }
229
+ }
230
+
231
+ async search(query) {
232
+ if (!this.pagefind) {
233
+ await this.initPagefind();
234
+ if (!this.pagefind) return;
235
+ }
236
+
237
+ this.showLoadingState();
238
+
239
+ try {
240
+ const searchResults = await this.pagefind.search(query);
241
+
242
+ if (searchResults.results.length === 0) {
243
+ this.showEmptyState(query);
244
+ return;
245
+ }
246
+
247
+ this.allSearchResults = searchResults.results;
248
+ this.currentQuery = query;
249
+ this.displayedCount = 0;
250
+ this.groupedResults = [];
251
+
252
+ await this.loadMoreResults();
253
+ } catch (error) {
254
+ console.error('Search error:', error);
255
+ this.showErrorState('An error occurred while searching.');
256
+ }
257
+ }
258
+
259
+ async loadMoreResults() {
260
+ const startIndex = this.displayedCount;
261
+ const endIndex = Math.min(startIndex + this.RESULTS_PER_PAGE, this.allSearchResults.length);
262
+
263
+ if (startIndex >= this.allSearchResults.length) return;
264
+
265
+ const resultsData = await Promise.all(
266
+ this.allSearchResults.slice(startIndex, endIndex).map(r => r.data())
267
+ );
268
+
269
+ const newGrouped = this.groupResults(resultsData);
270
+ this.groupedResults = [...this.groupedResults, ...newGrouped];
271
+ this.displayedCount = endIndex;
272
+
273
+ this.results = this.flattenForNavigation(this.groupedResults);
274
+ this.renderGroupedResults(this.groupedResults, this.currentQuery, this.allSearchResults.length);
275
+ }
276
+
277
+ handleLoadMore(event) {
278
+ event.preventDefault();
279
+ this.loadMoreResults();
280
+ }
281
+
282
+ groupResults(resultsData) {
283
+ const grouped = [];
284
+
285
+ for (const result of resultsData) {
286
+ const pageTitle = result.meta?.title || this.extractTitleFromUrl(result.url);
287
+
288
+ const subResults = (result.sub_results || [])
289
+ .filter(sub => {
290
+ if (sub.url === result.url) return false;
291
+ if (!sub.title) return false;
292
+ const cleanedTitle = this.cleanSectionTitle(sub.title);
293
+ if (cleanedTitle.toLowerCase() === pageTitle.toLowerCase()) return false;
294
+ return true;
295
+ })
296
+ .slice(0, 3)
297
+ .map(sub => ({
298
+ url: sub.url,
299
+ title: this.cleanSectionTitle(sub.title),
300
+ excerpt: sub.excerpt || '',
301
+ type: 'section'
302
+ }));
303
+
304
+ grouped.push({
305
+ url: result.url,
306
+ title: pageTitle,
307
+ excerpt: result.excerpt || '',
308
+ type: 'page',
309
+ sections: subResults
310
+ });
311
+ }
312
+
313
+ return grouped;
314
+ }
315
+
316
+ cleanSectionTitle(title) {
317
+ return title.replace(/#$/, '').trim();
318
+ }
319
+
320
+ flattenForNavigation(grouped) {
321
+ const flat = [];
322
+ for (const page of grouped) {
323
+ flat.push({ url: page.url, title: page.title });
324
+ for (const section of page.sections) {
325
+ flat.push({ url: section.url, title: section.title });
326
+ }
327
+ }
328
+ return flat;
329
+ }
330
+
331
+ renderGroupedResults(grouped, query, totalResults) {
332
+ this.hideAllStates();
333
+ if (this.body) {
334
+ this.body.hidden = false;
335
+ }
336
+ this.resultsContainer.hidden = false;
337
+ this.selectedIndex = 0;
338
+
339
+ let navIndex = 0;
340
+ const resultsHtml = grouped.map(page => {
341
+ const pageIndex = navIndex++;
342
+ const isPageSelected = pageIndex === 0;
343
+
344
+ const sectionsHtml = page.sections.map(section => {
345
+ const sectionIndex = navIndex++;
346
+ const isSectionSelected = sectionIndex === 0;
347
+ return this.renderSectionResult(section, sectionIndex, isSectionSelected, query);
348
+ }).join('');
349
+
350
+ return this.renderPageResult(page, pageIndex, isPageSelected, sectionsHtml, query);
351
+ }).join('');
352
+
353
+ const hasMore = this.displayedCount < totalResults;
354
+ const loadMoreHtml = hasMore ? `
355
+ <li class="search-load-more">
356
+ <button type="button" class="search-load-more-btn" data-search-load-more>
357
+ View more results
358
+ </button>
359
+ </li>
360
+ ` : '';
361
+
362
+ this.resultsContainer.innerHTML = resultsHtml + loadMoreHtml;
363
+
364
+ const loadMoreBtn = this.resultsContainer.querySelector('[data-search-load-more]');
365
+ if (loadMoreBtn) {
366
+ loadMoreBtn.addEventListener('click', this.handleLoadMore);
367
+ }
368
+ }
369
+
370
+ renderPageResult(page, index, isSelected, sectionsHtml, query) {
371
+ const pageIcon = `<svg class="search-result-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor">
372
+ <path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,160H40V56H216V200ZM184,96a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,96Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,128Zm0,32a8,8,0,0,1-8,8H80a8,8,0,0,1,0-16h96A8,8,0,0,1,184,160Z"></path>
373
+ </svg>`;
374
+
375
+ const titleHtml = this.highlightTitle(page.title, query);
376
+ const excerptHtml = page.excerpt ? `<span class="search-result-excerpt">${this.highlightQuery(page.excerpt, query, page.title)}</span>` : '';
377
+
378
+ return `
379
+ <li class="search-result-group">
380
+ <a href="${page.url}" class="search-result search-result-page" role="option" aria-selected="${isSelected}" data-index="${index}">
381
+ ${pageIcon}
382
+ <div class="search-result-content">
383
+ <span class="search-result-title">${titleHtml}</span>
384
+ ${excerptHtml}
385
+ </div>
386
+ </a>
387
+ ${sectionsHtml ? `<ul class="search-result-sections">${sectionsHtml}</ul>` : ''}
388
+ </li>
389
+ `;
390
+ }
391
+
392
+ renderSectionResult(section, index, isSelected, query) {
393
+ const hashIcon = `<svg class="search-result-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor">
394
+ <path d="M224,88H175.4l8.47-46.57a8,8,0,0,0-15.74-2.86l-9,49.43H111.4l8.47-46.57a8,8,0,0,0-15.74-2.86L95.14,88H48a8,8,0,0,0,0,16H92.23L83.5,152H32a8,8,0,0,0,0,16H80.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,80,224a8,8,0,0,0,7.86-6.57l9-49.43H144.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,144,224a8,8,0,0,0,7.86-6.57l9-49.43H208a8,8,0,0,0,0-16H163.77l8.73-48H224a8,8,0,0,0,0-16Zm-76.5,64H99.77l8.73-48h47.73Z"></path>
395
+ </svg>`;
396
+
397
+ const titleHtml = this.highlightTitle(section.title, query);
398
+ const excerptHtml = section.excerpt ? `<span class="search-result-excerpt">${this.highlightQuery(section.excerpt, query, section.title)}</span>` : '';
399
+
400
+ return `
401
+ <li class="search-result-section-item">
402
+ <a href="${section.url}" class="search-result search-result-section" role="option" aria-selected="${isSelected}" data-index="${index}">
403
+ <span class="search-result-tree-line"></span>
404
+ ${hashIcon}
405
+ <div class="search-result-content">
406
+ <span class="search-result-title">${titleHtml}</span>
407
+ ${excerptHtml}
408
+ </div>
409
+ </a>
410
+ </li>
411
+ `;
412
+ }
413
+
414
+ extractTitleFromUrl(url) {
415
+ const path = url.replace(/\/$/, '').split('/').pop() || 'Home';
416
+ return path
417
+ .replace(/-/g, ' ')
418
+ .replace(/\b\w/g, c => c.toUpperCase());
419
+ }
420
+
421
+ escapeHtml(text) {
422
+ const div = document.createElement('div');
423
+ div.textContent = text;
424
+ return div.innerHTML;
425
+ }
426
+
427
+ highlightTitle(title, query) {
428
+ if (!query || !title) return this.escapeHtml(title);
429
+
430
+ const escaped = this.escapeHtml(title);
431
+ const terms = query.trim().split(/\s+/).filter(t => t.length > 1);
432
+ if (terms.length === 0) return escaped;
433
+
434
+ const regex = new RegExp(`(${terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
435
+ .join('|')})`, 'gi');
436
+ return escaped.replace(regex, '<mark class="search-title-highlight">$1</mark>');
437
+ }
438
+
439
+ highlightQuery(text, query, title = '') {
440
+ if (!query || !text) return this.escapeHtml(text);
441
+
442
+ let cleanText = this.decodeHtmlEntities(text);
443
+
444
+ cleanText = cleanText.replace(/<[^>]*>/g, '');
445
+
446
+ if (title) {
447
+ const titlePattern = new RegExp(`^${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[#:]?\\s*`, 'i');
448
+ cleanText = cleanText.replace(titlePattern, '');
449
+ }
450
+
451
+ cleanText = this.cleanMarkdown(cleanText);
452
+
453
+ const escaped = this.escapeHtml(cleanText);
454
+
455
+ const terms = query.trim().split(/\s+/).filter(t => t.length > 1);
456
+ if (terms.length === 0) return escaped;
457
+
458
+ const regex = new RegExp(`(${terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
459
+ .join('|')})`, 'gi');
460
+ return escaped.replace(regex, '<mark>$1</mark>');
461
+ }
462
+
463
+ decodeHtmlEntities(text) {
464
+ const textarea = document.createElement('textarea');
465
+ textarea.innerHTML = text;
466
+ return textarea.value;
467
+ }
468
+
469
+ cleanMarkdown(text) {
470
+ return text
471
+ .replace(/```\w*(?:\s+[^`\n]*)?\n?/g, '')
472
+ .replace(/```/g, '')
473
+ .replace(/\{:?:?[^}]*\}/g, '')
474
+ .replace(/(\w)#(?=\s|$|[A-Z])/g, '$1')
475
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
476
+ .replace(/\*([^*]+)\*/g, '$1')
477
+ .replace(/__([^_]+)__/g, '$1')
478
+ .replace(/_([^_]+)_/g, '$1')
479
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
480
+ .replace(/`([^`]+)`/g, '$1')
481
+ .replace(/`/g, '')
482
+ .replace(/^#+\s*/gm, '')
483
+ .replace(/\s*title=["'][^"']*["']/gi, '')
484
+ .replace(/https?:\/\/[^\s<>"{}|\\^`[\]]+/gi, '')
485
+ .replace(/ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi, '')
486
+ .replace(/\b\w+:\s*["']?[^"'\s,]+["']?(?=\s|,|$)/g, '')
487
+ .replace(/\b(const|let|var|function|interface|class|import|export|return|if|else)\b/g, '')
488
+ .replace(/[=;{}()<>[\]]/g, ' ')
489
+ .replace(/[✓✔✗✘→←↑↓•·►▸▹▶]/g, '')
490
+ .replace(/^---[\s\S]*?---/m, '')
491
+ .replace(/Skip to main content/gi, '')
492
+ .replace(/On this page/gi, '')
493
+ .replace(/Menu/gi, '')
494
+ .replace(/Search\.\.\./gi, '')
495
+ .replace(/^[\s]*[-*+]\s+/gm, '')
496
+ .replace(/^[\s]*\d+\.\s+/gm, '')
497
+ .replace(/\s+/g, ' ')
498
+ .replace(/^[.\s,;:]+/, '')
499
+ .replace(/[.\s,;:]+$/, '')
500
+ .trim();
501
+ }
502
+
503
+ selectNext() {
504
+ if (this.results.length === 0) return;
505
+
506
+ const newIndex = Math.min(this.selectedIndex + 1, this.results.length - 1);
507
+ this.updateSelection(newIndex);
508
+ }
509
+
510
+ selectPrevious() {
511
+ if (this.results.length === 0) return;
512
+
513
+ const newIndex = Math.max(this.selectedIndex - 1, 0);
514
+ this.updateSelection(newIndex);
515
+ }
516
+
517
+ updateSelection(newIndex) {
518
+ const resultElements = this.resultsContainer.querySelectorAll('.search-result');
519
+
520
+ if (this.selectedIndex >= 0 && resultElements[this.selectedIndex]) {
521
+ resultElements[this.selectedIndex].setAttribute('aria-selected', 'false');
522
+ }
523
+
524
+ this.selectedIndex = newIndex;
525
+ if (resultElements[newIndex]) {
526
+ resultElements[newIndex].setAttribute('aria-selected', 'true');
527
+ resultElements[newIndex].scrollIntoView({ block: 'nearest' });
528
+ }
529
+ }
530
+
531
+ navigateToSelected() {
532
+ if (this.selectedIndex < 0 || this.results.length === 0) return;
533
+
534
+ const result = this.results[this.selectedIndex];
535
+ if (result && result.url) {
536
+ this.close();
537
+ window.location.href = result.url;
538
+ }
539
+ }
540
+
541
+ showLoadingState() {
542
+ this.hideAllStates();
543
+ if (this.body) {
544
+ this.body.hidden = false;
545
+ }
546
+ if (this.loadingState) {
547
+ this.loadingState.hidden = false;
548
+ }
549
+ }
550
+
551
+ showEmptyState(query = '') {
552
+ this.hideAllStates();
553
+ if (this.body) {
554
+ this.body.hidden = false;
555
+ }
556
+ if (this.emptyState) {
557
+ const titleEl = this.emptyState.querySelector('.search-empty-title');
558
+ if (titleEl && query) {
559
+ titleEl.textContent = `No results for "${query}"`;
560
+ } else if (titleEl) {
561
+ titleEl.textContent = 'No results found';
562
+ }
563
+ this.emptyState.hidden = false;
564
+ }
565
+ }
566
+
567
+ showErrorState(message) {
568
+ this.hideAllStates();
569
+ if (this.body) {
570
+ this.body.hidden = false;
571
+ }
572
+ if (this.emptyState) {
573
+ this.emptyState.querySelector('span').textContent = message;
574
+ this.emptyState.hidden = false;
575
+ }
576
+ }
577
+
578
+ hideBody() {
579
+ this.hideAllStates();
580
+ if (this.body) {
581
+ this.body.hidden = true;
582
+ }
583
+ this.results = [];
584
+ this.selectedIndex = -1;
585
+ }
586
+
587
+ hideAllStates() {
588
+ if (this.loadingState) this.loadingState.hidden = true;
589
+ if (this.emptyState) this.emptyState.hidden = true;
590
+ if (this.resultsContainer) this.resultsContainer.hidden = true;
591
+ }
592
+
593
+ destroy() {
594
+ document.removeEventListener('keydown', this.handleKeyDown);
595
+ }
596
+ }
597
+
598
+ function initializeSearch() {
599
+ new SearchManager();
600
+ }
601
+
602
+ if (document.readyState === 'loading') {
603
+ document.addEventListener('DOMContentLoaded', initializeSearch);
604
+ } else {
605
+ initializeSearch();
606
+ }
607
+
608
+ if (typeof module !== 'undefined' && module.exports) {
609
+ module.exports = { SearchManager };
610
+ }
@@ -0,0 +1,29 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ var STORAGE_KEY = 'docyard_sidebar_collapsed';
5
+
6
+ function initSidebarToggle() {
7
+ var toggle = document.querySelector('.breadcrumb-toggle');
8
+ var sidebar = document.querySelector('.sidebar');
9
+
10
+ if (!toggle || !sidebar) {
11
+ return;
12
+ }
13
+
14
+ var isCollapsed = document.documentElement.classList.contains('sidebar-collapsed');
15
+ toggle.setAttribute('aria-expanded', !isCollapsed);
16
+
17
+ toggle.addEventListener('click', function() {
18
+ var collapsed = document.documentElement.classList.toggle('sidebar-collapsed');
19
+ localStorage.setItem(STORAGE_KEY, collapsed);
20
+ toggle.setAttribute('aria-expanded', !collapsed);
21
+ });
22
+ }
23
+
24
+ if (document.readyState === 'loading') {
25
+ document.addEventListener('DOMContentLoaded', initSidebarToggle);
26
+ } else {
27
+ initSidebarToggle();
28
+ }
29
+ })();