datalog-theme 0.6.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 (180) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +162 -0
  3. data/CITATION.cff +29 -0
  4. data/LICENSE +21 -0
  5. data/README.md +257 -0
  6. data/_data/academic.yml +217 -0
  7. data/_data/cdn-integrity.yml +51 -0
  8. data/_data/config/author.yml +121 -0
  9. data/_data/config/features.yml +262 -0
  10. data/_data/config/site.yml +42 -0
  11. data/_data/config/theme.yml +181 -0
  12. data/_data/datasets.yml +28 -0
  13. data/_data/i18n/en.yml +438 -0
  14. data/_data/i18n/es.yml +438 -0
  15. data/_data/i18n/pt.yml +438 -0
  16. data/_data/js_manifest.json +22 -0
  17. data/_data/js_meta.json +371 -0
  18. data/_data/navigation.yml +145 -0
  19. data/_data/projects.yml +41 -0
  20. data/_data/publications.yml +28 -0
  21. data/_data/social.yml +73 -0
  22. data/_data/visualizations.yml +51 -0
  23. data/_includes/analytics/dashboard.html +149 -0
  24. data/_includes/analytics.html +9 -0
  25. data/_includes/components/academic-dashboard.html +347 -0
  26. data/_includes/components/advanced-search.html +682 -0
  27. data/_includes/components/api-function.html +94 -0
  28. data/_includes/components/author-bio.html +166 -0
  29. data/_includes/components/bookmark-system.html +96 -0
  30. data/_includes/components/breadcrumbs.html +89 -0
  31. data/_includes/components/citation-tools.html +94 -0
  32. data/_includes/components/comments.html +244 -0
  33. data/_includes/components/content-provenance.html +43 -0
  34. data/_includes/components/content-recommendations.html +228 -0
  35. data/_includes/components/difficulty-badge.html +84 -0
  36. data/_includes/components/email-preferences.html +200 -0
  37. data/_includes/components/enhanced-code-block.html +212 -0
  38. data/_includes/components/enhanced-metadata.html +228 -0
  39. data/_includes/components/enhanced-toc.html +209 -0
  40. data/_includes/components/hero.html +15 -0
  41. data/_includes/components/language-switcher.html +396 -0
  42. data/_includes/components/math-fallback.html +19 -0
  43. data/_includes/components/navigation-enhancements.html +454 -0
  44. data/_includes/components/newsletter-signup.html +178 -0
  45. data/_includes/components/open-science-badges.html +16 -0
  46. data/_includes/components/package-install.html +194 -0
  47. data/_includes/components/performance-monitor.html +170 -0
  48. data/_includes/components/popular-posts.html +233 -0
  49. data/_includes/components/post-hero.html +62 -0
  50. data/_includes/components/reading-progress.html +133 -0
  51. data/_includes/components/reading-time.html +121 -0
  52. data/_includes/components/responsive-image.html +63 -0
  53. data/_includes/components/search-facets.html +307 -0
  54. data/_includes/components/series-navigation.html +124 -0
  55. data/_includes/components/social-proof.html +34 -0
  56. data/_includes/components/social-share.html +119 -0
  57. data/_includes/components/user-preferences.html +566 -0
  58. data/_includes/components/visualization-card.html +24 -0
  59. data/_includes/components/viz-table-fallback.html +19 -0
  60. data/_includes/critical-css/default.html +0 -0
  61. data/_includes/critical-css/home.html +0 -0
  62. data/_includes/critical-css/post.html +0 -0
  63. data/_includes/csp-meta.html +17 -0
  64. data/_includes/footer/nav-column.html +31 -0
  65. data/_includes/footer.html +76 -0
  66. data/_includes/head.html +337 -0
  67. data/_includes/header/navigation.html +48 -0
  68. data/_includes/header.html +70 -0
  69. data/_includes/helpers/array-to-sentence.html +21 -0
  70. data/_includes/helpers/date-format.html +16 -0
  71. data/_includes/helpers/link-with-icon.html +26 -0
  72. data/_includes/layouts/default/article.html +67 -0
  73. data/_includes/meta/language-attributes.html +25 -0
  74. data/_includes/meta/math-config.html +21 -0
  75. data/_includes/meta/schema.html +153 -0
  76. data/_includes/meta/scripts-loader.html +52 -0
  77. data/_includes/post/related-posts.html +63 -0
  78. data/_includes/scripts.html +72 -0
  79. data/_includes/skip-link.html +1 -0
  80. data/_includes/toc.html +2 -0
  81. data/_layouts/archive.html +282 -0
  82. data/_layouts/dataset.html +52 -0
  83. data/_layouts/default.html +33 -0
  84. data/_layouts/home.html +48 -0
  85. data/_layouts/notebook.html +173 -0
  86. data/_layouts/package.html +238 -0
  87. data/_layouts/page.html +16 -0
  88. data/_layouts/portfolio.html +41 -0
  89. data/_layouts/post-sidebar.html +183 -0
  90. data/_layouts/post.html +338 -0
  91. data/_layouts/project.html +252 -0
  92. data/_layouts/research.html +504 -0
  93. data/_plugins/analytics_dashboard.rb +315 -0
  94. data/_plugins/config_validator.rb +397 -0
  95. data/_plugins/csp_generator.rb +115 -0
  96. data/_plugins/datalog_bibliography.rb +19 -0
  97. data/_plugins/datalog_comments.rb +18 -0
  98. data/_plugins/datalog_slides.rb +20 -0
  99. data/_plugins/front_matter_compat.rb +109 -0
  100. data/_plugins/i18n.rb +160 -0
  101. data/_plugins/image_optimizer.rb +445 -0
  102. data/_plugins/math_preprocessor.rb +185 -0
  103. data/_plugins/notebook_converter.rb +1107 -0
  104. data/_plugins/plugin_loader.rb +185 -0
  105. data/_plugins/publications_generator.rb +234 -0
  106. data/_plugins/reading_time.rb +15 -0
  107. data/_plugins/search_normalizer.rb +74 -0
  108. data/_plugins/sri_filter.rb +18 -0
  109. data/_plugins/toc_filter.rb +68 -0
  110. data/_plugins/warning_filter.rb +3 -0
  111. data/_sass/_base.scss +142 -0
  112. data/_sass/_components.scss +2380 -0
  113. data/_sass/_header.scss +470 -0
  114. data/_sass/_interactive.scss +344 -0
  115. data/_sass/_layout.scss +1760 -0
  116. data/_sass/_mathematical.scss +368 -0
  117. data/_sass/_mixins.scss +99 -0
  118. data/_sass/_package-docs.scss +715 -0
  119. data/_sass/_phase1-enhancements.scss +721 -0
  120. data/_sass/_phase3-enhancements.scss +874 -0
  121. data/_sass/_phase4-enhancements.scss +1214 -0
  122. data/_sass/_phase5-enhancements.scss +414 -0
  123. data/_sass/_search.scss +654 -0
  124. data/_sass/_syntax-highlighting.scss +128 -0
  125. data/_sass/_theme.scss +79 -0
  126. data/_sass/_typography.scss +148 -0
  127. data/_sass/_utilities.scss +334 -0
  128. data/_sass/_variables.scss +153 -0
  129. data/_sass/_visualizations.scss +242 -0
  130. data/assets/css/main.scss +4 -0
  131. data/assets/img/20220607123041_detail.001.png +0 -0
  132. data/assets/img/favicons/android-chrome-192x192.png +0 -0
  133. data/assets/img/favicons/android-chrome-512x512.png +0 -0
  134. data/assets/img/favicons/apple-touch-icon.png +0 -0
  135. data/assets/img/favicons/favicon-16x16.png +0 -0
  136. data/assets/img/favicons/favicon-32x32.png +0 -0
  137. data/assets/img/favicons/favicon.ico +0 -0
  138. data/assets/img/favicons/site.webmanifest +21 -0
  139. data/assets/img/portfolio-placeholder.svg +20 -0
  140. data/assets/js/academic.js +262 -0
  141. data/assets/js/analytics-dashboard.js +382 -0
  142. data/assets/js/core/dark-mode.js +79 -0
  143. data/assets/js/core/github-cards.js +123 -0
  144. data/assets/js/core/language-filter.js +69 -0
  145. data/assets/js/core/navigation.js +184 -0
  146. data/assets/js/core/scroll-progress.js +45 -0
  147. data/assets/js/core/search-hotkeys.js +62 -0
  148. data/assets/js/core/skip-links.js +62 -0
  149. data/assets/js/loader.js +163 -0
  150. data/assets/js/main.js +23 -0
  151. data/assets/js/math.js +818 -0
  152. data/assets/js/notebook.js +158 -0
  153. data/assets/js/search/analytics.js +91 -0
  154. data/assets/js/search/app.js +271 -0
  155. data/assets/js/search/autocomplete.js +120 -0
  156. data/assets/js/search/engine.js +260 -0
  157. data/assets/js/search/filters.js +38 -0
  158. data/assets/js/search/render.js +217 -0
  159. data/assets/js/search/utils.js +99 -0
  160. data/assets/js/search.js +354 -0
  161. data/assets/js/visualizations.js +816 -0
  162. data/assets/publications/datalog-publications.bib +8 -0
  163. data/assets/publications/datalog-publications.ris +9 -0
  164. data/assets/publications/publications.bib +30 -0
  165. data/assets/templates/diogo-ribeiro-cv.md +31 -0
  166. data/assets/templates/diogo-ribeiro-cv.tex +32 -0
  167. data/bin/datalog +26 -0
  168. data/datalog-theme.gemspec +72 -0
  169. data/lib/datalog/cli.rb +563 -0
  170. data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
  171. data/lib/datalog/plugin_system.rb +237 -0
  172. data/lib/datalog/plugins/citations.rb +182 -0
  173. data/lib/datalog/plugins/comments.rb +183 -0
  174. data/lib/datalog/plugins/search.rb +45 -0
  175. data/lib/datalog/plugins/slides.rb +121 -0
  176. data/lib/datalog/theme/theme.rb +18 -0
  177. data/lib/datalog/theme/version.rb +7 -0
  178. data/lib/datalog/warning_filter.rb +28 -0
  179. data/lib/datalog-theme.rb +17 -0
  180. metadata +555 -0
@@ -0,0 +1,682 @@
1
+ {%- comment -%}
2
+ Advanced Search System Component
3
+ Full-text search with filters, highlighting, and sorting
4
+
5
+ Parameters:
6
+ - show_filters: Show filter panel (default: true)
7
+ - show_sort: Show sort options (default: true)
8
+ - show_results_count: Show results count (default: true)
9
+ - highlight_matches: Highlight search matches (default: true)
10
+ - results_per_page: Results per page (default: 10)
11
+ {%- endcomment -%}
12
+
13
+ {%- assign show_filters = include.show_filters | default: true -%}
14
+ {%- assign show_sort = include.show_sort | default: true -%}
15
+ {%- assign show_results_count = include.show_results_count | default: true -%}
16
+ {%- assign highlight_matches = include.highlight_matches | default: true -%}
17
+ {%- assign results_per_page = include.results_per_page | default: 10 -%}
18
+
19
+ <div class="advanced-search"
20
+ data-advanced-search
21
+ role="search"
22
+ aria-label="Advanced search">
23
+
24
+ <div class="advanced-search__header">
25
+ <div class="search-input-wrapper">
26
+ <svg class="search-input-wrapper__icon"
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ width="20"
29
+ height="20"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ stroke-width="2"
34
+ aria-hidden="true">
35
+ <circle cx="11" cy="11" r="8"></circle>
36
+ <path d="m21 21-4.35-4.35"></path>
37
+ </svg>
38
+
39
+ <input type="search"
40
+ class="search-input-wrapper__input"
41
+ data-search-input
42
+ placeholder="Search posts, tags, categories..."
43
+ aria-label="Search"
44
+ autocomplete="off">
45
+
46
+ <button type="button"
47
+ class="search-input-wrapper__clear"
48
+ data-search-clear
49
+ aria-label="Clear search"
50
+ hidden>
51
+ <svg xmlns="http://www.w3.org/2000/svg"
52
+ width="18"
53
+ height="18"
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ stroke="currentColor"
57
+ stroke-width="2"
58
+ aria-hidden="true">
59
+ <line x1="18" y1="6" x2="6" y2="18"></line>
60
+ <line x1="6" y1="6" x2="18" y2="18"></line>
61
+ </svg>
62
+ </button>
63
+ </div>
64
+
65
+ <button type="button"
66
+ class="advanced-search__filter-toggle"
67
+ data-filter-toggle
68
+ aria-label="Toggle filters"
69
+ aria-expanded="false">
70
+ <svg xmlns="http://www.w3.org/2000/svg"
71
+ width="18"
72
+ height="18"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ stroke-width="2"
77
+ aria-hidden="true">
78
+ <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
79
+ </svg>
80
+ <span>Filters</span>
81
+ <span class="filter-count" data-filter-count hidden>0</span>
82
+ </button>
83
+ </div>
84
+
85
+ {%- if show_filters -%}
86
+ <div class="advanced-search__filters"
87
+ data-search-filters
88
+ hidden>
89
+
90
+ <div class="search-filter">
91
+ <h4 class="search-filter__title">Categories</h4>
92
+ <div class="search-filter__options" data-filter-group="category">
93
+ {%- assign all_categories = site.posts | map: "categories" | join: "," | split: "," | uniq | sort -%}
94
+ {%- for category in all_categories -%}
95
+ {%- if category != "" -%}
96
+ <label class="filter-checkbox">
97
+ <input type="checkbox"
98
+ value="{{ category | slugify }}"
99
+ data-filter="category">
100
+ <span>{{ category }}</span>
101
+ <span class="filter-checkbox__count">
102
+ {%- assign cat_count = 0 -%}
103
+ {%- for post in site.posts -%}
104
+ {%- if post.categories contains category -%}
105
+ {%- assign cat_count = cat_count | plus: 1 -%}
106
+ {%- endif -%}
107
+ {%- endfor -%}
108
+ ({{ cat_count }})
109
+ </span>
110
+ </label>
111
+ {%- endif -%}
112
+ {%- endfor -%}
113
+ </div>
114
+ </div>
115
+
116
+ <div class="search-filter">
117
+ <h4 class="search-filter__title">Tags</h4>
118
+ <div class="search-filter__options search-filter__options--scrollable"
119
+ data-filter-group="tag">
120
+ {%- assign all_tags = site.posts | map: "tags" | join: "," | split: "," | uniq | sort -%}
121
+ {%- for tag in all_tags limit: 20 -%}
122
+ {%- if tag != "" -%}
123
+ <label class="filter-checkbox">
124
+ <input type="checkbox"
125
+ value="{{ tag | slugify }}"
126
+ data-filter="tag">
127
+ <span>{{ tag }}</span>
128
+ </label>
129
+ {%- endif -%}
130
+ {%- endfor -%}
131
+ </div>
132
+ </div>
133
+
134
+ <div class="search-filter">
135
+ <h4 class="search-filter__title">Difficulty</h4>
136
+ <div class="search-filter__options" data-filter-group="difficulty">
137
+ {%- assign difficulties = "beginner,intermediate,advanced,expert" | split: "," -%}
138
+ {%- for difficulty in difficulties -%}
139
+ <label class="filter-checkbox">
140
+ <input type="checkbox"
141
+ value="{{ difficulty }}"
142
+ data-filter="difficulty">
143
+ <span>{{ difficulty | capitalize }}</span>
144
+ </label>
145
+ {%- endfor -%}
146
+ </div>
147
+ </div>
148
+
149
+ <div class="search-filter">
150
+ <h4 class="search-filter__title">Date Range</h4>
151
+ <div class="search-filter__options">
152
+ <label class="filter-radio">
153
+ <input type="radio"
154
+ name="dateRange"
155
+ value="all"
156
+ data-filter="date"
157
+ checked>
158
+ <span>All time</span>
159
+ </label>
160
+ <label class="filter-radio">
161
+ <input type="radio"
162
+ name="dateRange"
163
+ value="week"
164
+ data-filter="date">
165
+ <span>Past week</span>
166
+ </label>
167
+ <label class="filter-radio">
168
+ <input type="radio"
169
+ name="dateRange"
170
+ value="month"
171
+ data-filter="date">
172
+ <span>Past month</span>
173
+ </label>
174
+ <label class="filter-radio">
175
+ <input type="radio"
176
+ name="dateRange"
177
+ value="year"
178
+ data-filter="date">
179
+ <span>Past year</span>
180
+ </label>
181
+ </div>
182
+ </div>
183
+
184
+ <div class="search-filter__actions">
185
+ <button type="button"
186
+ class="button button--secondary"
187
+ data-clear-filters>
188
+ Clear all filters
189
+ </button>
190
+ </div>
191
+ </div>
192
+ {%- endif -%}
193
+
194
+ <div class="advanced-search__toolbar">
195
+ {%- if show_results_count -%}
196
+ <div class="search-results-count" data-results-count>
197
+ <span data-count>0</span> results
198
+ </div>
199
+ {%- endif -%}
200
+
201
+ {%- if show_sort -%}
202
+ <div class="search-sort">
203
+ <label for="search-sort-select">Sort by:</label>
204
+ <select id="search-sort-select"
205
+ class="search-sort__select"
206
+ data-sort-select>
207
+ <option value="relevance">Relevance</option>
208
+ <option value="date-desc">Newest first</option>
209
+ <option value="date-asc">Oldest first</option>
210
+ <option value="title">Title (A-Z)</option>
211
+ </select>
212
+ </div>
213
+ {%- endif -%}
214
+ </div>
215
+
216
+ <div class="advanced-search__results" data-search-results>
217
+ <div class="search-empty-state" data-empty-state>
218
+ <svg xmlns="http://www.w3.org/2000/svg"
219
+ width="64"
220
+ height="64"
221
+ viewBox="0 0 24 24"
222
+ fill="none"
223
+ stroke="currentColor"
224
+ stroke-width="1.5"
225
+ aria-hidden="true">
226
+ <circle cx="11" cy="11" r="8"></circle>
227
+ <path d="m21 21-4.35-4.35"></path>
228
+ </svg>
229
+ <h3>Start searching</h3>
230
+ <p>Enter keywords to find relevant posts</p>
231
+ </div>
232
+
233
+ <div class="search-no-results" data-no-results hidden>
234
+ <svg xmlns="http://www.w3.org/2000/svg"
235
+ width="64"
236
+ height="64"
237
+ viewBox="0 0 24 24"
238
+ fill="none"
239
+ stroke="currentColor"
240
+ stroke-width="1.5"
241
+ aria-hidden="true">
242
+ <circle cx="12" cy="12" r="10"></circle>
243
+ <line x1="12" y1="8" x2="12" y2="12"></line>
244
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
245
+ </svg>
246
+ <h3>No results found</h3>
247
+ <p>Try adjusting your search or filters</p>
248
+ </div>
249
+
250
+ <div class="search-results-list" data-results-list></div>
251
+
252
+ <div class="search-pagination" data-pagination hidden></div>
253
+ </div>
254
+ </div>
255
+
256
+ {%- comment -%} Generate search index {%- endcomment -%}
257
+ <script type="application/json" data-search-index>
258
+ [
259
+ {%- for post in site.posts -%}
260
+ {
261
+ "id": "{{ post.id | slugify }}",
262
+ "title": {{ post.title | jsonify }},
263
+ "url": "{{ post.url | relative_url }}",
264
+ "excerpt": {{ post.excerpt | strip_html | truncatewords: 50 | jsonify }},
265
+ "content": {{ post.content | strip_html | jsonify }},
266
+ "date": "{{ post.date | date_to_xmlschema }}",
267
+ "categories": {{ post.categories | jsonify }},
268
+ "tags": {{ post.tags | jsonify }},
269
+ "difficulty": "{{ post.difficulty | default: '' }}",
270
+ "author": "{{ post.author | default: site.author.name }}",
271
+ "readingTime": {{ post.content | strip_html | number_of_words | divided_by: 200 | ceil }}
272
+ }{% unless forloop.last %},{% endunless %}
273
+ {%- endfor -%}
274
+ ]
275
+ </script>
276
+
277
+ <script>
278
+ (function() {
279
+ 'use strict';
280
+
281
+ class AdvancedSearch {
282
+ constructor() {
283
+ this.searchIndex = [];
284
+ this.filteredResults = [];
285
+ this.currentQuery = '';
286
+ this.filters = {
287
+ category: [],
288
+ tag: [],
289
+ difficulty: [],
290
+ date: 'all'
291
+ };
292
+ this.sortBy = 'relevance';
293
+ this.currentPage = 1;
294
+ this.resultsPerPage = {{ results_per_page }};
295
+
296
+ this.init();
297
+ }
298
+
299
+ init() {
300
+ this.loadSearchIndex();
301
+ this.cacheElements();
302
+ this.attachEventListeners();
303
+ }
304
+
305
+ loadSearchIndex() {
306
+ const indexEl = document.querySelector('[data-search-index]');
307
+ if (indexEl) {
308
+ try {
309
+ this.searchIndex = JSON.parse(indexEl.textContent);
310
+ } catch (e) {
311
+ console.error('Failed to load search index:', e);
312
+ }
313
+ }
314
+ }
315
+
316
+ cacheElements() {
317
+ this.container = document.querySelector('[data-advanced-search]');
318
+ if (!this.container) return;
319
+
320
+ this.input = this.container.querySelector('[data-search-input]');
321
+ this.clearBtn = this.container.querySelector('[data-search-clear]');
322
+ this.filterToggle = this.container.querySelector('[data-filter-toggle]');
323
+ this.filtersPanel = this.container.querySelector('[data-search-filters]');
324
+ this.resultsList = this.container.querySelector('[data-results-list]');
325
+ this.resultsCount = this.container.querySelector('[data-results-count]');
326
+ this.sortSelect = this.container.querySelector('[data-sort-select]');
327
+ this.emptyState = this.container.querySelector('[data-empty-state]');
328
+ this.noResults = this.container.querySelector('[data-no-results]');
329
+ this.pagination = this.container.querySelector('[data-pagination]');
330
+ this.filterCount = this.container.querySelector('[data-filter-count]');
331
+ }
332
+
333
+ attachEventListeners() {
334
+ if (!this.input) return;
335
+
336
+ // Search input
337
+ let searchTimeout;
338
+ this.input.addEventListener('input', (e) => {
339
+ clearTimeout(searchTimeout);
340
+ searchTimeout = setTimeout(() => {
341
+ this.currentQuery = e.target.value.trim();
342
+ this.search();
343
+ }, 300);
344
+
345
+ this.clearBtn.hidden = !e.target.value;
346
+ });
347
+
348
+ // Clear button
349
+ this.clearBtn?.addEventListener('click', () => {
350
+ this.input.value = '';
351
+ this.currentQuery = '';
352
+ this.clearBtn.hidden = true;
353
+ this.search();
354
+ });
355
+
356
+ // Filter toggle
357
+ this.filterToggle?.addEventListener('click', () => {
358
+ const isHidden = this.filtersPanel.hidden;
359
+ this.filtersPanel.hidden = !isHidden;
360
+ this.filterToggle.setAttribute('aria-expanded', !isHidden);
361
+ });
362
+
363
+ // Filter checkboxes
364
+ const filterCheckboxes = this.container.querySelectorAll('[data-filter]');
365
+ filterCheckboxes.forEach(checkbox => {
366
+ checkbox.addEventListener('change', () => {
367
+ this.updateFilters();
368
+ this.search();
369
+ });
370
+ });
371
+
372
+ // Clear filters
373
+ const clearFiltersBtn = this.container.querySelector('[data-clear-filters]');
374
+ clearFiltersBtn?.addEventListener('click', () => {
375
+ this.clearFilters();
376
+ this.search();
377
+ });
378
+
379
+ // Sort
380
+ this.sortSelect?.addEventListener('change', (e) => {
381
+ this.sortBy = e.target.value;
382
+ this.sortResults();
383
+ this.renderResults();
384
+ });
385
+ }
386
+
387
+ updateFilters() {
388
+ // Category filters
389
+ const categoryFilters = Array.from(
390
+ this.container.querySelectorAll('[data-filter="category"]:checked')
391
+ ).map(cb => cb.value);
392
+
393
+ // Tag filters
394
+ const tagFilters = Array.from(
395
+ this.container.querySelectorAll('[data-filter="tag"]:checked')
396
+ ).map(cb => cb.value);
397
+
398
+ // Difficulty filters
399
+ const difficultyFilters = Array.from(
400
+ this.container.querySelectorAll('[data-filter="difficulty"]:checked')
401
+ ).map(cb => cb.value);
402
+
403
+ // Date filter
404
+ const dateFilter = this.container.querySelector('[data-filter="date"]:checked')?.value || 'all';
405
+
406
+ this.filters = {
407
+ category: categoryFilters,
408
+ tag: tagFilters,
409
+ difficulty: difficultyFilters,
410
+ date: dateFilter
411
+ };
412
+
413
+ // Update filter count
414
+ const totalFilters = categoryFilters.length + tagFilters.length + difficultyFilters.length +
415
+ (dateFilter !== 'all' ? 1 : 0);
416
+ if (this.filterCount) {
417
+ this.filterCount.textContent = totalFilters;
418
+ this.filterCount.hidden = totalFilters === 0;
419
+ }
420
+ }
421
+
422
+ clearFilters() {
423
+ const allCheckboxes = this.container.querySelectorAll('[data-filter]:checked');
424
+ allCheckboxes.forEach(cb => {
425
+ if (cb.type === 'checkbox') {
426
+ cb.checked = false;
427
+ } else if (cb.type === 'radio' && cb.value !== 'all') {
428
+ cb.checked = false;
429
+ } else if (cb.value === 'all') {
430
+ cb.checked = true;
431
+ }
432
+ });
433
+
434
+ this.filters = {
435
+ category: [],
436
+ tag: [],
437
+ difficulty: [],
438
+ date: 'all'
439
+ };
440
+
441
+ if (this.filterCount) {
442
+ this.filterCount.hidden = true;
443
+ }
444
+ }
445
+
446
+ search() {
447
+ if (!this.currentQuery && this.isFiltersEmpty()) {
448
+ this.showEmptyState();
449
+ return;
450
+ }
451
+
452
+ // Filter results
453
+ this.filteredResults = this.searchIndex.filter(post => {
454
+ // Text search
455
+ if (this.currentQuery) {
456
+ const query = this.currentQuery.toLowerCase();
457
+ const searchableText = `${post.title} ${post.excerpt} ${post.content} ${post.tags.join(' ')} ${post.categories.join(' ')}`.toLowerCase();
458
+ if (!searchableText.includes(query)) {
459
+ return false;
460
+ }
461
+ }
462
+
463
+ // Category filter
464
+ if (this.filters.category.length > 0) {
465
+ const postCategories = post.categories.map(c => c.toLowerCase().replace(/\s+/g, '-'));
466
+ if (!this.filters.category.some(f => postCategories.includes(f))) {
467
+ return false;
468
+ }
469
+ }
470
+
471
+ // Tag filter
472
+ if (this.filters.tag.length > 0) {
473
+ const postTags = post.tags.map(t => t.toLowerCase().replace(/\s+/g, '-'));
474
+ if (!this.filters.tag.some(f => postTags.includes(f))) {
475
+ return false;
476
+ }
477
+ }
478
+
479
+ // Difficulty filter
480
+ if (this.filters.difficulty.length > 0) {
481
+ if (!this.filters.difficulty.includes(post.difficulty.toLowerCase())) {
482
+ return false;
483
+ }
484
+ }
485
+
486
+ // Date filter
487
+ if (this.filters.date !== 'all') {
488
+ const postDate = new Date(post.date);
489
+ const now = new Date();
490
+ const daysDiff = (now - postDate) / (1000 * 60 * 60 * 24);
491
+
492
+ if (this.filters.date === 'week' && daysDiff > 7) return false;
493
+ if (this.filters.date === 'month' && daysDiff > 30) return false;
494
+ if (this.filters.date === 'year' && daysDiff > 365) return false;
495
+ }
496
+
497
+ return true;
498
+ });
499
+
500
+ // Calculate relevance scores for text search
501
+ if (this.currentQuery) {
502
+ this.filteredResults = this.filteredResults.map(post => {
503
+ const query = this.currentQuery.toLowerCase();
504
+ let score = 0;
505
+
506
+ // Title match (highest weight)
507
+ if (post.title.toLowerCase().includes(query)) score += 10;
508
+
509
+ // Exact title match
510
+ if (post.title.toLowerCase() === query) score += 50;
511
+
512
+ // Tags match
513
+ if (post.tags.some(t => t.toLowerCase().includes(query))) score += 5;
514
+
515
+ // Categories match
516
+ if (post.categories.some(c => c.toLowerCase().includes(query))) score += 5;
517
+
518
+ // Content match (lower weight)
519
+ const contentMatches = (post.content.toLowerCase().match(new RegExp(query, 'g')) || []).length;
520
+ score += Math.min(contentMatches, 10);
521
+
522
+ return { ...post, relevanceScore: score };
523
+ });
524
+ }
525
+
526
+ this.sortResults();
527
+ this.currentPage = 1;
528
+ this.renderResults();
529
+ }
530
+
531
+ sortResults() {
532
+ switch (this.sortBy) {
533
+ case 'relevance':
534
+ if (this.currentQuery) {
535
+ this.filteredResults.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
536
+ }
537
+ break;
538
+ case 'date-desc':
539
+ this.filteredResults.sort((a, b) => new Date(b.date) - new Date(a.date));
540
+ break;
541
+ case 'date-asc':
542
+ this.filteredResults.sort((a, b) => new Date(a.date) - new Date(b.date));
543
+ break;
544
+ case 'title':
545
+ this.filteredResults.sort((a, b) => a.title.localeCompare(b.title));
546
+ break;
547
+ }
548
+ }
549
+
550
+ renderResults() {
551
+ if (this.filteredResults.length === 0) {
552
+ this.showNoResults();
553
+ return;
554
+ }
555
+
556
+ this.emptyState.hidden = true;
557
+ this.noResults.hidden = true;
558
+ this.resultsList.hidden = false;
559
+
560
+ // Update count
561
+ if (this.resultsCount) {
562
+ const countEl = this.resultsCount.querySelector('[data-count]');
563
+ if (countEl) countEl.textContent = this.filteredResults.length;
564
+ }
565
+
566
+ // Pagination
567
+ const start = (this.currentPage - 1) * this.resultsPerPage;
568
+ const end = start + this.resultsPerPage;
569
+ const pageResults = this.filteredResults.slice(start, end);
570
+
571
+ // Render results
572
+ this.resultsList.innerHTML = pageResults.map(post => this.renderResultItem(post)).join('');
573
+
574
+ // Render pagination
575
+ this.renderPagination();
576
+ }
577
+
578
+ renderResultItem(post) {
579
+ const excerpt = this.highlightMatches(post.excerpt);
580
+ const title = this.highlightMatches(post.title);
581
+
582
+ return `
583
+ <article class="search-result">
584
+ <h3 class="search-result__title">
585
+ <a href="${post.url}">${title}</a>
586
+ </h3>
587
+ <p class="search-result__excerpt">${excerpt}</p>
588
+ <div class="search-result__meta">
589
+ <time datetime="${post.date}">
590
+ ${new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}
591
+ </time>
592
+ <span>•</span>
593
+ <span>${post.readingTime} min read</span>
594
+ ${post.difficulty ? `<span>•</span><span class="difficulty-badge difficulty-badge--${post.difficulty}">${post.difficulty}</span>` : ''}
595
+ </div>
596
+ </article>
597
+ `;
598
+ }
599
+
600
+ highlightMatches(text) {
601
+ if (!this.currentQuery || !{{ highlight_matches }}) return text;
602
+
603
+ const query = this.currentQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
604
+ const regex = new RegExp(`(${query})`, 'gi');
605
+ return text.replace(regex, '<mark>$1</mark>');
606
+ }
607
+
608
+ renderPagination() {
609
+ const totalPages = Math.ceil(this.filteredResults.length / this.resultsPerPage);
610
+
611
+ if (totalPages <= 1) {
612
+ this.pagination.hidden = true;
613
+ return;
614
+ }
615
+
616
+ this.pagination.hidden = false;
617
+
618
+ const buttons = [];
619
+ for (let i = 1; i <= totalPages; i++) {
620
+ const isActive = i === this.currentPage;
621
+ buttons.push(`
622
+ <button type="button"
623
+ class="pagination__button${isActive ? ' pagination__button--active' : ''}"
624
+ data-page="${i}"
625
+ ${isActive ? 'aria-current="page"' : ''}>
626
+ ${i}
627
+ </button>
628
+ `);
629
+ }
630
+
631
+ this.pagination.innerHTML = buttons.join('');
632
+
633
+ // Attach click handlers
634
+ this.pagination.querySelectorAll('[data-page]').forEach(btn => {
635
+ btn.addEventListener('click', (e) => {
636
+ this.currentPage = parseInt(e.target.dataset.page, 10);
637
+ this.renderResults();
638
+ this.resultsList.scrollIntoView({ behavior: 'smooth' });
639
+ });
640
+ });
641
+ }
642
+
643
+ showEmptyState() {
644
+ this.emptyState.hidden = false;
645
+ this.noResults.hidden = true;
646
+ this.resultsList.hidden = true;
647
+ this.pagination.hidden = true;
648
+
649
+ if (this.resultsCount) {
650
+ const countEl = this.resultsCount.querySelector('[data-count]');
651
+ if (countEl) countEl.textContent = '0';
652
+ }
653
+ }
654
+
655
+ showNoResults() {
656
+ this.emptyState.hidden = true;
657
+ this.noResults.hidden = false;
658
+ this.resultsList.hidden = true;
659
+ this.pagination.hidden = true;
660
+
661
+ if (this.resultsCount) {
662
+ const countEl = this.resultsCount.querySelector('[data-count]');
663
+ if (countEl) countEl.textContent = '0';
664
+ }
665
+ }
666
+
667
+ isFiltersEmpty() {
668
+ return this.filters.category.length === 0 &&
669
+ this.filters.tag.length === 0 &&
670
+ this.filters.difficulty.length === 0 &&
671
+ this.filters.date === 'all';
672
+ }
673
+ }
674
+
675
+ // Initialize
676
+ if (document.readyState === 'loading') {
677
+ document.addEventListener('DOMContentLoaded', () => new AdvancedSearch());
678
+ } else {
679
+ new AdvancedSearch();
680
+ }
681
+ })();
682
+ </script>