jekyll-theme-zer0 1.2.0 → 1.3.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.
@@ -0,0 +1,382 @@
1
+ /*
2
+ * obsidian-local-graph.js
3
+ *
4
+ * Renders a small "local graph" (current page + immediate neighbors) into
5
+ * the element with id `obsidian-local-graph`. Mirrors Obsidian's local
6
+ * graph view: a focused, page-scoped subgraph instead of the full site map.
7
+ *
8
+ * Loaded by _includes/navigation/local-graph.html on every page that has
9
+ * a left sidebar. Cytoscape.js is loaded lazily (and only once) from the
10
+ * same CDN as the full graph page.
11
+ *
12
+ * Subgraph:
13
+ * - center = current page (matched against entry.url, falling back to
14
+ * normalized title/basename/aliases for permalink quirks)
15
+ * - depth = configurable via data-depth attribute (default 1)
16
+ * - direction = both incoming and outgoing wiki-links
17
+ *
18
+ * If the current page is not in the wiki-index (e.g. dynamically-built
19
+ * routes, the homepage with no edges, or excluded pages), the container
20
+ * is hidden and the script is a no-op.
21
+ */
22
+ (function () {
23
+ 'use strict';
24
+
25
+ var CONTAINER_ID = 'obsidian-local-graph';
26
+ var CYTOSCAPE_URL = 'https://cdn.jsdelivr.net/npm/cytoscape@3.30.0/dist/cytoscape.min.js';
27
+ var CYTOSCAPE_SRI = 'sha384-kpMsYllYzyaWU69Piok08rPNktpnjqAoDMdB00fjqUkEk3lkuUbSuwJ+oXrjvN6B';
28
+
29
+ function normalize(value) {
30
+ return String(value || '').toLowerCase().trim().replace(/\s+/g, ' ');
31
+ }
32
+
33
+ // Strip trailing slash and any leading site baseurl so we can match
34
+ // entry.url (which is always relative, with trailing slash).
35
+ function normalizePath(p) {
36
+ if (!p) return '';
37
+ try { p = decodeURIComponent(p); } catch (_) {}
38
+ p = p.split('#')[0].split('?')[0];
39
+ if (p.length > 1 && p.charAt(p.length - 1) !== '/') p += '/';
40
+ return p;
41
+ }
42
+
43
+ function buildLookup(entries) {
44
+ var byKey = Object.create(null);
45
+ var byUrl = Object.create(null);
46
+ entries.forEach(function (entry) {
47
+ if (!entry || !entry.url) return;
48
+ byUrl[normalizePath(entry.url)] = entry;
49
+ var keys = [];
50
+ if (entry.title) keys.push(entry.title);
51
+ if (entry.basename) keys.push(entry.basename);
52
+ (entry.aliases || []).forEach(function (a) { if (a) keys.push(a); });
53
+ keys.forEach(function (k) {
54
+ var nk = normalize(k);
55
+ if (!nk || byKey[nk]) return;
56
+ byKey[nk] = entry;
57
+ });
58
+ });
59
+ return { byKey: byKey, byUrl: byUrl };
60
+ }
61
+
62
+ function findCurrentEntry(lookup) {
63
+ var path = normalizePath(window.location.pathname);
64
+ if (lookup.byUrl[path]) return lookup.byUrl[path];
65
+ // Fallback: match by last path segment (handles baseurl mismatches).
66
+ var parts = path.split('/').filter(Boolean);
67
+ var last = parts[parts.length - 1];
68
+ if (last && lookup.byKey[normalize(last)]) {
69
+ return lookup.byKey[normalize(last)];
70
+ }
71
+ return null;
72
+ }
73
+
74
+ function collectionColor(name) {
75
+ var palette = {
76
+ posts: '#0d6efd', docs: '#198754', notes: '#6f42c1',
77
+ notebooks: '#d63384', quickstart: '#fd7e14', about: '#20c997',
78
+ hobbies: '#ffc107', news: '#6610f2', services: '#0dcaf0'
79
+ };
80
+ return palette[name] || '#6c757d';
81
+ }
82
+
83
+ // BFS from the current entry up to `depth` hops, following both
84
+ // outgoing edges and incoming edges (any other entry whose `outgoing`
85
+ // includes one of our keys).
86
+ function buildSubgraph(entries, lookup, current, depth) {
87
+ var visited = Object.create(null);
88
+ var queue = [{ entry: current, dist: 0 }];
89
+ var nodes = [];
90
+ var edges = [];
91
+ var seenEdge = Object.create(null);
92
+
93
+ // Pre-compute reverse adjacency so we can find incoming neighbors
94
+ // without scanning all entries each hop.
95
+ var reverse = Object.create(null);
96
+ entries.forEach(function (entry) {
97
+ (entry.outgoing || []).forEach(function (target) {
98
+ var nk = normalize(target);
99
+ if (!reverse[nk]) reverse[nk] = [];
100
+ reverse[nk].push(entry);
101
+ });
102
+ });
103
+
104
+ function keysFor(entry) {
105
+ var keys = [];
106
+ if (entry.title) keys.push(normalize(entry.title));
107
+ if (entry.basename) keys.push(normalize(entry.basename));
108
+ (entry.aliases || []).forEach(function (a) {
109
+ if (a) keys.push(normalize(a));
110
+ });
111
+ return keys;
112
+ }
113
+
114
+ function addEdge(srcId, tgtId, broken) {
115
+ var k = srcId + '|' + tgtId;
116
+ if (seenEdge[k]) return;
117
+ seenEdge[k] = true;
118
+ edges.push({
119
+ group: 'edges',
120
+ data: { id: 'le:' + k, source: srcId, target: tgtId, broken: !!broken }
121
+ });
122
+ }
123
+
124
+ while (queue.length) {
125
+ var item = queue.shift();
126
+ var entry = item.entry;
127
+ var nid = entry.url;
128
+ if (visited[nid]) continue;
129
+ visited[nid] = true;
130
+
131
+ nodes.push({
132
+ group: 'nodes',
133
+ data: {
134
+ id: nid,
135
+ label: entry.title || entry.basename || nid,
136
+ url: entry.url,
137
+ collection: entry.collection || 'page',
138
+ color: collectionColor(entry.collection),
139
+ isCurrent: entry.url === current.url
140
+ }
141
+ });
142
+
143
+ if (item.dist >= depth) continue;
144
+
145
+ // Outgoing edges
146
+ (entry.outgoing || []).forEach(function (target) {
147
+ var nk = normalize(target);
148
+ var resolved = lookup.byKey[nk];
149
+ if (resolved) {
150
+ if (resolved.url === entry.url) return;
151
+ addEdge(entry.url, resolved.url, false);
152
+ if (!visited[resolved.url]) {
153
+ queue.push({ entry: resolved, dist: item.dist + 1 });
154
+ }
155
+ } else {
156
+ var brokenId = '__broken__:' + nk;
157
+ if (!visited[brokenId]) {
158
+ visited[brokenId] = true;
159
+ nodes.push({
160
+ group: 'nodes',
161
+ data: {
162
+ id: brokenId,
163
+ label: target,
164
+ url: null,
165
+ collection: 'broken',
166
+ color: '#dc3545',
167
+ broken: true
168
+ }
169
+ });
170
+ }
171
+ addEdge(entry.url, brokenId, true);
172
+ }
173
+ });
174
+
175
+ // Incoming edges (anyone whose outgoing matches one of our keys)
176
+ keysFor(entry).forEach(function (k) {
177
+ (reverse[k] || []).forEach(function (src) {
178
+ if (src.url === entry.url) return;
179
+ addEdge(src.url, entry.url, false);
180
+ if (!visited[src.url]) {
181
+ queue.push({ entry: src, dist: item.dist + 1 });
182
+ }
183
+ });
184
+ });
185
+ }
186
+
187
+ return nodes.concat(edges);
188
+ }
189
+
190
+ function readTheme() {
191
+ var attr = (document.documentElement.getAttribute('data-bs-theme') ||
192
+ document.body.getAttribute('data-bs-theme') || '').toLowerCase();
193
+ var dark = attr === 'dark' || (!attr && window.matchMedia &&
194
+ window.matchMedia('(prefers-color-scheme: dark)').matches);
195
+ return dark ? {
196
+ label: '#e9ecef', labelOutline: '#1b1f23',
197
+ edge: 'rgba(173,181,189,0.45)', edgeArrow: 'rgba(173,181,189,0.65)',
198
+ canvasBg: '#1b1f23', nodeBorder: 'rgba(255,255,255,0.22)'
199
+ } : {
200
+ label: '#1b1f23', labelOutline: '#f8f9fa',
201
+ edge: 'rgba(73,80,87,0.40)', edgeArrow: 'rgba(73,80,87,0.60)',
202
+ canvasBg: '#f8f9fa', nodeBorder: 'rgba(0,0,0,0.20)'
203
+ };
204
+ }
205
+
206
+ function loadCytoscape(cb) {
207
+ if (typeof window.cytoscape === 'function') return cb();
208
+ // Re-use any in-flight load (e.g. when the full graph page also loads it).
209
+ if (window.__obsidianCytoscapeLoading) {
210
+ window.__obsidianCytoscapeLoading.push(cb);
211
+ return;
212
+ }
213
+ window.__obsidianCytoscapeLoading = [cb];
214
+ var existing = document.querySelector('script[src*="cytoscape"]');
215
+ if (existing) {
216
+ existing.addEventListener('load', function () {
217
+ window.__obsidianCytoscapeLoading.forEach(function (fn) { fn(); });
218
+ window.__obsidianCytoscapeLoading = null;
219
+ });
220
+ return;
221
+ }
222
+ var s = document.createElement('script');
223
+ s.src = CYTOSCAPE_URL;
224
+ s.integrity = CYTOSCAPE_SRI;
225
+ s.crossOrigin = 'anonymous';
226
+ s.defer = true;
227
+ s.onload = function () {
228
+ window.__obsidianCytoscapeLoading.forEach(function (fn) { fn(); });
229
+ window.__obsidianCytoscapeLoading = null;
230
+ };
231
+ s.onerror = function () {
232
+ console.warn('[obsidian-local-graph] failed to load cytoscape');
233
+ window.__obsidianCytoscapeLoading = null;
234
+ };
235
+ document.head.appendChild(s);
236
+ }
237
+
238
+ function render(container, elements, currentUrl) {
239
+ var theme = readTheme();
240
+ container.style.backgroundColor = theme.canvasBg;
241
+
242
+ var cy = window.cytoscape({
243
+ container: container,
244
+ elements: elements,
245
+ wheelSensitivity: 0.2,
246
+ minZoom: 0.3,
247
+ maxZoom: 3,
248
+ autoungrabify: false,
249
+ style: [
250
+ {
251
+ selector: 'node',
252
+ style: {
253
+ 'background-color': 'data(color)',
254
+ 'label': 'data(label)',
255
+ 'font-size': '10px',
256
+ 'font-weight': 500,
257
+ 'color': theme.label,
258
+ 'text-outline-color': theme.labelOutline,
259
+ 'text-outline-width': 2,
260
+ 'text-outline-opacity': 0.95,
261
+ 'text-background-opacity': 0,
262
+ 'text-valign': 'bottom',
263
+ 'text-margin-y': 4,
264
+ 'text-wrap': 'ellipsis',
265
+ 'text-max-width': '110px',
266
+ 'width': 14, 'height': 14,
267
+ 'border-width': 1.5,
268
+ 'border-color': theme.nodeBorder,
269
+ 'transition-property': 'background-color, border-color, width, height',
270
+ 'transition-duration': '160ms'
271
+ }
272
+ },
273
+ {
274
+ // Highlight the current page so users always know "you are here".
275
+ selector: 'node[?isCurrent]',
276
+ style: {
277
+ 'width': 22, 'height': 22,
278
+ 'border-width': 3,
279
+ 'border-color': '#fd7e14',
280
+ 'font-size': '11px',
281
+ 'font-weight': 700
282
+ }
283
+ },
284
+ {
285
+ selector: 'node[broken]',
286
+ style: {
287
+ 'background-color': '#dc3545',
288
+ 'border-style': 'dashed',
289
+ 'border-color': '#dc3545'
290
+ }
291
+ },
292
+ {
293
+ selector: 'edge',
294
+ style: {
295
+ 'width': 1,
296
+ 'line-color': theme.edge,
297
+ 'target-arrow-color': theme.edgeArrow,
298
+ 'target-arrow-shape': 'triangle',
299
+ 'arrow-scale': 0.7,
300
+ 'curve-style': 'bezier'
301
+ }
302
+ },
303
+ {
304
+ selector: 'edge[broken]',
305
+ style: { 'line-style': 'dashed', 'line-color': '#dc3545' }
306
+ }
307
+ ],
308
+ layout: {
309
+ name: 'cose',
310
+ animate: false,
311
+ padding: 14,
312
+ nodeRepulsion: function () { return 6000; },
313
+ idealEdgeLength: function () { return 60; },
314
+ edgeElasticity: function () { return 60; },
315
+ nodeOverlap: 12,
316
+ gravity: 0.3,
317
+ numIter: 1200,
318
+ fit: true
319
+ }
320
+ });
321
+
322
+ cy.on('tap', 'node', function (evt) {
323
+ var d = evt.target.data();
324
+ if (!d.url || d.broken) return;
325
+ if (d.url === currentUrl) return;
326
+ // ⌘/Ctrl-click opens in a new tab, mirroring the full graph page.
327
+ var oe = evt.originalEvent;
328
+ if (oe && (oe.metaKey || oe.ctrlKey)) {
329
+ window.open(d.url, '_blank', 'noopener');
330
+ } else {
331
+ window.location.href = d.url;
332
+ }
333
+ });
334
+
335
+ cy.on('mouseover', 'node', function (evt) {
336
+ evt.target.style('z-index', 99);
337
+ });
338
+
339
+ return cy;
340
+ }
341
+
342
+ function init() {
343
+ var container = document.getElementById(CONTAINER_ID);
344
+ if (!container) return;
345
+ var depth = parseInt(container.getAttribute('data-depth') || '1', 10);
346
+ if (!isFinite(depth) || depth < 1) depth = 1;
347
+ var indexUrl = container.getAttribute('data-index-url') ||
348
+ ((document.querySelector('base') || {}).href || '/') +
349
+ 'assets/data/wiki-index.json';
350
+
351
+ fetch(indexUrl, { credentials: 'same-origin' })
352
+ .then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
353
+ .then(function (data) {
354
+ var entries = Array.isArray(data && data.entries) ? data.entries : [];
355
+ if (!entries.length) { container.style.display = 'none'; return; }
356
+ var lookup = buildLookup(entries);
357
+ var current = findCurrentEntry(lookup);
358
+ if (!current) { container.style.display = 'none'; return; }
359
+ var elements = buildSubgraph(entries, lookup, current, depth);
360
+ // If the current page has no neighbors, show a tiny "no links yet"
361
+ // hint instead of a single dot.
362
+ if (elements.length <= 1) {
363
+ container.style.display = 'none';
364
+ return;
365
+ }
366
+ loadCytoscape(function () {
367
+ render(container, elements, current.url);
368
+ });
369
+ })
370
+ .catch(function (err) {
371
+ // Sidebar widget failing is non-fatal — hide and stay quiet.
372
+ console.warn('[obsidian-local-graph] init failed:', err);
373
+ container.style.display = 'none';
374
+ });
375
+ }
376
+
377
+ if (document.readyState === 'loading') {
378
+ document.addEventListener('DOMContentLoaded', init);
379
+ } else {
380
+ init();
381
+ }
382
+ })();