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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -0
- data/CITATION.cff +2 -2
- data/README.md +23 -17
- data/_data/cdn-integrity.yml +0 -30
- data/_includes/analytics/dashboard.html +3 -1
- data/_includes/components/api-function.html +20 -1
- data/_includes/components/enhanced-code-block.html +1 -1
- data/_includes/csp-meta.html +115 -11
- data/_includes/footer.html +19 -20
- data/_includes/head.html +60 -23
- data/_includes/header/navigation.html +12 -15
- data/_includes/header.html +28 -19
- data/_includes/layouts/default/article.html +5 -1
- data/_includes/meta/math-config.html +9 -5
- data/_includes/meta/schema.html +5 -2
- data/_includes/meta/scripts-loader.html +16 -32
- data/_includes/post/related-posts.html +4 -7
- data/_includes/search/index-data.json +9 -34
- data/_layouts/dataset.html +1 -0
- data/_layouts/default.html +16 -8
- data/_layouts/notebook.html +1 -0
- data/_layouts/package.html +1 -0
- data/_layouts/portfolio.html +1 -0
- data/_layouts/post.html +20 -5
- data/_layouts/project.html +2 -1
- data/_plugins/analytics_dashboard.rb +9 -3
- data/_plugins/config_validator.rb +12 -7
- data/_plugins/csp_generator.rb +18 -28
- data/_plugins/datalog_bibliography.rb +9 -7
- data/_plugins/datalog_comments.rb +8 -5
- data/_plugins/datalog_slides.rb +9 -8
- data/_plugins/i18n.rb +7 -8
- data/_plugins/image_optimizer.rb +17 -6
- data/_plugins/math_preprocessor.rb +33 -3
- data/_plugins/notebook_converter.rb +23 -4
- data/_plugins/plugin_loader.rb +3 -1
- data/_plugins/publications_generator.rb +8 -2
- data/_plugins/rouge_highlight_filter.rb +42 -0
- data/_plugins/search_code_blocks.rb +30 -0
- data/_plugins/search_normalizer.rb +15 -53
- data/_plugins/search_pages.rb +3 -4
- data/_sass/_academic-dashboard.scss +262 -0
- data/_sass/_base.scss +12 -0
- data/_sass/_components.scss +64 -1153
- data/_sass/_features.scss +17 -0
- data/_sass/_layout.scss +1 -1
- data/_sass/_notebooks.scss +322 -0
- data/_sass/_open-science-badges.scss +56 -0
- data/_sass/{_phase1-enhancements.scss → _post-components.scss} +1 -1
- data/_sass/_search-page.scss +530 -0
- data/_sass/_search.scss +46 -0
- data/_sass/_syntax-highlighting.scss +212 -97
- data/_sass/_theme.scss +29 -19
- data/_sass/_typography.scss +14 -0
- data/assets/css/main.scss +14 -0
- data/assets/js/dist/academic.js +1 -1
- data/assets/js/dist/analytics-dashboard.js +1 -1
- data/assets/js/dist/chunks/chunk-225H5YXE.js +1 -0
- data/assets/js/dist/core.js +1 -1
- data/assets/js/dist/loader.js +1 -1
- data/assets/js/dist/notebook.js +1 -1
- data/assets/js/dist/search.js +1 -1
- data/assets/js/dist/visualizations.js +11 -2
- data/assets/js/loader.js +3 -1
- data/datalog-theme.gemspec +36 -23
- data/lib/datalog/cli.rb +34 -14
- data/lib/datalog/plugin_system/dependency_resolver.rb +0 -2
- data/lib/datalog/theme/package.rb +57 -0
- data/lib/datalog/theme/repository_checkout.rb +90 -0
- data/lib/datalog/theme/version.rb +1 -1
- data/lib/datalog/warning_filter.rb +5 -11
- data/lib/datalog-theme.rb +5 -0
- metadata +48 -133
- data/_data/academic.yml +0 -217
- data/_data/config/author.yml +0 -121
- data/_data/datasets.yml +0 -28
- data/_data/js_meta.json +0 -371
- data/_data/navigation.yml +0 -145
- data/_data/projects.yml +0 -41
- data/_data/publications.yml +0 -28
- data/_data/social.yml +0 -73
- data/_data/visualizations.yml +0 -51
- data/_includes/components/advanced-search.html +0 -682
- data/_includes/components/bookmark-system.html +0 -96
- data/_includes/components/comments.html +0 -244
- data/_includes/components/content-recommendations.html +0 -228
- data/_includes/components/email-preferences.html +0 -200
- data/_includes/components/enhanced-metadata.html +0 -228
- data/_includes/components/language-switcher.html +0 -396
- data/_includes/components/navigation-enhancements.html +0 -454
- data/_includes/components/newsletter-signup.html +0 -178
- data/_includes/components/popular-posts.html +0 -233
- data/_includes/components/reading-progress.html +0 -133
- data/_includes/components/reading-time.html +0 -121
- data/_includes/components/series-navigation.html +0 -124
- data/_includes/components/social-proof.html +0 -34
- data/_includes/components/user-preferences.html +0 -566
- data/_includes/meta/syntax-config.html +0 -19
- data/_layouts/archive.html +0 -282
- data/_layouts/post-sidebar.html +0 -183
- data/_sass/_phase3-enhancements.scss +0 -874
- data/_sass/_phase4-enhancements.scss +0 -1214
- data/_sass/_phase5-enhancements.scss +0 -414
- data/assets/js/academic.js +0 -262
- data/assets/js/analytics-dashboard.js +0 -382
- data/assets/js/core/dark-mode.js +0 -79
- data/assets/js/core/github-cards.js +0 -123
- data/assets/js/core/language-filter.js +0 -69
- data/assets/js/core/navigation.js +0 -184
- data/assets/js/core/scroll-progress.js +0 -45
- data/assets/js/core/search-hotkeys.js +0 -62
- data/assets/js/core/skip-links.js +0 -62
- data/assets/js/dist/manifest.json +0 -22
- data/assets/js/dist/meta.json +0 -371
- data/assets/js/main.js +0 -23
- data/assets/js/math.js +0 -818
- data/assets/js/notebook.js +0 -158
- data/assets/js/search/analytics.js +0 -91
- data/assets/js/search/app.js +0 -271
- data/assets/js/search/autocomplete.js +0 -120
- data/assets/js/search/engine.js +0 -260
- data/assets/js/search/filters.js +0 -45
- data/assets/js/search/render.js +0 -217
- data/assets/js/search/utils.js +0 -99
- data/assets/js/search.js +0 -354
- data/assets/js/visualizations.js +0 -816
- data/assets/publications/datalog-publications.bib +0 -8
- data/assets/publications/datalog-publications.ris +0 -9
- data/assets/publications/publications.bib +0 -30
- data/assets/templates/diogo-ribeiro-cv.md +0 -31
- data/assets/templates/diogo-ribeiro-cv.tex +0 -32
- 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
|
-
}
|
data/assets/js/core/dark-mode.js
DELETED
|
@@ -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
|
-
}
|