datalog-theme 0.7.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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +125 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +23 -17
  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/enhanced-code-block.html +1 -1
  9. data/_includes/csp-meta.html +115 -11
  10. data/_includes/footer.html +19 -20
  11. data/_includes/head.html +60 -23
  12. data/_includes/header/navigation.html +12 -15
  13. data/_includes/header.html +28 -19
  14. data/_includes/layouts/default/article.html +5 -1
  15. data/_includes/meta/math-config.html +9 -5
  16. data/_includes/meta/schema.html +5 -2
  17. data/_includes/meta/scripts-loader.html +16 -32
  18. data/_includes/post/related-posts.html +4 -7
  19. data/_includes/search/index-data.json +9 -34
  20. data/_layouts/dataset.html +1 -0
  21. data/_layouts/default.html +16 -8
  22. data/_layouts/notebook.html +1 -0
  23. data/_layouts/package.html +1 -0
  24. data/_layouts/portfolio.html +1 -0
  25. data/_layouts/post.html +20 -5
  26. data/_layouts/project.html +2 -1
  27. data/_plugins/analytics_dashboard.rb +9 -3
  28. data/_plugins/config_validator.rb +12 -7
  29. data/_plugins/csp_generator.rb +18 -28
  30. data/_plugins/datalog_bibliography.rb +9 -7
  31. data/_plugins/datalog_comments.rb +8 -5
  32. data/_plugins/datalog_slides.rb +9 -8
  33. data/_plugins/i18n.rb +7 -8
  34. data/_plugins/image_optimizer.rb +17 -6
  35. data/_plugins/math_preprocessor.rb +33 -3
  36. data/_plugins/notebook_converter.rb +23 -4
  37. data/_plugins/plugin_loader.rb +3 -1
  38. data/_plugins/publications_generator.rb +8 -2
  39. data/_plugins/rouge_highlight_filter.rb +42 -0
  40. data/_plugins/search_code_blocks.rb +30 -0
  41. data/_plugins/search_normalizer.rb +15 -53
  42. data/_plugins/search_pages.rb +3 -4
  43. data/_sass/_academic-dashboard.scss +262 -0
  44. data/_sass/_base.scss +12 -0
  45. data/_sass/_components.scss +64 -1153
  46. data/_sass/_features.scss +17 -0
  47. data/_sass/_layout.scss +1 -1
  48. data/_sass/_notebooks.scss +322 -0
  49. data/_sass/_open-science-badges.scss +56 -0
  50. data/_sass/{_phase1-enhancements.scss → _post-components.scss} +1 -1
  51. data/_sass/_search-page.scss +530 -0
  52. data/_sass/_search.scss +46 -0
  53. data/_sass/_syntax-highlighting.scss +212 -97
  54. data/_sass/_theme.scss +29 -19
  55. data/_sass/_typography.scss +14 -0
  56. data/assets/css/main.scss +14 -0
  57. data/assets/js/dist/academic.js +1 -1
  58. data/assets/js/dist/analytics-dashboard.js +1 -1
  59. data/assets/js/dist/chunks/chunk-225H5YXE.js +1 -0
  60. data/assets/js/dist/core.js +1 -1
  61. data/assets/js/dist/loader.js +1 -1
  62. data/assets/js/dist/notebook.js +1 -1
  63. data/assets/js/dist/search.js +1 -1
  64. data/assets/js/dist/visualizations.js +11 -2
  65. data/assets/js/loader.js +3 -1
  66. data/datalog-theme.gemspec +36 -23
  67. data/lib/datalog/cli.rb +34 -14
  68. data/lib/datalog/plugin_system/dependency_resolver.rb +0 -2
  69. data/lib/datalog/theme/package.rb +57 -0
  70. data/lib/datalog/theme/repository_checkout.rb +90 -0
  71. data/lib/datalog/theme/version.rb +1 -1
  72. data/lib/datalog/warning_filter.rb +5 -11
  73. data/lib/datalog-theme.rb +5 -0
  74. metadata +48 -133
  75. data/_data/academic.yml +0 -217
  76. data/_data/config/author.yml +0 -121
  77. data/_data/datasets.yml +0 -28
  78. data/_data/js_meta.json +0 -371
  79. data/_data/navigation.yml +0 -145
  80. data/_data/projects.yml +0 -41
  81. data/_data/publications.yml +0 -28
  82. data/_data/social.yml +0 -73
  83. data/_data/visualizations.yml +0 -51
  84. data/_includes/components/advanced-search.html +0 -682
  85. data/_includes/components/bookmark-system.html +0 -96
  86. data/_includes/components/comments.html +0 -244
  87. data/_includes/components/content-recommendations.html +0 -228
  88. data/_includes/components/email-preferences.html +0 -200
  89. data/_includes/components/enhanced-metadata.html +0 -228
  90. data/_includes/components/language-switcher.html +0 -396
  91. data/_includes/components/navigation-enhancements.html +0 -454
  92. data/_includes/components/newsletter-signup.html +0 -178
  93. data/_includes/components/popular-posts.html +0 -233
  94. data/_includes/components/reading-progress.html +0 -133
  95. data/_includes/components/reading-time.html +0 -121
  96. data/_includes/components/series-navigation.html +0 -124
  97. data/_includes/components/social-proof.html +0 -34
  98. data/_includes/components/user-preferences.html +0 -566
  99. data/_includes/meta/syntax-config.html +0 -19
  100. data/_layouts/archive.html +0 -282
  101. data/_layouts/post-sidebar.html +0 -183
  102. data/_sass/_phase3-enhancements.scss +0 -874
  103. data/_sass/_phase4-enhancements.scss +0 -1214
  104. data/_sass/_phase5-enhancements.scss +0 -414
  105. data/assets/js/academic.js +0 -262
  106. data/assets/js/analytics-dashboard.js +0 -382
  107. data/assets/js/core/dark-mode.js +0 -79
  108. data/assets/js/core/github-cards.js +0 -123
  109. data/assets/js/core/language-filter.js +0 -69
  110. data/assets/js/core/navigation.js +0 -184
  111. data/assets/js/core/scroll-progress.js +0 -45
  112. data/assets/js/core/search-hotkeys.js +0 -62
  113. data/assets/js/core/skip-links.js +0 -62
  114. data/assets/js/dist/manifest.json +0 -22
  115. data/assets/js/dist/meta.json +0 -371
  116. data/assets/js/main.js +0 -23
  117. data/assets/js/math.js +0 -818
  118. data/assets/js/notebook.js +0 -158
  119. data/assets/js/search/analytics.js +0 -91
  120. data/assets/js/search/app.js +0 -271
  121. data/assets/js/search/autocomplete.js +0 -120
  122. data/assets/js/search/engine.js +0 -260
  123. data/assets/js/search/filters.js +0 -45
  124. data/assets/js/search/render.js +0 -217
  125. data/assets/js/search/utils.js +0 -99
  126. data/assets/js/search.js +0 -354
  127. data/assets/js/visualizations.js +0 -816
  128. data/assets/publications/datalog-publications.bib +0 -8
  129. data/assets/publications/datalog-publications.ris +0 -9
  130. data/assets/publications/publications.bib +0 -30
  131. data/assets/templates/diogo-ribeiro-cv.md +0 -31
  132. data/assets/templates/diogo-ribeiro-cv.tex +0 -32
  133. 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
- }