jekyll-theme-zer0 1.19.1 → 1.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +395 -0
  3. data/README.md +27 -19
  4. data/_data/authors.yml +154 -5
  5. data/_data/backlog.yml +5 -5
  6. data/_data/content_statistics.yml +273 -297
  7. data/_data/features.yml +4 -25
  8. data/_data/navigation/README.md +24 -0
  9. data/_data/navigation/about.yml +2 -0
  10. data/_data/navigation/main.yml +2 -7
  11. data/_data/roadmap.yml +86 -12
  12. data/_includes/components/author-avatar-url.html +28 -0
  13. data/_includes/components/author-bio.html +86 -0
  14. data/_includes/components/author-card.html +184 -121
  15. data/_includes/components/author-eeat.html +10 -4
  16. data/_includes/components/info-section.html +1 -1
  17. data/_includes/components/mermaid.html +0 -3
  18. data/_includes/components/post-card.html +19 -9
  19. data/_includes/content/giscus.html +3 -2
  20. data/_includes/core/footer-fabs.html +28 -0
  21. data/_includes/core/footer.html +7 -17
  22. data/_includes/core/head.html +2 -2
  23. data/_includes/navigation/breadcrumbs.html +20 -2
  24. data/_includes/navigation/local-graph.html +18 -2
  25. data/_includes/obsidian/full-graph.html +4 -6
  26. data/_layouts/article.html +44 -74
  27. data/_layouts/author.html +274 -0
  28. data/_layouts/authors.html +55 -0
  29. data/_layouts/news.html +3 -3
  30. data/_layouts/note.html +21 -6
  31. data/_layouts/notebook.html +21 -6
  32. data/_layouts/root.html +31 -17
  33. data/_layouts/section.html +3 -3
  34. data/_plugins/author_pages_generator.rb +121 -0
  35. data/_sass/components/_author.scss +219 -0
  36. data/_sass/components/_content-tables.scss +16 -1
  37. data/_sass/components/_notes-index.scss +102 -0
  38. data/_sass/components/_search-modal.scss +40 -0
  39. data/_sass/components/_ui-enhancements.scss +570 -0
  40. data/_sass/core/_docs-code-examples.scss +463 -0
  41. data/_sass/core/_docs-layout.scss +0 -453
  42. data/_sass/core/_navbar.scss +253 -0
  43. data/_sass/core/_sidebar-extras.scss +79 -0
  44. data/_sass/core/_toc.scss +87 -0
  45. data/_sass/core/_variables.scss +7 -142
  46. data/_sass/custom.scss +24 -1122
  47. data/_sass/layouts/_global-chrome.scss +59 -0
  48. data/assets/css/main.scss +19 -2
  49. data/assets/js/author-profile.js +190 -0
  50. data/assets/js/modules/navigation/navbar.js +104 -0
  51. data/assets/js/obsidian-graph.js +2 -2
  52. data/assets/js/obsidian-local-graph.js +11 -5
  53. data/assets/vendor/cytoscape/cytoscape.min.js +32 -0
  54. data/scripts/README.md +39 -0
  55. data/scripts/bin/validate +11 -1
  56. data/scripts/dev/css-diff.sh +49 -0
  57. data/scripts/dev/shot.js +37 -0
  58. data/scripts/features/generate-preview-images +110 -6
  59. data/scripts/features/pixelate-preview-images +126 -0
  60. data/scripts/features/pixelate_images.py +662 -0
  61. data/scripts/github-setup.sh +0 -0
  62. data/scripts/lib/preview_generator.py +47 -3
  63. data/scripts/pixelate-preview-images.sh +12 -0
  64. data/scripts/test/integration/auto-version +10 -8
  65. data/scripts/test/lib/run_tests.sh +2 -0
  66. data/scripts/test/lib/test_content_review.sh +205 -0
  67. data/scripts/test/lib/test_pixelate_images.sh +108 -0
  68. metadata +25 -20
  69. data/_data/hub.yml +0 -68
  70. data/_data/hub_index.yml +0 -203
  71. data/_data/navigation/hub.yml +0 -110
  72. data/assets/vendor/font-awesome/css/all.min.css +0 -9
  73. data/assets/vendor/font-awesome/webfonts/fa-brands-400.ttf +0 -0
  74. data/assets/vendor/font-awesome/webfonts/fa-brands-400.woff2 +0 -0
  75. data/assets/vendor/font-awesome/webfonts/fa-regular-400.ttf +0 -0
  76. data/assets/vendor/font-awesome/webfonts/fa-regular-400.woff2 +0 -0
  77. data/assets/vendor/font-awesome/webfonts/fa-solid-900.ttf +0 -0
  78. data/assets/vendor/font-awesome/webfonts/fa-solid-900.woff2 +0 -0
  79. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.ttf +0 -0
  80. data/assets/vendor/font-awesome/webfonts/fa-v4compatibility.woff2 +0 -0
  81. data/assets/vendor/jquery/jquery-3.7.1.min.js +0 -2
  82. data/scripts/lib/hub.rb +0 -208
  83. data/scripts/provision-org-sites.rb +0 -252
  84. data/scripts/provision-org-sites.sh +0 -23
  85. data/scripts/sync-hub-metadata.rb +0 -184
  86. data/scripts/sync-hub-metadata.sh +0 -22
@@ -0,0 +1,59 @@
1
+ // ============================================================================
2
+
3
+ // Global page chrome — base element resets + sticky/shadow/min-height helpers
4
+
5
+ // ----------------------------------------------------------------------------
6
+
7
+ // Extracted from custom.scss (Phase 5). Imported via the custom.scss barrel at
8
+
9
+ // its original position so the cascade is unchanged.
10
+
11
+ // ============================================================================
12
+
13
+ html, body {
14
+ max-width: 100%;
15
+ }
16
+
17
+ // Theme-wide horizontal-overflow safety net.
18
+ // A stray wide element (an unwrapped Bootstrap `.row`, a long code token, a
19
+ // markdown table) creates a horizontal page scrollbar. Because the header is
20
+ // `position: fixed` it only spans the viewport, so any page-level h-scroll
21
+ // makes the navbar look "cut off" on the right. `overflow-x: clip` removes
22
+ // that scrollbar at the root WITHOUT breaking `position: sticky` descendants
23
+ // (the docs sidebar / TOC) the way `overflow-x: hidden` would — clip does not
24
+ // create a scroll container. Genuinely wide content (tables, code blocks)
25
+ // keeps its own local `overflow-x: auto` so nothing is hidden by this clip.
26
+ html {
27
+ overflow-x: clip;
28
+ }
29
+
30
+ * {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ .make-me-sticky {
35
+ position: -webkit-sticky;
36
+ position: sticky;
37
+ top: 0;
38
+ padding: 0 15px;
39
+ display: flex;
40
+ flex-wrap: wrap;
41
+ }
42
+
43
+ .bottom-shadow {
44
+ // background-color: rgba(255,255,255,0.95);
45
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 -1px 0 rgba(0, 0, 0, .15);
46
+ }
47
+
48
+ // Vendored Bootstrap build may omit vh utilities; landing hero uses this for stable min height
49
+ .min-vh-50 {
50
+ min-height: 50vh;
51
+ }
52
+
53
+ // MOVED → _sass/layouts/_landing.scss (token-aware version)
54
+ // Retained shim: keeps the inline-loading aspect-ratio fallback for the hero
55
+ // media slot so existing inline `aspect-ratio` markup in landing.html still
56
+ // has a visible placeholder background while the image streams in.
57
+ .landing-hero .landing-hero-media {
58
+ background-color: rgba(255, 255, 255, 0.06);
59
+ }
data/assets/css/main.scss CHANGED
@@ -18,6 +18,9 @@
18
18
 
19
19
  // 3. Bootstrap-docs-style layout (trimmed; was ~3.2k-line vendor snapshot)
20
20
  @import "core/docs-layout";
21
+ // Code-example chrome split out of docs-layout; imported immediately after
22
+ // it so the concatenated cascade order is unchanged.
23
+ @import "core/docs-code-examples";
21
24
 
22
25
  // 4. Reusable utilities (motion, focus rings) — used by components below
23
26
  @import "utilities/motion";
@@ -30,9 +33,11 @@
30
33
  @import "components/skeleton";
31
34
  @import "components/callout";
32
35
  @import "components/post-navigation";
36
+ @import "components/author";
33
37
  @import "components/footer";
34
38
  @import "components/theme-preview";
35
39
  @import "components/content-tables";
40
+ @import "components/search-modal";
36
41
 
37
42
  // 6. Layout partials
38
43
  @import "layouts/landing";
@@ -42,8 +47,20 @@
42
47
  // 7. Bootstrap Styles
43
48
  // @import "bootstrap.scss";
44
49
 
45
- // 8. Remaining custom styles (thin barrel see _sass/custom.scss header).
46
- // Slated for full decomposition in a follow-up release.
50
+ // 7b. Core nav/content partials formerly imported from the top of custom.scss.
51
+ // Lifted here so assets/css/main.scss is the single assembly manifest; kept
52
+ // in their original relative order (immediately before the custom layer) so
53
+ // the compiled cascade is unchanged.
54
+ @import "notebooks";
55
+ @import "core/nav-tree";
56
+ @import "core/sidebar-categories";
57
+ @import "core/navbar";
58
+ @import "core/offcanvas-panels";
59
+ @import "core/obsidian";
60
+
61
+ // 8. Custom layer — thin barrel that imports the focused partials the old
62
+ // custom.scss monolith was decomposed into (global-chrome, toc,
63
+ // sidebar-extras, ui-enhancements, notes-index). See _sass/custom.scss.
47
64
  @import "custom.scss";
48
65
 
49
66
  // 9. Custom Features
@@ -0,0 +1,190 @@
1
+ /*
2
+ * author-profile.js
3
+ * ---------------------------------------------------------------------------
4
+ * Progressive-enhancement controller for the interactive author profile page
5
+ * (_layouts/author.html). Self-activates on any [data-author-profile] container,
6
+ * so it is safe to load globally and is a no-op everywhere else.
7
+ *
8
+ * Powers: type filters (the stat cards), free-text search (title + tags),
9
+ * sort (newest / oldest / A–Z), topic/tag chips, a live result count, a
10
+ * clear-filters control, deep-linkable type filter via the URL hash
11
+ * (#type=posts), and a reduced-motion-aware count-up on the stat numbers.
12
+ *
13
+ * No dependencies. With JS disabled every item stays visible (crawlable).
14
+ * ---------------------------------------------------------------------------
15
+ */
16
+ (function () {
17
+ 'use strict';
18
+
19
+ function init(root) {
20
+ var grid = root.querySelector('[data-author-grid]');
21
+ if (!grid) return;
22
+
23
+ var items = Array.prototype.slice.call(grid.querySelectorAll('.author-item'));
24
+ var filterBtns = Array.prototype.slice.call(root.querySelectorAll('[data-filter]'));
25
+ var tagBtns = Array.prototype.slice.call(root.querySelectorAll('[data-tag-filter]'));
26
+ var searchInput = root.querySelector('[data-author-search]');
27
+ var sortSelect = root.querySelector('[data-author-sort]');
28
+ var clearBtns = Array.prototype.slice.call(root.querySelectorAll('[data-author-clear]'));
29
+ var countEl = root.querySelector('[data-author-count]');
30
+ var noResults = root.querySelector('[data-author-noresults]');
31
+ var total = items.length;
32
+
33
+ var state = {
34
+ filter: 'all',
35
+ query: '',
36
+ sort: sortSelect ? sortSelect.value : 'newest',
37
+ tag: null
38
+ };
39
+
40
+ function matches(item) {
41
+ if (state.filter !== 'all' && item.getAttribute('data-collection') !== state.filter) {
42
+ return false;
43
+ }
44
+ if (state.tag) {
45
+ var bag = '|' + (item.getAttribute('data-tags') || '') + '|';
46
+ if (bag.indexOf('|' + state.tag + '|') === -1) return false;
47
+ }
48
+ if (state.query) {
49
+ var hay = (item.getAttribute('data-title') || '') + '|' + (item.getAttribute('data-tags') || '');
50
+ if (hay.indexOf(state.query) === -1) return false;
51
+ }
52
+ return true;
53
+ }
54
+
55
+ function sortItems() {
56
+ var ordered = items.slice();
57
+ ordered.sort(function (a, b) {
58
+ if (state.sort === 'az') {
59
+ return (a.getAttribute('data-title') || '').localeCompare(b.getAttribute('data-title') || '');
60
+ }
61
+ var da = parseInt(a.getAttribute('data-date') || '0', 10) || 0;
62
+ var db = parseInt(b.getAttribute('data-date') || '0', 10) || 0;
63
+ return state.sort === 'oldest' ? da - db : db - da;
64
+ });
65
+ ordered.forEach(function (el) { grid.appendChild(el); });
66
+ }
67
+
68
+ function apply() {
69
+ sortItems();
70
+ var visible = 0;
71
+ items.forEach(function (item) {
72
+ if (matches(item)) { item.classList.remove('d-none'); visible++; }
73
+ else { item.classList.add('d-none'); }
74
+ });
75
+ if (countEl) countEl.textContent = 'Showing ' + visible + ' of ' + total;
76
+ if (noResults) noResults.classList.toggle('d-none', visible !== 0);
77
+ var active = state.filter !== 'all' || state.query !== '' || state.tag !== null;
78
+ clearBtns.forEach(function (b) { b.hidden = !active; });
79
+ }
80
+
81
+ function setFilter(type) {
82
+ state.filter = type;
83
+ filterBtns.forEach(function (b) {
84
+ var on = b.getAttribute('data-filter') === type;
85
+ b.classList.toggle('is-active', on);
86
+ b.setAttribute('aria-pressed', on ? 'true' : 'false');
87
+ });
88
+ writeHash();
89
+ apply();
90
+ }
91
+
92
+ function setTag(tag) {
93
+ state.tag = (state.tag === tag) ? null : tag;
94
+ tagBtns.forEach(function (b) {
95
+ var on = b.getAttribute('data-tag-filter') === state.tag;
96
+ b.classList.toggle('is-active', on);
97
+ b.setAttribute('aria-pressed', on ? 'true' : 'false');
98
+ });
99
+ apply();
100
+ }
101
+
102
+ function clearAll() {
103
+ state.query = '';
104
+ state.tag = null;
105
+ if (searchInput) searchInput.value = '';
106
+ tagBtns.forEach(function (b) {
107
+ b.classList.remove('is-active');
108
+ b.setAttribute('aria-pressed', 'false');
109
+ });
110
+ setFilter('all'); // also re-applies + resets hash
111
+ }
112
+
113
+ function writeHash() {
114
+ if (!window.history || !window.history.replaceState) return;
115
+ try {
116
+ if (state.filter && state.filter !== 'all') {
117
+ window.history.replaceState(null, '', '#type=' + state.filter);
118
+ } else {
119
+ window.history.replaceState(null, '', window.location.pathname + window.location.search);
120
+ }
121
+ } catch (e) { /* no-op */ }
122
+ }
123
+
124
+ function readHash() {
125
+ var m = /[#&]type=([a-z0-9_-]+)/i.exec(window.location.hash);
126
+ if (m && filterBtns.some(function (b) { return b.getAttribute('data-filter') === m[1]; })) {
127
+ setFilter(m[1]);
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+
133
+ // ---- Wire events ----
134
+ filterBtns.forEach(function (b) {
135
+ b.addEventListener('click', function () { setFilter(b.getAttribute('data-filter')); });
136
+ });
137
+ tagBtns.forEach(function (b) {
138
+ b.addEventListener('click', function () { setTag(b.getAttribute('data-tag-filter')); });
139
+ });
140
+ clearBtns.forEach(function (b) { b.addEventListener('click', clearAll); });
141
+ if (sortSelect) {
142
+ sortSelect.addEventListener('change', function () { state.sort = sortSelect.value; apply(); });
143
+ }
144
+ if (searchInput) {
145
+ var t;
146
+ searchInput.addEventListener('input', function () {
147
+ clearTimeout(t);
148
+ t = setTimeout(function () {
149
+ state.query = searchInput.value.trim().toLowerCase();
150
+ apply();
151
+ }, 150);
152
+ });
153
+ }
154
+
155
+ countUp(root);
156
+
157
+ // Deep link (e.g. /authors/bamr87/#type=docs); falls back to a plain apply.
158
+ if (!readHash()) apply();
159
+ }
160
+
161
+ // Reduced-motion-aware count-up for the stat numbers.
162
+ function countUp(root) {
163
+ var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
164
+ var nums = root.querySelectorAll('.author-stat__num');
165
+ if (reduce || !nums.length || !window.requestAnimationFrame) return;
166
+ Array.prototype.forEach.call(nums, function (el) {
167
+ var target = parseInt(el.textContent, 10);
168
+ if (isNaN(target) || target <= 0) return;
169
+ var duration = 600, start = null;
170
+ el.textContent = '0';
171
+ function step(ts) {
172
+ if (start === null) start = ts;
173
+ var p = Math.min((ts - start) / duration, 1);
174
+ el.textContent = String(Math.round(p * target));
175
+ if (p < 1) window.requestAnimationFrame(step);
176
+ }
177
+ window.requestAnimationFrame(step);
178
+ });
179
+ }
180
+
181
+ function boot() {
182
+ Array.prototype.forEach.call(document.querySelectorAll('[data-author-profile]'), init);
183
+ }
184
+
185
+ if (document.readyState === 'loading') {
186
+ document.addEventListener('DOMContentLoaded', boot);
187
+ } else {
188
+ boot();
189
+ }
190
+ })();
@@ -31,6 +31,8 @@ export class Navbar {
31
31
  this._tooltips = [];
32
32
  this._listeners = [];
33
33
  this._resizeTimer = null;
34
+ this._fitTimer = null;
35
+ this._lastFitWarnings = new Set();
34
36
  }
35
37
 
36
38
  // -----------------------------------------------------------------
@@ -57,6 +59,7 @@ export class Navbar {
57
59
  this._setupDropdownHoverDelay();
58
60
  this._setupFocusTrap();
59
61
  this._setupResponsiveReset();
62
+ this._setupFitWarnings();
60
63
  }
61
64
 
62
65
  destroy() {
@@ -277,4 +280,105 @@ export class Navbar {
277
280
  }, 250);
278
281
  });
279
282
  }
283
+
284
+ // -----------------------------------------------------------------
285
+ // Dev-only constraint warnings
286
+ // -----------------------------------------------------------------
287
+ // Surfaces two failure modes that otherwise pass silently until a user
288
+ // hits a specific viewport: (1) the inline menubar has more items than can
289
+ // fit even icon-only, and (2) page content overflows the viewport, which
290
+ // makes the fixed-top navbar look "cut off" on the right. Logged to the
291
+ // console only on local/dev hosts — never on a deployed (e.g. *.github.io)
292
+ // site — so production consoles stay clean.
293
+ // -----------------------------------------------------------------
294
+ _isDevHost() {
295
+ try {
296
+ const h = window.location.hostname;
297
+ return h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' ||
298
+ h === '::1' || h === '[::1]' || h === '' ||
299
+ /\.(local|test|localhost)$/.test(h);
300
+ } catch (e) {
301
+ return false;
302
+ }
303
+ }
304
+
305
+ _setupFitWarnings() {
306
+ if (!this._isDevHost()) return;
307
+ const run = () => this._checkFit();
308
+ // Let first paint + async chrome settle before the first check.
309
+ setTimeout(run, 600);
310
+ this._on(window, 'resize', () => {
311
+ clearTimeout(this._fitTimer);
312
+ this._fitTimer = setTimeout(run, 400);
313
+ });
314
+ }
315
+
316
+ /** Emit a warning at most once per distinct message until conditions change. */
317
+ _warnOnce(key, message, ...extra) {
318
+ if (this._lastFitWarnings.has(key)) return;
319
+ this._lastFitWarnings.add(key);
320
+ console.warn(message, ...extra);
321
+ }
322
+
323
+ _checkFit() {
324
+ const PREFIX = '[zer0-mistakes navbar]';
325
+ const vw = window.innerWidth;
326
+ // Re-evaluate from scratch each pass so a fixed layout stops warning.
327
+ this._lastFitWarnings.clear();
328
+
329
+ // 1) Inline menubar overflow at lg+ — more/wider items than the track holds.
330
+ if (vw >= this.config.breakpoints.lg) {
331
+ const navList = document.querySelector('#bdNavbar .navbar-nav');
332
+ if (navList && navList.scrollWidth > navList.clientWidth + 2) {
333
+ const items = navList.querySelectorAll(':scope > li:not(.d-lg-none)').length;
334
+ this._warnOnce(
335
+ 'menu-overflow',
336
+ `${PREFIX} Top navigation overflows the bar at ${vw}px (~${items} ` +
337
+ `visible items). Items are already icon-only here and may still clip. ` +
338
+ `Reduce top-level entries, shorten titles, or group them under ` +
339
+ `dropdowns in _data/navigation/main.yml.`
340
+ );
341
+ }
342
+ }
343
+
344
+ // 2) Page-level horizontal overflow (the usual "navbar looks cut off" cause).
345
+ // `main.scrollWidth` only exceeds its clientWidth when something overflows
346
+ // WITHOUT its own scroll container — locally-scrollable wide content
347
+ // (tables/code) does not trip this. Cheap gate before the DOM scan.
348
+ const main = document.getElementById('main-content');
349
+ if (main && main.scrollWidth > main.clientWidth + 2) {
350
+ const cw = document.documentElement.clientWidth;
351
+ let worst = null;
352
+ main.querySelectorAll('*').forEach((el) => {
353
+ const r = el.getBoundingClientRect();
354
+ if (r.width === 0 || r.right <= cw + 2) return;
355
+ if (getComputedStyle(el).position === 'fixed') return;
356
+ // Overflow contained by an ancestor clip/scroll box never reaches
357
+ // the page. Exclude the root <html> clip so we still report what
358
+ // the safety net is hiding.
359
+ let contained = false, n = el.parentElement;
360
+ while (n && n !== document.body && n !== document.documentElement) {
361
+ if (getComputedStyle(n).overflowX !== 'visible') { contained = true; break; }
362
+ n = n.parentElement;
363
+ }
364
+ if (contained) return;
365
+ if (!worst || r.right > worst.right) worst = { el, right: r.right };
366
+ });
367
+ if (worst) {
368
+ const el = worst.el;
369
+ const sel = el.tagName.toLowerCase() +
370
+ (el.id ? `#${el.id}` : '') +
371
+ (el.className && typeof el.className === 'string'
372
+ ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '');
373
+ this._warnOnce(
374
+ 'page-overflow',
375
+ `${PREFIX} Content overflows the viewport by ` +
376
+ `${Math.round(worst.right - cw)}px at ${vw}px (widest: ${sel}). This ` +
377
+ `forces a horizontal scrollbar and can make the fixed navbar look cut ` +
378
+ `off. Wrap wide content in a scroll container or a Bootstrap .container.`,
379
+ el
380
+ );
381
+ }
382
+ }
383
+ }
280
384
  }
@@ -5,8 +5,8 @@
5
5
  * /assets/data/wiki-index.json into the element with id `obsidian-graph`.
6
6
  *
7
7
  * Loaded only on the graph page (pages/_docs/obsidian/graph.md), which
8
- * also pulls in cytoscape.js from a CDN. No runtime dependencies are
9
- * added to the rest of the site.
8
+ * also pulls in the vendored cytoscape.js (assets/vendor/cytoscape/). No
9
+ * runtime CDN dependencies are added to the rest of the site.
10
10
  *
11
11
  * Nodes: one per indexed entry (collection doc or standalone page)
12
12
  * Edges: directed, source -> target, derived from `entry.outgoing`
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Loaded by _includes/navigation/local-graph.html inside a dedicated
9
9
  * collapsible side panel. Cytoscape.js is loaded lazily (and only once) from
10
- * the same CDN as the full graph page.
10
+ * the vendored copy under assets/vendor/cytoscape/ (no CDN).
11
11
  *
12
12
  * Subgraph:
13
13
  * - center = current page (matched against entry.url, falling back to
@@ -26,8 +26,16 @@
26
26
  var PANEL_SELECTOR = '[data-obsidian-local-graph-panel]';
27
27
  var TOGGLE_SELECTOR = '[data-obsidian-local-graph-toggle]';
28
28
  var STATUS_SELECTOR = '[data-obsidian-local-graph-status]';
29
- var CYTOSCAPE_URL = 'https://cdn.jsdelivr.net/npm/cytoscape@3.30.0/dist/cytoscape.min.js';
30
- var CYTOSCAPE_SRI = 'sha384-kpMsYllYzyaWU69Piok08rPNktpnjqAoDMdB00fjqUkEk3lkuUbSuwJ+oXrjvN6B';
29
+ // Cytoscape is vendored locally (assets/vendor/cytoscape/) — no CDN, so the
30
+ // graph works under strict CSP and offline. Resolve the vendored URL from this
31
+ // script's own (trusted) src so it stays correct under any site baseurl,
32
+ // without flowing DOM-supplied text into a script element. See issues #152, #205.
33
+ var SELF_SRC = (document.currentScript && document.currentScript.src) || '';
34
+ var CYTOSCAPE_URL = '/assets/vendor/cytoscape/cytoscape.min.js';
35
+ var _selfMatch = SELF_SRC.match(/^(.*\/)assets\/js\/obsidian-local-graph\.js(?:[?#].*)?$/);
36
+ if (_selfMatch) {
37
+ CYTOSCAPE_URL = _selfMatch[1] + 'assets/vendor/cytoscape/cytoscape.min.js';
38
+ }
31
39
 
32
40
  function companionElements(container) {
33
41
  return {
@@ -256,8 +264,6 @@
256
264
  }
257
265
  var s = document.createElement('script');
258
266
  s.src = CYTOSCAPE_URL;
259
- s.integrity = CYTOSCAPE_SRI;
260
- s.crossOrigin = 'anonymous';
261
267
  s.defer = true;
262
268
  s.onload = function () {
263
269
  window.__obsidianCytoscapeLoading.forEach(function (fn) { fn(); });