jekyll-theme-zer0 1.26.0 → 1.28.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 +558 -5
- data/README.md +10 -27
- data/_data/authors.yml +4 -3
- data/_data/backlog.yml +28 -0
- data/_data/consumers.yml +147 -0
- data/_data/features.yml +142 -23
- data/_data/i18n/fr.yml +98 -0
- data/_data/i18n/languages.yml +36 -0
- data/_data/i18n/manifest.yml +1502 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +6 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +348 -268
- data/_data/ui-text.yml +36 -246
- data/_includes/README.md +25 -2
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/author-avatar-url.html +4 -2
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/env-switcher.html +3 -1
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/language-toggle.html +81 -0
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/search-modal.html +2 -2
- data/_includes/components/shortcuts-modal.html +1 -1
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/components/translation-notice.html +27 -0
- data/_includes/content/intro.html +16 -15
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/footer.html +9 -4
- data/_includes/core/head.html +18 -0
- data/_includes/core/header.html +9 -4
- data/_includes/core/hreflang.html +33 -0
- data/_includes/core/i18n.html +36 -0
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/breadcrumbs.html +1 -1
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/navbar.html +5 -4
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/sidebar-right.html +3 -2
- data/_includes/navigation/unified-drawer.html +9 -3
- data/_includes/obsidian/full-graph.html +165 -136
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +2 -0
- data/_layouts/article.html +7 -1
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/default.html +5 -3
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +159 -33
- data/_layouts/root.html +29 -9
- data/_layouts/section.html +61 -17
- data/_sass/components/_book.scss +423 -0
- data/_sass/core/_navbar.scss +13 -0
- data/_sass/core/_obsidian.scss +295 -7
- data/_sass/layouts/_navbar-extras.scss +6 -1
- data/_sass/theme/_backgrounds.scss +21 -8
- data/assets/css/main.scss +1 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/obsidian-graph.js +707 -265
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/search-modal.js +4 -1
- data/scripts/README.md +20 -26
- data/scripts/bin/audit-consumer +1 -1
- data/scripts/bin/manifest +31 -5
- data/scripts/bin/sync-plugins +0 -1
- data/scripts/bin/validate +5 -1
- data/scripts/dev/rasterize-svg.js +65 -0
- data/scripts/features/generate-preview-images +49 -1390
- data/scripts/features/install-preview-generator +55 -33
- data/scripts/install/README.md +55 -25
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/prompts/wizard.system.md +8 -17
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/lib/README.md +1 -5
- data/scripts/lib/install/deploy/README.md +3 -9
- data/scripts/lib/preview_generator.py +2261 -1341
- data/scripts/propagate.rb +277 -0
- data/scripts/translate.rb +1204 -0
- metadata +34 -3
- data/_plugins/preview_image_generator.rb +0 -351
data/assets/js/obsidian-graph.js
CHANGED
|
@@ -19,18 +19,86 @@
|
|
|
19
19
|
* targets become floating "broken" nodes drawn in red so the graph also
|
|
20
20
|
* surfaces dangling links.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* Mirrors the Obsidian desktop graph controls: Filters (search with tag:/
|
|
23
|
+
* path: operators, per-collection toggles, orphans, unresolved), Display
|
|
24
|
+
* (arrows, node size, link thickness, label fade threshold) and Forces
|
|
25
|
+
* (repulsion, link distance, animated re-layout). Settings persist to
|
|
26
|
+
* localStorage. The stylesheet re-derives itself when the Bootstrap
|
|
27
|
+
* color mode (data-bs-theme) changes — no reload needed.
|
|
28
|
+
*
|
|
29
|
+
* Public hooks (read-only, used by test/visual/features/obsidian-graph.spec.js):
|
|
23
30
|
* window.ObsidianGraph.cy — cytoscape instance once initialized
|
|
24
31
|
* window.ObsidianGraph.byKey — normalized lookup table
|
|
25
32
|
* window.ObsidianGraph.entries — raw entries array
|
|
33
|
+
* window.ObsidianGraph.settings — live settings object
|
|
26
34
|
*/
|
|
27
35
|
(function () {
|
|
28
36
|
'use strict';
|
|
29
37
|
|
|
30
38
|
var CONTAINER_ID = 'obsidian-graph';
|
|
31
|
-
var
|
|
39
|
+
var STORAGE_KEY = 'zer0.obsidianGraph.v1';
|
|
40
|
+
// Index URL resolution mirrors obsidian-wiki-links.js: the baseurl-aware
|
|
41
|
+
// value js-cdn.html emits (OBSIDIAN_CONFIG.wikiIndexUrl), then the legacy
|
|
42
|
+
// per-page override global, then a <base>-relative fallback.
|
|
43
|
+
var INDEX_URL = ((window.OBSIDIAN_CONFIG || {}).wikiIndexUrl ||
|
|
44
|
+
window.OBSIDIAN_WIKI_INDEX_URL ||
|
|
32
45
|
((document.querySelector('base') || {}).href || '/') + 'assets/data/wiki-index.json');
|
|
33
46
|
|
|
47
|
+
var HUB_DEGREE_THRESHOLD = 12;
|
|
48
|
+
|
|
49
|
+
// User-tunable view state. Multipliers are relative to the base style so
|
|
50
|
+
// "1" always means the stock look. Persisted per-browser via localStorage.
|
|
51
|
+
var DEFAULTS = {
|
|
52
|
+
collections: {}, // name -> false when hidden; missing = visible
|
|
53
|
+
showOrphans: false, // matches Obsidian's "Show orphans" off state
|
|
54
|
+
showBroken: true, // unresolved [[targets]] as dashed red nodes
|
|
55
|
+
showArrows: true,
|
|
56
|
+
nodeScale: 1, // 0.5 – 2
|
|
57
|
+
linkScale: 1, // 0.5 – 3
|
|
58
|
+
labelZoom: 1.25, // zoom level past which labels appear (0.4 – 2.5)
|
|
59
|
+
repulsion: 1, // 0.3 – 3 × base node repulsion
|
|
60
|
+
linkDistance: 1, // 0.4 – 2.5 × base ideal edge length
|
|
61
|
+
animateLayout: true // ignored when prefers-reduced-motion
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function clamp(value, min, max, fallback) {
|
|
65
|
+
var n = parseFloat(value);
|
|
66
|
+
if (!isFinite(n)) return fallback;
|
|
67
|
+
return Math.min(max, Math.max(min, n));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadSettings() {
|
|
71
|
+
var merged = JSON.parse(JSON.stringify(DEFAULTS));
|
|
72
|
+
try {
|
|
73
|
+
var raw = window.localStorage.getItem(STORAGE_KEY);
|
|
74
|
+
if (!raw) return merged;
|
|
75
|
+
var saved = JSON.parse(raw);
|
|
76
|
+
if (!saved || typeof saved !== 'object') return merged;
|
|
77
|
+
if (saved.collections && typeof saved.collections === 'object') {
|
|
78
|
+
merged.collections = saved.collections;
|
|
79
|
+
}
|
|
80
|
+
['showOrphans', 'showBroken', 'showArrows', 'animateLayout'].forEach(function (k) {
|
|
81
|
+
if (typeof saved[k] === 'boolean') merged[k] = saved[k];
|
|
82
|
+
});
|
|
83
|
+
merged.nodeScale = clamp(saved.nodeScale, 0.5, 2, merged.nodeScale);
|
|
84
|
+
merged.linkScale = clamp(saved.linkScale, 0.5, 3, merged.linkScale);
|
|
85
|
+
merged.labelZoom = clamp(saved.labelZoom, 0.4, 2.5, merged.labelZoom);
|
|
86
|
+
merged.repulsion = clamp(saved.repulsion, 0.3, 3, merged.repulsion);
|
|
87
|
+
merged.linkDistance = clamp(saved.linkDistance, 0.4, 2.5, merged.linkDistance);
|
|
88
|
+
} catch (e) { /* private mode / corrupt payload — fall back to defaults */ }
|
|
89
|
+
return merged;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var saveTimer = null;
|
|
93
|
+
function saveSettings(settings) {
|
|
94
|
+
clearTimeout(saveTimer);
|
|
95
|
+
saveTimer = setTimeout(function () {
|
|
96
|
+
try {
|
|
97
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
98
|
+
} catch (e) { /* storage unavailable — view state is session-only */ }
|
|
99
|
+
}, 150);
|
|
100
|
+
}
|
|
101
|
+
|
|
34
102
|
function normalize(value) {
|
|
35
103
|
return String(value || '').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
36
104
|
}
|
|
@@ -39,6 +107,22 @@
|
|
|
39
107
|
return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
40
108
|
}
|
|
41
109
|
|
|
110
|
+
function debounce(fn, wait) {
|
|
111
|
+
var t;
|
|
112
|
+
return function () {
|
|
113
|
+
var ctx = this;
|
|
114
|
+
var args = arguments;
|
|
115
|
+
clearTimeout(t);
|
|
116
|
+
t = setTimeout(function () { fn.apply(ctx, args); }, wait);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function escapeHtml(value) {
|
|
121
|
+
return String(value == null ? '' : value)
|
|
122
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
123
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
124
|
+
}
|
|
125
|
+
|
|
42
126
|
// Cytoscape is vendored under assets/vendor/ (no runtime CDN). Path comes from
|
|
43
127
|
// window.OBSIDIAN_CONFIG.cytoscapeUrl (set by Liquid), with a base fallback.
|
|
44
128
|
function cytoscapeSrc() {
|
|
@@ -95,27 +179,7 @@
|
|
|
95
179
|
}
|
|
96
180
|
|
|
97
181
|
// Cytoscape needs concrete color values (not CSS vars). readTheme()
|
|
98
|
-
// resolves light/dark
|
|
99
|
-
// Shared cose layout — looser packing, more edge length, wider padding.
|
|
100
|
-
var COSE_LAYOUT = {
|
|
101
|
-
name: 'cose',
|
|
102
|
-
animate: false,
|
|
103
|
-
randomize: true,
|
|
104
|
-
nodeRepulsion: function () { return 32000; },
|
|
105
|
-
idealEdgeLength: function () { return 190; },
|
|
106
|
-
edgeElasticity: function () { return 60; },
|
|
107
|
-
nodeOverlap: 40,
|
|
108
|
-
gravity: 0.1,
|
|
109
|
-
nestingFactor: 1.5,
|
|
110
|
-
numIter: 3000,
|
|
111
|
-
padding: 80,
|
|
112
|
-
componentSpacing: 140
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Labels appear when zoomed past this level or on hover/highlight.
|
|
116
|
-
var LABEL_ZOOM_THRESHOLD = 1.25;
|
|
117
|
-
var HUB_DEGREE_THRESHOLD = 12;
|
|
118
|
-
|
|
182
|
+
// resolves light/dark from the live Bootstrap color mode.
|
|
119
183
|
function readTheme() {
|
|
120
184
|
var attr = (document.documentElement.getAttribute('data-bs-theme') ||
|
|
121
185
|
document.body.getAttribute('data-bs-theme') || '').toLowerCase();
|
|
@@ -126,56 +190,63 @@
|
|
|
126
190
|
labelOutline: 'rgba(13,17,23,0.88)',
|
|
127
191
|
edge: 'rgba(173,181,189,0.28)',
|
|
128
192
|
edgeArrow: 'rgba(173,181,189,0.42)',
|
|
129
|
-
canvasBg: 'transparent',
|
|
130
193
|
nodeBorder: 'rgba(255,255,255,0.18)'
|
|
131
194
|
} : {
|
|
132
195
|
label: 'rgba(27,31,35,0.92)',
|
|
133
196
|
labelOutline: 'rgba(248,249,250,0.90)',
|
|
134
197
|
edge: 'rgba(73,80,87,0.22)',
|
|
135
198
|
edgeArrow: 'rgba(73,80,87,0.38)',
|
|
136
|
-
canvasBg: 'transparent',
|
|
137
199
|
nodeBorder: 'rgba(0,0,0,0.14)'
|
|
138
200
|
};
|
|
139
201
|
}
|
|
140
202
|
|
|
203
|
+
var COLLECTION_PALETTE = {
|
|
204
|
+
posts: [13, 110, 253],
|
|
205
|
+
docs: [25, 135, 84],
|
|
206
|
+
notes: [111, 66, 193],
|
|
207
|
+
notebooks: [214, 51, 132],
|
|
208
|
+
quickstart: [253, 126, 20],
|
|
209
|
+
about: [32, 201, 151],
|
|
210
|
+
hobbies: [255, 193, 7],
|
|
211
|
+
news: [102, 16, 242],
|
|
212
|
+
services: [13, 202, 240],
|
|
213
|
+
books: [220, 53, 133]
|
|
214
|
+
};
|
|
215
|
+
|
|
141
216
|
function collectionColor(name, alpha) {
|
|
142
217
|
alpha = alpha == null ? 0.72 : alpha;
|
|
143
|
-
|
|
144
|
-
var palette = {
|
|
145
|
-
posts: [13, 110, 253],
|
|
146
|
-
docs: [25, 135, 84],
|
|
147
|
-
notes: [111, 66, 193],
|
|
148
|
-
notebooks: [214, 51, 132],
|
|
149
|
-
quickstart: [253, 126, 20],
|
|
150
|
-
about: [32, 201, 151],
|
|
151
|
-
hobbies: [255, 193, 7],
|
|
152
|
-
news: [102, 16, 242],
|
|
153
|
-
services: [13, 202, 240]
|
|
154
|
-
};
|
|
155
|
-
var rgb = palette[name] || [108, 117, 125];
|
|
218
|
+
var rgb = COLLECTION_PALETTE[name] || [108, 117, 125];
|
|
156
219
|
return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + alpha + ')';
|
|
157
220
|
}
|
|
158
221
|
|
|
222
|
+
function baseNodeSize(degree) {
|
|
223
|
+
// Same mapping the old mapData(degree, 0, 25, 8, 34) produced, computed in
|
|
224
|
+
// data so the Display "node size" multiplier can rescale it live.
|
|
225
|
+
var d = Math.min(Math.max(degree || 0, 0), 25);
|
|
226
|
+
return 8 + (d / 25) * 26;
|
|
227
|
+
}
|
|
228
|
+
|
|
159
229
|
function buildElements(entries, byKey) {
|
|
160
230
|
var elements = [];
|
|
161
|
-
var seenTargets = Object.create(null);
|
|
162
231
|
var brokenIds = Object.create(null);
|
|
163
232
|
|
|
164
233
|
entries.forEach(function (entry) {
|
|
165
|
-
var nid = entry.url;
|
|
166
234
|
elements.push({
|
|
167
235
|
group: 'nodes',
|
|
168
236
|
data: {
|
|
169
|
-
id:
|
|
170
|
-
label: entry.title || entry.basename ||
|
|
237
|
+
id: entry.url,
|
|
238
|
+
label: entry.title || entry.basename || entry.url,
|
|
171
239
|
url: entry.url,
|
|
172
240
|
collection: entry.collection || 'page',
|
|
173
241
|
color: collectionColor(entry.collection),
|
|
174
242
|
excerpt: entry.excerpt || '',
|
|
243
|
+
// Search-operator fodder (tag:/path: + alias matches). Client-side
|
|
244
|
+
// only — the wiki-index.json contract is unchanged.
|
|
245
|
+
tags: (entry.tags || []).map(normalize),
|
|
246
|
+
aliasText: (entry.aliases || []).map(normalize).join(' | '),
|
|
175
247
|
broken: false
|
|
176
248
|
}
|
|
177
249
|
});
|
|
178
|
-
seenTargets[nid] = true;
|
|
179
250
|
});
|
|
180
251
|
|
|
181
252
|
entries.forEach(function (entry) {
|
|
@@ -199,6 +270,8 @@
|
|
|
199
270
|
collection: 'broken',
|
|
200
271
|
color: 'rgba(220,53,69,0.78)',
|
|
201
272
|
excerpt: 'Unresolved wiki-link',
|
|
273
|
+
tags: [],
|
|
274
|
+
aliasText: '',
|
|
202
275
|
broken: true
|
|
203
276
|
}
|
|
204
277
|
});
|
|
@@ -229,261 +302,620 @@
|
|
|
229
302
|
elements.forEach(function (el) {
|
|
230
303
|
if (el.group !== 'nodes') return;
|
|
231
304
|
el.data.degree = degree[el.data.id] || 0;
|
|
305
|
+
el.data.size = baseNodeSize(el.data.degree);
|
|
232
306
|
});
|
|
233
307
|
}
|
|
234
308
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Graph application — wraps one cytoscape instance plus the settings-driven
|
|
311
|
+
// style/filter/layout plumbing shared by every control.
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
function GraphApp(container, entries, byKey) {
|
|
314
|
+
this.container = container;
|
|
315
|
+
this.entries = entries;
|
|
316
|
+
this.byKey = byKey;
|
|
317
|
+
this.settings = loadSettings();
|
|
318
|
+
this.cy = null;
|
|
319
|
+
this.searchQuery = '';
|
|
243
320
|
}
|
|
244
321
|
|
|
245
|
-
function
|
|
246
|
-
if (typeof window.cytoscape !== 'function') {
|
|
247
|
-
container.innerHTML =
|
|
248
|
-
'<div class="alert alert-danger" role="alert">' +
|
|
249
|
-
'Graph view failed to load: <code>cytoscape</code> library is not ' +
|
|
250
|
-
'available. Check your network connection or content security policy.' +
|
|
251
|
-
'</div>';
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
322
|
+
GraphApp.prototype.buildStyle = function () {
|
|
255
323
|
var theme = readTheme();
|
|
256
|
-
|
|
257
|
-
var
|
|
258
|
-
|
|
324
|
+
var s = this.settings;
|
|
325
|
+
var motion = prefersReducedMotion() ? '0ms' : '160ms';
|
|
326
|
+
return [
|
|
327
|
+
{
|
|
328
|
+
selector: 'node',
|
|
329
|
+
style: {
|
|
330
|
+
'background-color': 'data(color)',
|
|
331
|
+
'background-opacity': 0.82,
|
|
332
|
+
'label': 'data(label)',
|
|
333
|
+
'font-size': '10px',
|
|
334
|
+
'font-weight': 500,
|
|
335
|
+
'color': theme.label,
|
|
336
|
+
'text-outline-color': theme.labelOutline,
|
|
337
|
+
'text-outline-width': 3,
|
|
338
|
+
'text-outline-opacity': 1,
|
|
339
|
+
'text-background-opacity': 0,
|
|
340
|
+
'text-border-opacity': 0,
|
|
341
|
+
'text-valign': 'bottom',
|
|
342
|
+
'text-margin-y': 6,
|
|
343
|
+
'text-wrap': 'ellipsis',
|
|
344
|
+
'text-max-width': '120px',
|
|
345
|
+
// Hidden until zoomed-in, hovered, or a high-degree hub.
|
|
346
|
+
'min-zoomed-font-size': 11,
|
|
347
|
+
'text-opacity': 0,
|
|
348
|
+
'width': 'data(size)',
|
|
349
|
+
'height': 'data(size)',
|
|
350
|
+
'border-width': 1,
|
|
351
|
+
'border-color': theme.nodeBorder,
|
|
352
|
+
'opacity': 0.88,
|
|
353
|
+
'transition-property': 'background-color, border-color, width, height, text-opacity, opacity',
|
|
354
|
+
'transition-duration': motion
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
// Hub landmarks stay labeled when zoomed out.
|
|
359
|
+
selector: 'node[degree >= ' + HUB_DEGREE_THRESHOLD + ']',
|
|
360
|
+
style: {
|
|
361
|
+
'text-opacity': 0.88,
|
|
362
|
+
'font-size': '11px',
|
|
363
|
+
'font-weight': 600,
|
|
364
|
+
'min-zoomed-font-size': 0
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
selector: 'node[?broken]',
|
|
369
|
+
style: {
|
|
370
|
+
'border-style': 'dashed',
|
|
371
|
+
'border-color': 'rgba(220,53,69,0.75)',
|
|
372
|
+
'border-width': 1.5,
|
|
373
|
+
'background-opacity': 0.55,
|
|
374
|
+
'opacity': 0.78
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
selector: 'edge',
|
|
379
|
+
style: {
|
|
380
|
+
'curve-style': 'bezier',
|
|
381
|
+
'width': s.linkScale,
|
|
382
|
+
'line-color': theme.edge,
|
|
383
|
+
'target-arrow-color': theme.edgeArrow,
|
|
384
|
+
'target-arrow-shape': s.showArrows ? 'triangle' : 'none',
|
|
385
|
+
'arrow-scale': 0.75,
|
|
386
|
+
'opacity': 0.55,
|
|
387
|
+
'transition-property': 'line-color, width, opacity',
|
|
388
|
+
'transition-duration': motion
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
selector: 'edge[?broken]',
|
|
393
|
+
style: {
|
|
394
|
+
'line-color': 'rgba(220,53,69,0.32)',
|
|
395
|
+
'target-arrow-color': 'rgba(220,53,69,0.42)',
|
|
396
|
+
'line-style': 'dashed',
|
|
397
|
+
'opacity': 0.45
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
selector: '.faded',
|
|
402
|
+
style: {
|
|
403
|
+
'opacity': 0.08,
|
|
404
|
+
'text-opacity': 0
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
selector: 'node.highlighted',
|
|
409
|
+
style: {
|
|
410
|
+
'border-color': 'rgba(253,126,20,0.95)',
|
|
411
|
+
'border-width': 2.5,
|
|
412
|
+
'opacity': 1,
|
|
413
|
+
'background-opacity': 0.95,
|
|
414
|
+
'text-opacity': 1,
|
|
415
|
+
'font-size': '11px',
|
|
416
|
+
'font-weight': 700,
|
|
417
|
+
'min-zoomed-font-size': 0,
|
|
418
|
+
'z-index': 9999
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
selector: 'edge.highlighted',
|
|
423
|
+
style: {
|
|
424
|
+
'line-color': 'rgba(253,126,20,0.85)',
|
|
425
|
+
'target-arrow-color': 'rgba(253,126,20,0.85)',
|
|
426
|
+
'width': Math.max(2, 2 * this.settings.linkScale),
|
|
427
|
+
'opacity': 0.95,
|
|
428
|
+
'z-index': 9999
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
// Filtered out by the Filters panel. display:none also hides the
|
|
433
|
+
// connected edges, so the visible subgraph stays consistent.
|
|
434
|
+
selector: '.obsidian-hidden',
|
|
435
|
+
style: { 'display': 'none' }
|
|
436
|
+
}
|
|
437
|
+
];
|
|
438
|
+
};
|
|
259
439
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
440
|
+
GraphApp.prototype.render = function () {
|
|
441
|
+
var app = this;
|
|
442
|
+
this.cy = window.cytoscape({
|
|
443
|
+
container: this.container,
|
|
444
|
+
elements: this.elements,
|
|
263
445
|
minZoom: 0.1,
|
|
264
446
|
maxZoom: 4,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
style: {
|
|
269
|
-
'background-color': 'data(color)',
|
|
270
|
-
'background-opacity': 0.82,
|
|
271
|
-
'label': 'data(label)',
|
|
272
|
-
'font-size': '10px',
|
|
273
|
-
'font-weight': 500,
|
|
274
|
-
'color': theme.label,
|
|
275
|
-
'text-outline-color': theme.labelOutline,
|
|
276
|
-
'text-outline-width': 3,
|
|
277
|
-
'text-outline-opacity': 1,
|
|
278
|
-
'text-background-opacity': 0,
|
|
279
|
-
'text-border-opacity': 0,
|
|
280
|
-
'text-valign': 'bottom',
|
|
281
|
-
'text-margin-y': 6,
|
|
282
|
-
'text-wrap': 'ellipsis',
|
|
283
|
-
'text-max-width': '120px',
|
|
284
|
-
// Hidden until zoomed-in, hovered, or a high-degree hub.
|
|
285
|
-
'min-zoomed-font-size': 11,
|
|
286
|
-
'text-opacity': 0,
|
|
287
|
-
'width': 'mapData(degree, 0, 25, 8, 34)',
|
|
288
|
-
'height': 'mapData(degree, 0, 25, 8, 34)',
|
|
289
|
-
'border-width': 1,
|
|
290
|
-
'border-color': theme.nodeBorder,
|
|
291
|
-
'opacity': 0.88,
|
|
292
|
-
'transition-property': 'background-color, border-color, width, height, text-opacity, opacity',
|
|
293
|
-
'transition-duration': nodeDur
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
// Hub landmarks stay labeled when zoomed out.
|
|
298
|
-
selector: 'node[degree >= ' + HUB_DEGREE_THRESHOLD + ']',
|
|
299
|
-
style: {
|
|
300
|
-
'text-opacity': 0.88,
|
|
301
|
-
'font-size': '11px',
|
|
302
|
-
'font-weight': 600,
|
|
303
|
-
'min-zoomed-font-size': 0
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
selector: 'node[?broken]',
|
|
308
|
-
style: {
|
|
309
|
-
'border-style': 'dashed',
|
|
310
|
-
'border-color': 'rgba(220,53,69,0.75)',
|
|
311
|
-
'border-width': 1.5,
|
|
312
|
-
'background-opacity': 0.55,
|
|
313
|
-
'opacity': 0.78
|
|
314
|
-
}
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
selector: 'edge',
|
|
318
|
-
style: {
|
|
319
|
-
'curve-style': 'bezier',
|
|
320
|
-
'width': 1,
|
|
321
|
-
'line-color': theme.edge,
|
|
322
|
-
'target-arrow-color': theme.edgeArrow,
|
|
323
|
-
'target-arrow-shape': 'triangle',
|
|
324
|
-
'arrow-scale': 0.75,
|
|
325
|
-
'opacity': 0.55,
|
|
326
|
-
'transition-property': 'line-color, width, opacity',
|
|
327
|
-
'transition-duration': edgeDur
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
selector: 'edge[?broken]',
|
|
332
|
-
style: {
|
|
333
|
-
'line-color': 'rgba(220,53,69,0.32)',
|
|
334
|
-
'target-arrow-color': 'rgba(220,53,69,0.42)',
|
|
335
|
-
'line-style': 'dashed',
|
|
336
|
-
'opacity': 0.45
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
selector: '.faded',
|
|
341
|
-
style: {
|
|
342
|
-
'opacity': 0.08,
|
|
343
|
-
'text-opacity': 0
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
selector: 'node.highlighted',
|
|
348
|
-
style: {
|
|
349
|
-
'border-color': 'rgba(253,126,20,0.95)',
|
|
350
|
-
'border-width': 2.5,
|
|
351
|
-
'opacity': 1,
|
|
352
|
-
'background-opacity': 0.95,
|
|
353
|
-
'text-opacity': 1,
|
|
354
|
-
'font-size': '11px',
|
|
355
|
-
'font-weight': 700,
|
|
356
|
-
'min-zoomed-font-size': 0,
|
|
357
|
-
'z-index': 9999
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
selector: 'edge.highlighted',
|
|
362
|
-
style: {
|
|
363
|
-
'line-color': 'rgba(253,126,20,0.85)',
|
|
364
|
-
'target-arrow-color': 'rgba(253,126,20,0.85)',
|
|
365
|
-
'width': 2,
|
|
366
|
-
'opacity': 0.95,
|
|
367
|
-
'z-index': 9999
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
],
|
|
371
|
-
layout: COSE_LAYOUT
|
|
447
|
+
wheelSensitivity: 0.3,
|
|
448
|
+
style: this.buildStyle(),
|
|
449
|
+
layout: { name: 'preset' } // real layout runs below, honouring settings
|
|
372
450
|
});
|
|
373
451
|
|
|
374
|
-
cy.on('tap', 'node', function (evt) {
|
|
375
|
-
var
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
window.open(url, '_blank', 'noopener');
|
|
383
|
-
} else {
|
|
384
|
-
window.location.href = url;
|
|
385
|
-
}
|
|
452
|
+
this.cy.on('tap', 'node', function (evt) {
|
|
453
|
+
var url = evt.target.data('url');
|
|
454
|
+
if (!url) return;
|
|
455
|
+
var native = evt.originalEvent;
|
|
456
|
+
if (native && (native.metaKey || native.ctrlKey)) {
|
|
457
|
+
window.open(url, '_blank', 'noopener');
|
|
458
|
+
} else {
|
|
459
|
+
window.location.href = url;
|
|
386
460
|
}
|
|
387
461
|
});
|
|
388
462
|
|
|
389
|
-
// Hover: highlight neighborhood, fade everything else.
|
|
390
|
-
cy.on('mouseover', 'node', function (evt) {
|
|
463
|
+
// Hover: highlight neighborhood, fade everything else, show the preview.
|
|
464
|
+
this.cy.on('mouseover', 'node', function (evt) {
|
|
391
465
|
var node = evt.target;
|
|
392
466
|
var nhood = node.closedNeighborhood();
|
|
393
|
-
cy.elements().not(nhood).addClass('faded');
|
|
467
|
+
app.cy.elements().not(nhood).addClass('faded');
|
|
394
468
|
nhood.addClass('highlighted');
|
|
469
|
+
app.showPreview(node);
|
|
395
470
|
});
|
|
396
|
-
cy.on('mouseout', 'node', function () {
|
|
397
|
-
cy.elements().removeClass('faded highlighted');
|
|
398
|
-
|
|
471
|
+
this.cy.on('mouseout', 'node', function () {
|
|
472
|
+
app.cy.elements().removeClass('faded highlighted');
|
|
473
|
+
app.hidePreview();
|
|
474
|
+
app.updateLabelVisibility();
|
|
475
|
+
app.applySearch(); // restore an active search highlight after hover
|
|
399
476
|
});
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
updateLabelVisibility(
|
|
477
|
+
this.cy.on('zoom pan', function () {
|
|
478
|
+
app.hidePreview();
|
|
479
|
+
app.updateLabelVisibility();
|
|
403
480
|
});
|
|
481
|
+
this.cy.on('grab', 'node', function () { app.hidePreview(); });
|
|
482
|
+
|
|
483
|
+
this.applyFilters(false);
|
|
484
|
+
this.runLayout(true);
|
|
485
|
+
this.updateLabelVisibility();
|
|
486
|
+
};
|
|
404
487
|
|
|
405
|
-
|
|
488
|
+
GraphApp.prototype.visibleElements = function () {
|
|
489
|
+
return this.cy.elements().not('.obsidian-hidden');
|
|
490
|
+
};
|
|
406
491
|
|
|
407
|
-
|
|
408
|
-
|
|
492
|
+
GraphApp.prototype.runLayout = function (randomize) {
|
|
493
|
+
if (!this.cy) return;
|
|
494
|
+
var s = this.settings;
|
|
495
|
+
if (this.activeLayout) { try { this.activeLayout.stop(); } catch (e) { /* ignore */ } }
|
|
496
|
+
this.activeLayout = this.cy.layout({
|
|
497
|
+
name: 'cose',
|
|
498
|
+
animate: s.animateLayout && !prefersReducedMotion(),
|
|
499
|
+
randomize: !!randomize,
|
|
500
|
+
nodeRepulsion: function () { return 32000 * s.repulsion; },
|
|
501
|
+
idealEdgeLength: function () { return 190 * s.linkDistance; },
|
|
502
|
+
edgeElasticity: function () { return 60; },
|
|
503
|
+
nodeOverlap: 40,
|
|
504
|
+
gravity: 0.1,
|
|
505
|
+
nestingFactor: 1.5,
|
|
506
|
+
numIter: 3000,
|
|
507
|
+
padding: 80,
|
|
508
|
+
componentSpacing: 140,
|
|
509
|
+
fit: true
|
|
510
|
+
});
|
|
511
|
+
this.activeLayout.run();
|
|
512
|
+
};
|
|
409
513
|
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
var
|
|
413
|
-
|
|
514
|
+
GraphApp.prototype.applyDisplay = function () {
|
|
515
|
+
if (!this.cy) return;
|
|
516
|
+
var s = this.settings;
|
|
517
|
+
this.cy.style().fromJson(this.buildStyle()).update();
|
|
518
|
+
var app = this;
|
|
519
|
+
this.cy.batch(function () {
|
|
520
|
+
app.cy.nodes().forEach(function (node) {
|
|
521
|
+
node.data('size', baseNodeSize(node.data('degree')) * s.nodeScale);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
this.updateLabelVisibility();
|
|
525
|
+
saveSettings(s);
|
|
526
|
+
};
|
|
414
527
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
528
|
+
GraphApp.prototype.applyFilters = function (refit) {
|
|
529
|
+
if (!this.cy) return;
|
|
530
|
+
var s = this.settings;
|
|
531
|
+
var app = this;
|
|
532
|
+
this.cy.batch(function () {
|
|
533
|
+
app.cy.nodes().forEach(function (node) {
|
|
534
|
+
var d = node.data();
|
|
535
|
+
var hide = false;
|
|
536
|
+
if (d.broken) {
|
|
537
|
+
hide = !s.showBroken;
|
|
538
|
+
} else if (s.collections[d.collection || 'page'] === false) {
|
|
539
|
+
hide = true;
|
|
540
|
+
}
|
|
541
|
+
if (!hide && !s.showOrphans && (d.degree || 0) === 0) hide = true;
|
|
542
|
+
node.toggleClass('obsidian-hidden', hide);
|
|
425
543
|
});
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
544
|
+
});
|
|
545
|
+
this.updateStats();
|
|
546
|
+
if (refit !== false) this.cy.fit(this.visibleElements(), 80);
|
|
547
|
+
saveSettings(s);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
GraphApp.prototype.updateLabelVisibility = function () {
|
|
551
|
+
if (!this.cy) return;
|
|
552
|
+
var show = this.cy.zoom() >= this.settings.labelZoom;
|
|
553
|
+
this.cy.nodes().forEach(function (node) {
|
|
554
|
+
if (node.hasClass('highlighted')) return;
|
|
555
|
+
if ((node.data('degree') || 0) >= HUB_DEGREE_THRESHOLD) return;
|
|
556
|
+
node.style('text-opacity', show ? 0.9 : 0);
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// Search supports Obsidian-flavoured operators alongside plain text:
|
|
561
|
+
// tag:foo entries tagged foo path:docs URL contains "docs"
|
|
562
|
+
// Plain terms match title/basename/aliases; all terms must match (AND).
|
|
563
|
+
GraphApp.prototype.matchesQuery = function (node, terms) {
|
|
564
|
+
var d = node.data();
|
|
565
|
+
var label = normalize(d.label);
|
|
566
|
+
var aliases = d.aliasText || '';
|
|
567
|
+
for (var i = 0; i < terms.length; i++) {
|
|
568
|
+
var term = terms[i];
|
|
569
|
+
if (term.slice(0, 4) === 'tag:') {
|
|
570
|
+
var tag = term.slice(4).replace(/^#/, '');
|
|
571
|
+
if (!tag) continue;
|
|
572
|
+
if ((d.tags || []).indexOf(tag) === -1) return false;
|
|
573
|
+
} else if (term.slice(0, 5) === 'path:') {
|
|
574
|
+
var path = term.slice(5);
|
|
575
|
+
if (!path) continue;
|
|
576
|
+
if (String(d.url || '').toLowerCase().indexOf(path) === -1) return false;
|
|
577
|
+
} else if (label.indexOf(term) === -1 && aliases.indexOf(term) === -1) {
|
|
578
|
+
return false;
|
|
429
579
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
580
|
+
}
|
|
581
|
+
return true;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
GraphApp.prototype.applySearch = function () {
|
|
585
|
+
if (!this.cy) return;
|
|
586
|
+
var status = document.getElementById('obsidian-graph-status');
|
|
587
|
+
var q = normalize(this.searchQuery);
|
|
588
|
+
this.cy.elements().removeClass('faded highlighted');
|
|
589
|
+
if (!q) {
|
|
590
|
+
if (status) status.textContent = '';
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
var terms = q.split(/\s+/).filter(Boolean);
|
|
594
|
+
var app = this;
|
|
595
|
+
var matches = this.visibleElements().nodes().filter(function (n) {
|
|
596
|
+
return app.matchesQuery(n, terms);
|
|
597
|
+
});
|
|
598
|
+
if (matches.length === 0) {
|
|
599
|
+
if (status) status.textContent = 'No nodes match “' + this.searchQuery + '”.';
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
var nhood = matches.closedNeighborhood().not('.obsidian-hidden');
|
|
603
|
+
this.cy.elements().not(nhood).addClass('faded');
|
|
604
|
+
matches.addClass('highlighted');
|
|
605
|
+
if (status) {
|
|
606
|
+
status.textContent = matches.length + ' node' +
|
|
434
607
|
(matches.length === 1 ? '' : 's') + ' matched.';
|
|
435
|
-
cy.fit(matches, 110);
|
|
436
608
|
}
|
|
609
|
+
this.cy.fit(matches, 110);
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
GraphApp.prototype.updateStats = function () {
|
|
613
|
+
var el = document.getElementById('obsidian-graph-stats');
|
|
614
|
+
if (!el || !this.cy) return;
|
|
615
|
+
var total = this.cy.nodes('[!broken]').length;
|
|
616
|
+
var visible = this.cy.nodes('[!broken]').not('.obsidian-hidden').length;
|
|
617
|
+
var edges = this.visibleElements().edges().length;
|
|
618
|
+
var broken = this.cy.nodes('[?broken]').length;
|
|
619
|
+
var pages = (visible === total) ?
|
|
620
|
+
total + ' pages' : visible + ' of ' + total + ' pages';
|
|
621
|
+
el.innerHTML =
|
|
622
|
+
'<span class="badge text-bg-secondary me-2">' + pages + '</span>' +
|
|
623
|
+
'<span class="badge text-bg-info me-2">' + edges + ' links</span>' +
|
|
624
|
+
(broken > 0 ?
|
|
625
|
+
'<span class="badge text-bg-danger">' + broken + ' broken</span>' :
|
|
626
|
+
'<span class="badge text-bg-success">0 broken</span>');
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// --- Hover preview card ----------------------------------------------------
|
|
630
|
+
GraphApp.prototype.showPreview = function (node) {
|
|
631
|
+
var card = document.getElementById('obsidian-graph-preview');
|
|
632
|
+
if (!card || node.data('broken')) { this.hidePreview(); return; }
|
|
633
|
+
var d = node.data();
|
|
634
|
+
var links = d.degree === 1 ? '1 link' : (d.degree || 0) + ' links';
|
|
635
|
+
card.innerHTML =
|
|
636
|
+
'<div class="card-body p-2">' +
|
|
637
|
+
'<div class="d-flex align-items-center gap-2 mb-1">' +
|
|
638
|
+
'<span class="obsidian-graph-preview-swatch" aria-hidden="true"></span>' +
|
|
639
|
+
'<strong class="small text-truncate">' + escapeHtml(d.label) + '</strong>' +
|
|
640
|
+
'</div>' +
|
|
641
|
+
'<div class="d-flex gap-2 align-items-center small text-secondary mb-1">' +
|
|
642
|
+
'<span class="badge text-bg-light border">' + escapeHtml(d.collection) + '</span>' +
|
|
643
|
+
'<span>' + links + '</span>' +
|
|
644
|
+
'</div>' +
|
|
645
|
+
(d.excerpt ?
|
|
646
|
+
'<p class="small mb-0 obsidian-graph-preview-excerpt">' + escapeHtml(d.excerpt) + '</p>' : '') +
|
|
647
|
+
'</div>';
|
|
648
|
+
var swatch = card.querySelector('.obsidian-graph-preview-swatch');
|
|
649
|
+
if (swatch) swatch.style.backgroundColor = d.color;
|
|
650
|
+
card.hidden = false;
|
|
651
|
+
// Position near the node, clamped inside the shell.
|
|
652
|
+
var pos = node.renderedPosition();
|
|
653
|
+
var shell = card.parentNode;
|
|
654
|
+
var maxX = shell.clientWidth - card.offsetWidth - 8;
|
|
655
|
+
var maxY = shell.clientHeight - card.offsetHeight - 8;
|
|
656
|
+
card.style.left = Math.max(8, Math.min(pos.x + 16, maxX)) + 'px';
|
|
657
|
+
card.style.top = Math.max(8, Math.min(pos.y + 16, maxY)) + 'px';
|
|
658
|
+
};
|
|
437
659
|
|
|
438
|
-
|
|
660
|
+
GraphApp.prototype.hidePreview = function () {
|
|
661
|
+
var card = document.getElementById('obsidian-graph-preview');
|
|
662
|
+
if (card) card.hidden = true;
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// --- Theme reactivity ------------------------------------------------------
|
|
666
|
+
GraphApp.prototype.watchTheme = function () {
|
|
667
|
+
var app = this;
|
|
668
|
+
var restyle = debounce(function () { app.applyDisplay(); }, 50);
|
|
669
|
+
var observer = new MutationObserver(restyle);
|
|
670
|
+
[document.documentElement, document.body].forEach(function (el) {
|
|
671
|
+
observer.observe(el, { attributes: true, attributeFilter: ['data-bs-theme'] });
|
|
672
|
+
});
|
|
673
|
+
if (window.matchMedia) {
|
|
674
|
+
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
675
|
+
if (mq.addEventListener) mq.addEventListener('change', restyle);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// Control panel wiring
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
function bindCheckbox(id, value, onChange) {
|
|
683
|
+
var input = document.getElementById(id);
|
|
684
|
+
if (!input) return;
|
|
685
|
+
input.checked = !!value;
|
|
686
|
+
input.addEventListener('change', function () { onChange(input.checked); });
|
|
439
687
|
}
|
|
440
688
|
|
|
441
|
-
function
|
|
442
|
-
var
|
|
443
|
-
if (!
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
var
|
|
447
|
-
if (
|
|
448
|
-
var status = document.getElementById('obsidian-graph-status');
|
|
449
|
-
if (status) status.textContent = '';
|
|
450
|
-
cy.fit(undefined, 80);
|
|
689
|
+
function bindRange(id, value, onInput) {
|
|
690
|
+
var input = document.getElementById(id);
|
|
691
|
+
if (!input) return;
|
|
692
|
+
input.value = value;
|
|
693
|
+
input.addEventListener('input', function () {
|
|
694
|
+
var n = parseFloat(input.value);
|
|
695
|
+
if (isFinite(n)) onInput(n);
|
|
451
696
|
});
|
|
452
697
|
}
|
|
453
698
|
|
|
454
|
-
function
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
699
|
+
function wireCollectionFilters(app) {
|
|
700
|
+
var host = document.getElementById('obsidian-graph-collections');
|
|
701
|
+
if (!host) return;
|
|
702
|
+
var counts = Object.create(null);
|
|
703
|
+
app.entries.forEach(function (entry) {
|
|
704
|
+
var name = entry.collection || 'page';
|
|
705
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
706
|
+
});
|
|
707
|
+
var names = Object.keys(counts).sort(function (a, b) {
|
|
708
|
+
return counts[b] - counts[a] || a.localeCompare(b);
|
|
709
|
+
});
|
|
710
|
+
host.innerHTML = '';
|
|
711
|
+
names.forEach(function (name) {
|
|
712
|
+
var id = 'obsidian-graph-col-' + name.replace(/[^a-z0-9-]/gi, '-');
|
|
713
|
+
var item = document.createElement('div');
|
|
714
|
+
item.className = 'form-check form-switch obsidian-graph-collection-filter';
|
|
715
|
+
item.innerHTML =
|
|
716
|
+
'<input class="form-check-input" type="checkbox" role="switch" id="' + id + '">' +
|
|
717
|
+
'<label class="form-check-label d-flex align-items-center gap-2" for="' + id + '">' +
|
|
718
|
+
'<span class="swatch" aria-hidden="true"></span>' +
|
|
719
|
+
'<span class="flex-grow-1 text-capitalize">' + escapeHtml(name) + '</span>' +
|
|
720
|
+
'<span class="badge text-bg-secondary">' + counts[name] + '</span>' +
|
|
721
|
+
'</label>';
|
|
722
|
+
var input = item.querySelector('input');
|
|
723
|
+
input.checked = app.settings.collections[name] !== false;
|
|
724
|
+
input.addEventListener('change', function () {
|
|
725
|
+
app.settings.collections[name] = input.checked;
|
|
726
|
+
app.applyFilters();
|
|
727
|
+
app.applySearch();
|
|
728
|
+
});
|
|
729
|
+
item.querySelector('.swatch').style.backgroundColor = collectionColor(name, 0.92);
|
|
730
|
+
host.appendChild(item);
|
|
731
|
+
});
|
|
461
732
|
}
|
|
462
733
|
|
|
463
|
-
function
|
|
464
|
-
var
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
734
|
+
function wireControls(app) {
|
|
735
|
+
var s = app.settings;
|
|
736
|
+
|
|
737
|
+
var search = document.getElementById('obsidian-graph-search');
|
|
738
|
+
if (search) {
|
|
739
|
+
search.addEventListener('input', debounce(function () {
|
|
740
|
+
app.searchQuery = search.value;
|
|
741
|
+
app.applySearch();
|
|
742
|
+
}, 150));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
wireCollectionFilters(app);
|
|
746
|
+
|
|
747
|
+
bindCheckbox('obsidian-graph-orphans', s.showOrphans, function (checked) {
|
|
748
|
+
s.showOrphans = checked;
|
|
749
|
+
app.applyFilters();
|
|
750
|
+
});
|
|
751
|
+
bindCheckbox('obsidian-graph-broken', s.showBroken, function (checked) {
|
|
752
|
+
s.showBroken = checked;
|
|
753
|
+
app.applyFilters();
|
|
754
|
+
});
|
|
755
|
+
bindCheckbox('obsidian-graph-arrows', s.showArrows, function (checked) {
|
|
756
|
+
s.showArrows = checked;
|
|
757
|
+
app.applyDisplay();
|
|
758
|
+
});
|
|
759
|
+
bindCheckbox('obsidian-graph-animate', s.animateLayout, function (checked) {
|
|
760
|
+
s.animateLayout = checked;
|
|
761
|
+
saveSettings(s);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
bindRange('obsidian-graph-node-size', s.nodeScale, debounce(function (n) {
|
|
765
|
+
s.nodeScale = clamp(n, 0.5, 2, 1);
|
|
766
|
+
app.applyDisplay();
|
|
767
|
+
}, 80));
|
|
768
|
+
bindRange('obsidian-graph-link-width', s.linkScale, debounce(function (n) {
|
|
769
|
+
s.linkScale = clamp(n, 0.5, 3, 1);
|
|
770
|
+
app.applyDisplay();
|
|
771
|
+
}, 80));
|
|
772
|
+
bindRange('obsidian-graph-label-zoom', s.labelZoom, debounce(function (n) {
|
|
773
|
+
s.labelZoom = clamp(n, 0.4, 2.5, 1.25);
|
|
774
|
+
app.applyDisplay();
|
|
775
|
+
}, 80));
|
|
776
|
+
|
|
777
|
+
var relayout = debounce(function () { app.runLayout(false); }, 350);
|
|
778
|
+
bindRange('obsidian-graph-repulsion', s.repulsion, function (n) {
|
|
779
|
+
s.repulsion = clamp(n, 0.3, 3, 1);
|
|
780
|
+
saveSettings(s);
|
|
781
|
+
relayout();
|
|
470
782
|
});
|
|
783
|
+
bindRange('obsidian-graph-link-distance', s.linkDistance, function (n) {
|
|
784
|
+
s.linkDistance = clamp(n, 0.4, 2.5, 1);
|
|
785
|
+
saveSettings(s);
|
|
786
|
+
relayout();
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
var rerun = document.getElementById('obsidian-graph-relayout');
|
|
790
|
+
if (rerun) {
|
|
791
|
+
rerun.addEventListener('click', function () { app.runLayout(true); });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
var reset = document.getElementById('obsidian-graph-reset');
|
|
795
|
+
if (reset) {
|
|
796
|
+
reset.addEventListener('click', function () {
|
|
797
|
+
app.settings = JSON.parse(JSON.stringify(DEFAULTS));
|
|
798
|
+
s = app.settings;
|
|
799
|
+
try { window.localStorage.removeItem(STORAGE_KEY); } catch (e) { /* ignore */ }
|
|
800
|
+
// Reflect defaults back into every control, then re-apply the view.
|
|
801
|
+
syncControls(app);
|
|
802
|
+
wireCollectionFilters(app);
|
|
803
|
+
app.applyDisplay();
|
|
804
|
+
app.applyFilters();
|
|
805
|
+
window.ObsidianGraph.settings = app.settings;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
var fit = document.getElementById('obsidian-graph-fit');
|
|
810
|
+
if (fit) {
|
|
811
|
+
fit.addEventListener('click', function () {
|
|
812
|
+
app.cy.elements().removeClass('faded highlighted');
|
|
813
|
+
if (search) search.value = '';
|
|
814
|
+
app.searchQuery = '';
|
|
815
|
+
var status = document.getElementById('obsidian-graph-status');
|
|
816
|
+
if (status) status.textContent = '';
|
|
817
|
+
app.cy.fit(app.visibleElements(), 80);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function zoomBy(factor) {
|
|
822
|
+
if (!app.cy) return;
|
|
823
|
+
app.cy.animate({
|
|
824
|
+
zoom: {
|
|
825
|
+
level: app.cy.zoom() * factor,
|
|
826
|
+
renderedPosition: { x: app.container.clientWidth / 2, y: app.container.clientHeight / 2 }
|
|
827
|
+
},
|
|
828
|
+
duration: prefersReducedMotion() ? 0 : 120
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
var zoomIn = document.getElementById('obsidian-graph-zoom-in');
|
|
832
|
+
if (zoomIn) zoomIn.addEventListener('click', function () { zoomBy(1.35); });
|
|
833
|
+
var zoomOut = document.getElementById('obsidian-graph-zoom-out');
|
|
834
|
+
if (zoomOut) zoomOut.addEventListener('click', function () { zoomBy(1 / 1.35); });
|
|
835
|
+
|
|
836
|
+
wireFullscreen(app);
|
|
471
837
|
}
|
|
472
838
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
var
|
|
476
|
-
var
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
'
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
839
|
+
// Reflect app.settings into the checkbox/range inputs (used by Reset).
|
|
840
|
+
function syncControls(app) {
|
|
841
|
+
var s = app.settings;
|
|
842
|
+
var map = {
|
|
843
|
+
'obsidian-graph-orphans': s.showOrphans,
|
|
844
|
+
'obsidian-graph-broken': s.showBroken,
|
|
845
|
+
'obsidian-graph-arrows': s.showArrows,
|
|
846
|
+
'obsidian-graph-animate': s.animateLayout
|
|
847
|
+
};
|
|
848
|
+
Object.keys(map).forEach(function (id) {
|
|
849
|
+
var el = document.getElementById(id);
|
|
850
|
+
if (el) el.checked = !!map[id];
|
|
851
|
+
});
|
|
852
|
+
var ranges = {
|
|
853
|
+
'obsidian-graph-node-size': s.nodeScale,
|
|
854
|
+
'obsidian-graph-link-width': s.linkScale,
|
|
855
|
+
'obsidian-graph-label-zoom': s.labelZoom,
|
|
856
|
+
'obsidian-graph-repulsion': s.repulsion,
|
|
857
|
+
'obsidian-graph-link-distance': s.linkDistance
|
|
858
|
+
};
|
|
859
|
+
Object.keys(ranges).forEach(function (id) {
|
|
860
|
+
var el = document.getElementById(id);
|
|
861
|
+
if (el) el.value = ranges[id];
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function wireFullscreen(app) {
|
|
866
|
+
var btn = document.getElementById('obsidian-graph-fullscreen');
|
|
867
|
+
var shell = document.getElementById('obsidian-graph-shell');
|
|
868
|
+
if (!btn || !shell) return;
|
|
869
|
+
|
|
870
|
+
function isFullscreen() {
|
|
871
|
+
return document.fullscreenElement === shell ||
|
|
872
|
+
shell.classList.contains('obsidian-graph-shell-expanded');
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function refresh() {
|
|
876
|
+
var active = isFullscreen();
|
|
877
|
+
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|
878
|
+
var icon = btn.querySelector('.bi');
|
|
879
|
+
if (icon) {
|
|
880
|
+
icon.className = active ?
|
|
881
|
+
'bi bi-fullscreen-exit' : 'bi bi-arrows-fullscreen';
|
|
882
|
+
}
|
|
883
|
+
if (app.cy) {
|
|
884
|
+
app.cy.resize();
|
|
885
|
+
app.cy.fit(app.visibleElements(), 80);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
btn.addEventListener('click', function () {
|
|
890
|
+
if (isFullscreen()) {
|
|
891
|
+
if (document.fullscreenElement === shell && document.exitFullscreen) {
|
|
892
|
+
document.exitFullscreen();
|
|
893
|
+
}
|
|
894
|
+
shell.classList.remove('obsidian-graph-shell-expanded');
|
|
895
|
+
refresh();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (shell.requestFullscreen) {
|
|
899
|
+
shell.requestFullscreen().catch(function () {
|
|
900
|
+
// Fullscreen API blocked (iframe / permission) — CSS overlay fallback.
|
|
901
|
+
shell.classList.add('obsidian-graph-shell-expanded');
|
|
902
|
+
refresh();
|
|
903
|
+
});
|
|
904
|
+
} else {
|
|
905
|
+
shell.classList.add('obsidian-graph-shell-expanded');
|
|
906
|
+
refresh();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
document.addEventListener('fullscreenchange', refresh);
|
|
910
|
+
document.addEventListener('keydown', function (evt) {
|
|
911
|
+
if (evt.key === 'Escape' && shell.classList.contains('obsidian-graph-shell-expanded')) {
|
|
912
|
+
shell.classList.remove('obsidian-graph-shell-expanded');
|
|
913
|
+
refresh();
|
|
914
|
+
}
|
|
915
|
+
});
|
|
485
916
|
}
|
|
486
917
|
|
|
918
|
+
// ---------------------------------------------------------------------------
|
|
487
919
|
function init() {
|
|
488
920
|
var container = document.getElementById(CONTAINER_ID);
|
|
489
921
|
if (!container) return;
|
|
@@ -501,8 +933,6 @@
|
|
|
501
933
|
.then(function (payload) {
|
|
502
934
|
var entries = (payload && payload.entries) || [];
|
|
503
935
|
var byKey = buildLookup(entries);
|
|
504
|
-
var elements = buildElements(entries, byKey);
|
|
505
|
-
computeNodeDegree(elements);
|
|
506
936
|
loadCytoscape(function (ok) {
|
|
507
937
|
if (!ok) {
|
|
508
938
|
container.innerHTML =
|
|
@@ -513,12 +943,24 @@
|
|
|
513
943
|
return;
|
|
514
944
|
}
|
|
515
945
|
container.innerHTML = '';
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
946
|
+
var app = new GraphApp(container, entries, byKey);
|
|
947
|
+
app.elements = buildElements(entries, byKey);
|
|
948
|
+
computeNodeDegree(app.elements);
|
|
949
|
+
// Apply the persisted node-size multiplier before first paint.
|
|
950
|
+
app.elements.forEach(function (el) {
|
|
951
|
+
if (el.group === 'nodes') el.data.size *= app.settings.nodeScale;
|
|
952
|
+
});
|
|
953
|
+
app.render();
|
|
954
|
+
app.updateStats();
|
|
955
|
+
wireControls(app);
|
|
956
|
+
app.watchTheme();
|
|
957
|
+
window.ObsidianGraph = {
|
|
958
|
+
cy: app.cy,
|
|
959
|
+
byKey: byKey,
|
|
960
|
+
entries: entries,
|
|
961
|
+
settings: app.settings,
|
|
962
|
+
app: app
|
|
963
|
+
};
|
|
522
964
|
});
|
|
523
965
|
})
|
|
524
966
|
.catch(function (err) {
|