datalog-theme 0.6.1 → 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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +26 -19
  5. data/_data/cdn-integrity.yml +0 -30
  6. data/_includes/analytics/dashboard.html +3 -1
  7. data/_includes/components/api-function.html +20 -1
  8. data/_includes/components/author-bio.html +2 -2
  9. data/_includes/components/enhanced-code-block.html +1 -1
  10. data/_includes/components/hero.html +19 -1
  11. data/_includes/csp-meta.html +115 -11
  12. data/_includes/footer.html +19 -20
  13. data/_includes/head.html +61 -29
  14. data/_includes/header/navigation.html +12 -15
  15. data/_includes/header.html +29 -20
  16. data/_includes/layouts/default/article.html +5 -1
  17. data/_includes/meta/math-config.html +27 -1
  18. data/_includes/meta/schema.html +5 -2
  19. data/_includes/meta/scripts-loader.html +16 -31
  20. data/_includes/post/related-posts.html +4 -7
  21. data/_includes/scripts.html +1 -9
  22. data/_includes/search/index-data.json +131 -0
  23. data/_includes/search/page.html +136 -0
  24. data/_layouts/dataset.html +1 -0
  25. data/_layouts/default.html +16 -8
  26. data/_layouts/home.html +6 -0
  27. data/_layouts/notebook.html +1 -0
  28. data/_layouts/package.html +1 -0
  29. data/_layouts/portfolio.html +1 -0
  30. data/_layouts/post.html +20 -5
  31. data/_layouts/project.html +2 -1
  32. data/_plugins/analytics_dashboard.rb +9 -3
  33. data/_plugins/config_validator.rb +12 -7
  34. data/_plugins/csp_generator.rb +18 -28
  35. data/_plugins/datalog_bibliography.rb +9 -7
  36. data/_plugins/datalog_comments.rb +8 -5
  37. data/_plugins/datalog_slides.rb +9 -8
  38. data/_plugins/i18n.rb +7 -8
  39. data/_plugins/image_optimizer.rb +17 -6
  40. data/_plugins/math_preprocessor.rb +36 -3
  41. data/_plugins/notebook_converter.rb +23 -43
  42. data/_plugins/plugin_loader.rb +3 -1
  43. data/_plugins/publications_generator.rb +8 -2
  44. data/_plugins/rouge_highlight_filter.rb +42 -0
  45. data/_plugins/search_code_blocks.rb +30 -0
  46. data/_plugins/search_normalizer.rb +15 -53
  47. data/_plugins/search_pages.rb +72 -0
  48. data/_sass/_academic-dashboard.scss +262 -0
  49. data/_sass/_base.scss +19 -1
  50. data/_sass/_components.scss +77 -1144
  51. data/_sass/_features.scss +17 -0
  52. data/_sass/_font-fallbacks.scss +43 -0
  53. data/_sass/_header.scss +10 -0
  54. data/_sass/_layout.scss +18 -12
  55. data/_sass/_mathematical.scss +1 -1
  56. data/_sass/_notebooks.scss +322 -0
  57. data/_sass/_open-science-badges.scss +56 -0
  58. data/_sass/{_phase1-enhancements.scss → _post-components.scss} +46 -1
  59. data/_sass/_search-page.scss +530 -0
  60. data/_sass/_search.scss +47 -1
  61. data/_sass/_syntax-highlighting.scss +212 -97
  62. data/_sass/_theme.scss +42 -18
  63. data/_sass/_typography.scss +14 -0
  64. data/_sass/_variables.scss +7 -4
  65. data/assets/css/main.scss +14 -0
  66. data/assets/img/hero-detail-640.webp +0 -0
  67. data/assets/img/hero-detail.webp +0 -0
  68. data/assets/img/social-card.png +0 -0
  69. data/assets/js/dist/academic.js +1 -0
  70. data/assets/js/dist/analytics-dashboard.js +1 -0
  71. data/assets/js/dist/chunks/chunk-225H5YXE.js +1 -0
  72. data/assets/js/dist/core.js +1 -0
  73. data/assets/js/dist/loader.js +1 -0
  74. data/assets/js/dist/math.js +1 -0
  75. data/assets/js/dist/notebook.js +1 -0
  76. data/assets/js/dist/search.js +1 -0
  77. data/assets/js/dist/visualizations.js +11 -0
  78. data/assets/js/loader.js +3 -1
  79. data/datalog-theme.gemspec +43 -22
  80. data/lib/datalog/cli.rb +53 -15
  81. data/lib/datalog/plugin_system/dependency_resolver.rb +0 -2
  82. data/lib/datalog/theme/package.rb +57 -0
  83. data/lib/datalog/theme/repository_checkout.rb +90 -0
  84. data/lib/datalog/theme/version.rb +1 -1
  85. data/lib/datalog/warning_filter.rb +5 -11
  86. data/lib/datalog-theme.rb +21 -0
  87. metadata +75 -146
  88. data/_data/academic.yml +0 -217
  89. data/_data/config/author.yml +0 -121
  90. data/_data/config/features.yml +0 -262
  91. data/_data/config/site.yml +0 -42
  92. data/_data/config/theme.yml +0 -181
  93. data/_data/datasets.yml +0 -28
  94. data/_data/js_meta.json +0 -371
  95. data/_data/navigation.yml +0 -145
  96. data/_data/projects.yml +0 -41
  97. data/_data/publications.yml +0 -28
  98. data/_data/social.yml +0 -73
  99. data/_data/visualizations.yml +0 -51
  100. data/_includes/components/advanced-search.html +0 -682
  101. data/_includes/components/bookmark-system.html +0 -96
  102. data/_includes/components/comments.html +0 -244
  103. data/_includes/components/content-recommendations.html +0 -228
  104. data/_includes/components/email-preferences.html +0 -200
  105. data/_includes/components/enhanced-metadata.html +0 -228
  106. data/_includes/components/language-switcher.html +0 -396
  107. data/_includes/components/navigation-enhancements.html +0 -454
  108. data/_includes/components/newsletter-signup.html +0 -178
  109. data/_includes/components/popular-posts.html +0 -233
  110. data/_includes/components/reading-progress.html +0 -133
  111. data/_includes/components/reading-time.html +0 -121
  112. data/_includes/components/series-navigation.html +0 -124
  113. data/_includes/components/social-proof.html +0 -34
  114. data/_includes/components/user-preferences.html +0 -566
  115. data/_layouts/archive.html +0 -282
  116. data/_layouts/post-sidebar.html +0 -183
  117. data/_sass/_phase3-enhancements.scss +0 -874
  118. data/_sass/_phase4-enhancements.scss +0 -1214
  119. data/_sass/_phase5-enhancements.scss +0 -414
  120. data/assets/img/20220607123041_detail.001.png +0 -0
  121. data/assets/js/academic.js +0 -262
  122. data/assets/js/analytics-dashboard.js +0 -382
  123. data/assets/js/core/dark-mode.js +0 -79
  124. data/assets/js/core/github-cards.js +0 -123
  125. data/assets/js/core/language-filter.js +0 -69
  126. data/assets/js/core/navigation.js +0 -184
  127. data/assets/js/core/scroll-progress.js +0 -45
  128. data/assets/js/core/search-hotkeys.js +0 -62
  129. data/assets/js/core/skip-links.js +0 -62
  130. data/assets/js/main.js +0 -23
  131. data/assets/js/math.js +0 -818
  132. data/assets/js/notebook.js +0 -158
  133. data/assets/js/search/analytics.js +0 -91
  134. data/assets/js/search/app.js +0 -271
  135. data/assets/js/search/autocomplete.js +0 -120
  136. data/assets/js/search/engine.js +0 -260
  137. data/assets/js/search/filters.js +0 -38
  138. data/assets/js/search/render.js +0 -217
  139. data/assets/js/search/utils.js +0 -99
  140. data/assets/js/search.js +0 -354
  141. data/assets/js/visualizations.js +0 -816
  142. data/assets/publications/datalog-publications.bib +0 -8
  143. data/assets/publications/datalog-publications.ris +0 -9
  144. data/assets/publications/publications.bib +0 -30
  145. data/assets/templates/diogo-ribeiro-cv.md +0 -31
  146. data/assets/templates/diogo-ribeiro-cv.tex +0 -32
  147. data/lib/datalog/theme/theme.rb +0 -18
@@ -1,382 +0,0 @@
1
- /**
2
- * @fileoverview Analytics dashboard for displaying site metrics.
3
- * Renders visitor charts, engagement stats, and search term reports.
4
- * @module analytics-dashboard
5
- */
6
-
7
- /**
8
- * Retrieves analytics data from global window object.
9
- * @returns {Object} Analytics data object
10
- */
11
- function getAnalyticsData() {
12
- return window.__DATALOG_ANALYTICS__ || {};
13
- }
14
-
15
- /**
16
- * Executes callback when DOM is ready.
17
- * @param {function} callback - Function to execute
18
- * @returns {void}
19
- */
20
- export function onReady(callback) {
21
- if (document.readyState === "loading") {
22
- document.addEventListener("DOMContentLoaded", callback, { once: true });
23
- } else {
24
- callback();
25
- }
26
- }
27
-
28
- /**
29
- * Parses rows from an analytics report.
30
- * @param {Object} report - Report object with rows array
31
- * @returns {Array} Array of row objects
32
- */
33
- export function parseRows(report) {
34
- if (!report || !Array.isArray(report.rows)) {
35
- return [];
36
- }
37
- return report.rows;
38
- }
39
-
40
- /**
41
- * Formats a number for display using locale settings.
42
- * @param {number} value - Number to format
43
- * @param {Object} [options] - Intl.NumberFormat options
44
- * @returns {string} Formatted number string
45
- */
46
- export function formatNumber(value, options) {
47
- const formatter = new Intl.NumberFormat(undefined, options || { maximumFractionDigits: 0 });
48
- return formatter.format(value || 0);
49
- }
50
-
51
- /**
52
- * Displays a placeholder message in a table container.
53
- * @param {HTMLElement} container - Table body element
54
- * @param {string} message - Message to display
55
- * @param {number} [columns] - Number of columns to span
56
- * @returns {void}
57
- */
58
- export function ensurePlaceholder(container, message, columns) {
59
- if (!container) return;
60
- const colSpan = columns || container.closest("table")?.querySelectorAll("th").length || 1;
61
- const tr = document.createElement("tr");
62
- tr.className = "placeholder";
63
- const td = document.createElement("td");
64
- td.setAttribute("colspan", colSpan);
65
- td.textContent = message;
66
- tr.appendChild(td);
67
- container.replaceChildren(tr);
68
- }
69
-
70
- /**
71
- * Renders the top posts table with page view and engagement data.
72
- * @returns {void}
73
- */
74
- export function renderTopPosts() {
75
- const tbody = document.getElementById("analytics-top-posts");
76
- const rows = parseRows(getAnalyticsData().top_posts);
77
- if (!tbody) return;
78
-
79
- if (!rows.length) {
80
- ensurePlaceholder(tbody, tbody.dataset.emptyMessage || "No data available", 3);
81
- return;
82
- }
83
-
84
- tbody.replaceChildren();
85
- rows.forEach((row) => {
86
- const dimensions = row.dimensionValues || [];
87
- const metrics = row.metricValues || [];
88
- const path = dimensions[0]?.value || "";
89
- const views = Number(metrics[0]?.value || 0);
90
- const engagement = Number(metrics[1]?.value || 0);
91
- const tr = document.createElement("tr");
92
- const link = document.createElement("a");
93
- link.href = path.startsWith("/") ? path : `/${path}`;
94
- link.textContent = path || "—";
95
- link.rel = "noopener";
96
- link.className = "analytics-link";
97
-
98
- const pathCell = document.createElement("td");
99
- pathCell.appendChild(link);
100
-
101
- const viewsCell = document.createElement("td");
102
- viewsCell.textContent = formatNumber(views);
103
-
104
- const engagementCell = document.createElement("td");
105
- engagementCell.textContent = formatNumber(engagement, { maximumFractionDigits: 0 });
106
-
107
- tr.append(pathCell, viewsCell, engagementCell);
108
- tbody.appendChild(tr);
109
- });
110
- }
111
-
112
- export function renderSearchTerms() {
113
- const tbody = document.getElementById("analytics-search-terms");
114
- if (!tbody) return;
115
- const rows = parseRows(getAnalyticsData().search_terms);
116
-
117
- if (!rows.length) {
118
- ensurePlaceholder(tbody, tbody.dataset.emptyMessage || "No search queries recorded", 2);
119
- return;
120
- }
121
-
122
- tbody.replaceChildren();
123
- rows.forEach((row) => {
124
- const dimensions = row.dimensionValues || [];
125
- const metrics = row.metricValues || [];
126
- const term = dimensions[0]?.value || "(not provided)";
127
- const sessions = Number(metrics[0]?.value || 0);
128
- const tr = document.createElement("tr");
129
- const termCell = document.createElement("td");
130
- termCell.textContent = term;
131
- const countCell = document.createElement("td");
132
- countCell.textContent = formatNumber(sessions);
133
- tr.append(termCell, countCell);
134
- tbody.appendChild(tr);
135
- });
136
- }
137
-
138
- export function formatDateLabel(value) {
139
- if (!value) return value;
140
- if (value.includes("-")) return value;
141
- if (value.length === 8) {
142
- return `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`;
143
- }
144
- return value;
145
- }
146
-
147
- export function renderVisitorChart() {
148
- const canvas = document.getElementById("analytics-visitors-chart");
149
- if (!canvas || typeof Chart === "undefined") return;
150
-
151
- const rows = parseRows(getAnalyticsData().visitor_trends);
152
- if (!rows.length) return;
153
-
154
- const labels = rows.map((row) => formatDateLabel(row.dimensionValues?.[0]?.value));
155
- const totalUsers = rows.map((row) => Number(row.metricValues?.[0]?.value || 0));
156
- const newUsers = rows.map((row) => Number(row.metricValues?.[1]?.value || 0));
157
- const sessions = rows.map((row) => Number(row.metricValues?.[2]?.value || 0));
158
- const pageViews = rows.map((row) => Number(row.metricValues?.[3]?.value || 0));
159
-
160
- new Chart(canvas, {
161
- type: "line",
162
- data: {
163
- labels,
164
- datasets: [
165
- {
166
- label: "Total users",
167
- data: totalUsers,
168
- borderColor: "#2563eb",
169
- backgroundColor: "rgba(37, 99, 235, 0.15)",
170
- tension: 0.35,
171
- fill: true,
172
- },
173
- {
174
- label: "New users",
175
- data: newUsers,
176
- borderColor: "#10b981",
177
- backgroundColor: "rgba(16, 185, 129, 0.12)",
178
- tension: 0.35,
179
- fill: false,
180
- },
181
- {
182
- label: "Sessions",
183
- data: sessions,
184
- borderColor: "#9333ea",
185
- backgroundColor: "rgba(147, 51, 234, 0.1)",
186
- tension: 0.35,
187
- fill: false,
188
- },
189
- {
190
- label: "Page views",
191
- data: pageViews,
192
- borderColor: "#f97316",
193
- backgroundColor: "rgba(249, 115, 22, 0.1)",
194
- borderDash: [6, 6],
195
- tension: 0.35,
196
- fill: false,
197
- },
198
- ],
199
- },
200
- options: {
201
- responsive: true,
202
- maintainAspectRatio: false,
203
- interaction: { mode: "index", intersect: false },
204
- scales: {
205
- y: {
206
- beginAtZero: true,
207
- ticks: { callback: (val) => formatNumber(val) },
208
- },
209
- },
210
- plugins: {
211
- legend: { position: "top" },
212
- },
213
- },
214
- });
215
- }
216
-
217
- export function renderEngagementChart() {
218
- const canvas = document.getElementById("analytics-engagement-chart");
219
- if (!canvas || typeof Chart === "undefined") return;
220
- const rows = parseRows(getAnalyticsData().engagement_by_page).slice(0, 10);
221
- if (!rows.length) return;
222
-
223
- const labels = rows.map((row) => row.dimensionValues?.[0]?.value || "—");
224
- const engagedSessions = rows.map((row) => Number(row.metricValues?.[2]?.value || 0));
225
- const views = rows.map((row) => Number(row.metricValues?.[0]?.value || 0));
226
-
227
- new Chart(canvas, {
228
- type: "bar",
229
- data: {
230
- labels,
231
- datasets: [
232
- {
233
- label: "Engaged sessions",
234
- data: engagedSessions,
235
- backgroundColor: "rgba(59, 130, 246, 0.7)",
236
- },
237
- {
238
- label: "Page views",
239
- data: views,
240
- backgroundColor: "rgba(14, 165, 233, 0.5)",
241
- },
242
- ],
243
- },
244
- options: {
245
- responsive: true,
246
- maintainAspectRatio: false,
247
- indexAxis: "y",
248
- scales: {
249
- x: { beginAtZero: true, ticks: { callback: (val) => formatNumber(val) } },
250
- },
251
- plugins: {
252
- legend: { position: "top" },
253
- },
254
- },
255
- });
256
- }
257
-
258
- export function renderKeyEvents() {
259
- const container = document.getElementById("analytics-key-events");
260
- if (!container) return;
261
- const rows = parseRows(getAnalyticsData().key_events);
262
- if (!rows.length) {
263
- const li = document.createElement("li");
264
- li.className = "placeholder";
265
- li.textContent = "No tracked events in the selected window.";
266
- container.replaceChildren(li);
267
- return;
268
- }
269
-
270
- container.replaceChildren();
271
- rows.forEach((row) => {
272
- const name = row.dimensionValues?.[0]?.value || "event";
273
- const count = Number(row.metricValues?.[0]?.value || 0);
274
- const li = document.createElement("li");
275
- const strong = document.createElement("strong");
276
- strong.textContent = formatNumber(count);
277
- li.appendChild(strong);
278
- li.appendChild(document.createTextNode(` ${name.replace(/_/g, " ")}`));
279
- container.appendChild(li);
280
- });
281
- }
282
-
283
- export function renderScholar() {
284
- const metrics = getAnalyticsData().scholar || {};
285
- const totalField = document.querySelector('[data-field="scholar-total"]');
286
- const hIndexField = document.querySelector('[data-field="scholar-h-index"]');
287
- const i10Field = document.querySelector('[data-field="scholar-i10-index"]');
288
- if (totalField) totalField.textContent = metrics.total ? formatNumber(Number(metrics.total)) : "—";
289
- if (hIndexField) hIndexField.textContent = metrics.h_index ? formatNumber(Number(metrics.h_index)) : "—";
290
- if (i10Field) i10Field.textContent = metrics.i10_index ? formatNumber(Number(metrics.i10_index)) : "—";
291
-
292
- const canvas = document.getElementById("analytics-scholar-chart");
293
- if (!canvas || typeof Chart === "undefined") return;
294
- const yearly = metrics.yearly_totals || {};
295
- const entries = Object.entries(yearly)
296
- .map(([year, value]) => [Number(year), Number(value)])
297
- .sort((a, b) => a[0] - b[0]);
298
- if (!entries.length) return;
299
-
300
- new Chart(canvas, {
301
- type: "bar",
302
- data: {
303
- labels: entries.map((item) => item[0]),
304
- datasets: [
305
- {
306
- label: "Citations",
307
- data: entries.map((item) => item[1]),
308
- backgroundColor: "rgba(99, 102, 241, 0.8)",
309
- },
310
- ],
311
- },
312
- options: {
313
- responsive: true,
314
- maintainAspectRatio: false,
315
- scales: {
316
- y: { beginAtZero: true, ticks: { callback: (val) => formatNumber(val) } },
317
- },
318
- },
319
- });
320
- }
321
-
322
- export function renderMonthlyReports() {
323
- const tbody = document.getElementById("analytics-monthly-reports");
324
- if (!tbody) return;
325
- const reports = Array.isArray(getAnalyticsData().monthly_reports) ? getAnalyticsData().monthly_reports : [];
326
- if (!reports.length) {
327
- ensurePlaceholder(tbody, tbody.dataset.emptyMessage || "No monthly reports available", 5);
328
- return;
329
- }
330
-
331
- tbody.replaceChildren();
332
- reports.forEach((report) => {
333
- const tr = document.createElement("tr");
334
- const month = document.createElement("td");
335
- month.textContent = report.month || "—";
336
- const total = document.createElement("td");
337
- total.textContent = formatNumber(report.total_users || 0);
338
- const news = document.createElement("td");
339
- news.textContent = formatNumber(report.new_users || 0);
340
- const sessions = document.createElement("td");
341
- sessions.textContent = formatNumber(report.sessions || 0);
342
- const views = document.createElement("td");
343
- views.textContent = formatNumber(report.page_views || 0);
344
- tr.append(month, total, news, sessions, views);
345
- tbody.appendChild(tr);
346
- });
347
- }
348
-
349
- export function render() {
350
- const status = getAnalyticsData().status;
351
- if (status !== "ok") {
352
- ["analytics-top-posts", "analytics-search-terms", "analytics-monthly-reports"].forEach((id) => {
353
- const el = document.getElementById(id);
354
- if (el) {
355
- ensurePlaceholder(el, getAnalyticsData().message || "Analytics data unavailable.");
356
- }
357
- });
358
- const events = document.getElementById("analytics-key-events");
359
- if (events) {
360
- const li = document.createElement("li");
361
- li.className = "placeholder";
362
- li.textContent = getAnalyticsData().message || "Analytics data unavailable.";
363
- events.replaceChildren(li);
364
- }
365
- renderScholar();
366
- return;
367
- }
368
-
369
- renderTopPosts();
370
- renderSearchTerms();
371
- renderVisitorChart();
372
- renderEngagementChart();
373
- renderKeyEvents();
374
- renderScholar();
375
- renderMonthlyReports();
376
- }
377
-
378
- // Auto-initialize when module loads (backward compatibility)
379
- // Skip auto-init in test environment
380
- if (typeof window !== 'undefined' && !window.__VITEST__) {
381
- onReady(render);
382
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * @fileoverview Dark mode toggle functionality.
3
- * Persists user preference and respects system color scheme.
4
- * @module core/dark-mode
5
- */
6
-
7
- /** @constant {string} LocalStorage key for color mode preference */
8
- const STORAGE_KEY = "datalog-color-mode";
9
- /** @constant {string} Class applied to body for dark mode */
10
- const DARK_CLASS = "dark-mode";
11
-
12
- /**
13
- * Resolves the initial dark mode preference from storage or system settings.
14
- * @param {MediaQueryList} prefersDarkScheme - Media query for dark scheme preference
15
- * @returns {boolean} True if dark mode should be enabled
16
- */
17
- function resolveInitialPreference(prefersDarkScheme) {
18
- const storedPreference = localStorage.getItem(STORAGE_KEY);
19
- if (storedPreference === "dark") {
20
- return true;
21
- }
22
- if (storedPreference === "light") {
23
- return false;
24
- }
25
- return prefersDarkScheme.matches;
26
- }
27
-
28
- /**
29
- * Initializes the dark mode toggle button and system preference listener.
30
- * Reads initial preference from localStorage or system settings.
31
- * @returns {void}
32
- */
33
- export function initDarkModeToggle() {
34
- const toggleButton = document.querySelector("[data-toggle-dark-mode]");
35
- const body = document.body;
36
- const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
37
-
38
- if (resolveInitialPreference(prefersDarkScheme)) {
39
- body.classList.add(DARK_CLASS);
40
- }
41
-
42
- /** Mirrors the effective theme on <body data-theme> for CSS hooks and tests. */
43
- const syncThemeAttribute = () => {
44
- const theme = body.classList.contains(DARK_CLASS) ? "dark" : "light";
45
- body.dataset.theme = theme;
46
- document.documentElement.dataset.theme = theme;
47
- };
48
-
49
- const syncButton = () => {
50
- syncThemeAttribute();
51
- if (!toggleButton) {
52
- return;
53
- }
54
- toggleButton.setAttribute("aria-pressed", body.classList.contains(DARK_CLASS));
55
- };
56
-
57
- syncButton();
58
-
59
- if (toggleButton) {
60
- toggleButton.addEventListener("click", () => {
61
- const isDark = body.classList.toggle(DARK_CLASS);
62
- toggleButton.setAttribute("aria-pressed", String(isDark));
63
- localStorage.setItem(STORAGE_KEY, isDark ? "dark" : "light");
64
- syncThemeAttribute();
65
- });
66
- }
67
-
68
- prefersDarkScheme.addEventListener("change", (event) => {
69
- if (localStorage.getItem(STORAGE_KEY)) {
70
- return;
71
- }
72
- if (event.matches) {
73
- body.classList.add(DARK_CLASS);
74
- } else {
75
- body.classList.remove(DARK_CLASS);
76
- }
77
- syncButton();
78
- });
79
- }
@@ -1,123 +0,0 @@
1
- /**
2
- * @fileoverview GitHub repository cards with live stats.
3
- * Fetches and displays repository statistics with caching.
4
- * @module core/github-cards
5
- */
6
-
7
- /** @constant {string} Prefix for localStorage cache keys */
8
- const CACHE_PREFIX = "datalog-github-";
9
-
10
- /**
11
- * Generates a cache key for a repository.
12
- * @param {string} owner - Repository owner
13
- * @param {string} repo - Repository name
14
- * @returns {string} Cache key
15
- */
16
- function cacheKey(owner, repo) {
17
- return `${CACHE_PREFIX}${owner}-${repo}`;
18
- }
19
-
20
- function readCache(owner, repo, cacheTtl) {
21
- try {
22
- const raw = localStorage.getItem(cacheKey(owner, repo));
23
- if (!raw) {
24
- return null;
25
- }
26
- const parsed = JSON.parse(raw);
27
- if (!parsed || typeof parsed !== "object") {
28
- return null;
29
- }
30
- if (Date.now() - parsed.timestamp > cacheTtl) {
31
- localStorage.removeItem(cacheKey(owner, repo));
32
- return null;
33
- }
34
- return parsed.data;
35
- } catch (error) {
36
- console.warn("GitHub cache read failed", error);
37
- return null;
38
- }
39
- }
40
-
41
- function writeCache(owner, repo, data) {
42
- try {
43
- localStorage.setItem(
44
- cacheKey(owner, repo),
45
- JSON.stringify({ timestamp: Date.now(), data })
46
- );
47
- } catch (error) {
48
- console.warn("GitHub cache write failed", error);
49
- }
50
- }
51
-
52
- async function fetchRepository(owner, repo, apiBase, cacheTtl) {
53
- const cached = readCache(owner, repo, cacheTtl);
54
- if (cached) {
55
- return cached;
56
- }
57
- try {
58
- const response = await fetch(`${apiBase}/${owner}/${repo}`);
59
- if (!response.ok) {
60
- throw new Error(`GitHub API error: ${response.status}`);
61
- }
62
- const json = await response.json();
63
- writeCache(owner, repo, json);
64
- return json;
65
- } catch (error) {
66
- console.warn(`Unable to load GitHub data for ${owner}/${repo}`);
67
- return null;
68
- }
69
- }
70
-
71
- function applyRepositoryData(card, data) {
72
- const statElements = card.querySelectorAll("[data-github-stat]");
73
- statElements.forEach((element) => {
74
- const stat = element.getAttribute("data-github-stat");
75
- if (!stat) {
76
- return;
77
- }
78
- const value = data[stat];
79
- if (typeof value === "number") {
80
- element.textContent = value.toLocaleString();
81
- } else if (typeof value === "string" && value.length > 0) {
82
- element.textContent = value;
83
- }
84
- });
85
- }
86
-
87
- /**
88
- * Initializes GitHub repository cards by fetching live stats.
89
- * Uses localStorage caching to minimize API calls.
90
- * @returns {Promise<void[]>} Resolves when all cards are populated
91
- */
92
- export function initGitHubCards() {
93
- const cards = document.querySelectorAll("[data-github-owner][data-github-repo]");
94
- if (!cards.length) {
95
- return Promise.resolve();
96
- }
97
-
98
- const integrations = window.DatalogIntegrations || {};
99
- const githubConfig = integrations.github || {};
100
- if (githubConfig.enabled === false) {
101
- return Promise.resolve();
102
- }
103
-
104
- const apiBase = githubConfig.api_base || "https://api.github.com/repos";
105
- const cacheTtl = Number(githubConfig.cache_ttl || 43200) * 1000;
106
-
107
- const promises = Array.from(cards).map((card) => {
108
- const owner = card.getAttribute("data-github-owner");
109
- const repo = card.getAttribute("data-github-repo");
110
- if (!owner || !repo) {
111
- return Promise.resolve();
112
- }
113
-
114
- return fetchRepository(owner, repo, apiBase, cacheTtl).then((data) => {
115
- if (!data) {
116
- return;
117
- }
118
- applyRepositoryData(card, data);
119
- });
120
- });
121
-
122
- return Promise.all(promises);
123
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * @fileoverview Navigate-on-change select handling.
3
- * @module core/language-filter
4
- */
5
-
6
- const NAVIGATING_SELECTS = "[data-language-filter], [data-navigate-on-change]";
7
-
8
- /**
9
- * Resolves an option value to a target that is safe to navigate to.
10
- *
11
- * Option values come from site data and front matter, so they are not visitor
12
- * controlled, but rejecting non-http(s) schemes keeps a stray `javascript:`
13
- * value from turning a dropdown into a script sink. Relative values carry no
14
- * scheme and pass through untouched, so existing links keep working.
15
- *
16
- * @param {unknown} value Raw option value.
17
- * @returns {string|null} The value to navigate to, or null when unsafe.
18
- */
19
- function safeNavigationTarget(value) {
20
- if (typeof value !== "string") {
21
- return null;
22
- }
23
-
24
- // Browsers ignore control characters when parsing a scheme, so strip them
25
- // first; otherwise "java script:alert(1)" would read as scheme-less here
26
- // and still execute once assigned. Done by code point rather than a regex,
27
- // which would trip no-control-regex.
28
- const candidate = Array.from(value)
29
- .filter((character) => {
30
- const code = character.codePointAt(0);
31
- return code > 0x1f && code !== 0x7f;
32
- })
33
- .join("")
34
- .trim();
35
- if (!candidate) {
36
- return null;
37
- }
38
-
39
- const scheme = /^[a-z][a-z0-9+.-]*:/i.exec(candidate);
40
- if (scheme && !/^https?:$/i.test(scheme[0])) {
41
- return null;
42
- }
43
-
44
- return candidate;
45
- }
46
-
47
- /**
48
- * Binds selects that navigate when their value changes.
49
- *
50
- * Replaces inline `onchange` handlers, which the site's CSP blocks because
51
- * script-src carries no 'unsafe-inline'.
52
- *
53
- * @returns {void}
54
- */
55
- export function initLanguageFilter() {
56
- const selects = document.querySelectorAll(NAVIGATING_SELECTS);
57
- if (selects.length === 0) {
58
- return;
59
- }
60
-
61
- selects.forEach((select) => {
62
- select.addEventListener("change", (event) => {
63
- const target = safeNavigationTarget(event.target && event.target.value);
64
- if (target) {
65
- window.location.href = target;
66
- }
67
- });
68
- });
69
- }