jekyll-theme-zer0 1.3.0 → 1.4.1

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.
@@ -1,13 +1,13 @@
1
1
  /*
2
2
  * obsidian-local-graph.js
3
3
  *
4
- * Renders a small "local graph" (current page + immediate neighbors) into
4
+ * Renders a focused "local graph" (current page + immediate neighbors) into
5
5
  * the element with id `obsidian-local-graph`. Mirrors Obsidian's local
6
6
  * graph view: a focused, page-scoped subgraph instead of the full site map.
7
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.
8
+ * Loaded by _includes/navigation/local-graph.html inside a dedicated
9
+ * collapsible side panel. Cytoscape.js is loaded lazily (and only once) from
10
+ * the same CDN as the full graph page.
11
11
  *
12
12
  * Subgraph:
13
13
  * - center = current page (matched against entry.url, falling back to
@@ -15,17 +15,52 @@
15
15
  * - depth = configurable via data-depth attribute (default 1)
16
16
  * - direction = both incoming and outgoing wiki-links
17
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.
18
+ * If the current page is in the wiki-index but has no local links, the panel
19
+ * stays available and renders a single-node graph for the current page.
20
+ * Pages outside the wiki-index still hide the panel.
21
21
  */
22
22
  (function () {
23
23
  'use strict';
24
24
 
25
25
  var CONTAINER_ID = 'obsidian-local-graph';
26
+ var PANEL_SELECTOR = '[data-obsidian-local-graph-panel]';
27
+ var TOGGLE_SELECTOR = '[data-obsidian-local-graph-toggle]';
28
+ var STATUS_SELECTOR = '[data-obsidian-local-graph-status]';
26
29
  var CYTOSCAPE_URL = 'https://cdn.jsdelivr.net/npm/cytoscape@3.30.0/dist/cytoscape.min.js';
27
30
  var CYTOSCAPE_SRI = 'sha384-kpMsYllYzyaWU69Piok08rPNktpnjqAoDMdB00fjqUkEk3lkuUbSuwJ+oXrjvN6B';
28
31
 
32
+ function companionElements(container) {
33
+ return {
34
+ panel: container.closest(PANEL_SELECTOR),
35
+ toggle: document.querySelector(TOGGLE_SELECTOR),
36
+ status: document.querySelector(STATUS_SELECTOR)
37
+ };
38
+ }
39
+
40
+ function setStatus(container, message, isError) {
41
+ var status = companionElements(container).status;
42
+ if (!status) return;
43
+ status.textContent = message || '';
44
+ status.hidden = !message;
45
+ status.classList.toggle('text-danger', !!isError);
46
+ status.classList.toggle('text-secondary', !isError);
47
+ }
48
+
49
+ function setPanelAvailable(container, available) {
50
+ var companions = companionElements(container);
51
+ [companions.panel, companions.toggle].forEach(function (element) {
52
+ if (!element) return;
53
+ element.hidden = !available;
54
+ });
55
+ }
56
+
57
+ function resizeGraph(container) {
58
+ var cy = container.__obsidianLocalGraph;
59
+ if (!cy) return;
60
+ cy.resize();
61
+ cy.fit(cy.elements(), 24);
62
+ }
63
+
29
64
  function normalize(value) {
30
65
  return String(value || '').toLowerCase().trim().replace(/\s+/g, ' ');
31
66
  }
@@ -242,7 +277,6 @@
242
277
  var cy = window.cytoscape({
243
278
  container: container,
244
279
  elements: elements,
245
- wheelSensitivity: 0.2,
246
280
  minZoom: 0.3,
247
281
  maxZoom: 3,
248
282
  autoungrabify: false,
@@ -252,7 +286,7 @@
252
286
  style: {
253
287
  'background-color': 'data(color)',
254
288
  'label': 'data(label)',
255
- 'font-size': '10px',
289
+ 'font-size': '11px',
256
290
  'font-weight': 500,
257
291
  'color': theme.label,
258
292
  'text-outline-color': theme.labelOutline,
@@ -262,8 +296,8 @@
262
296
  'text-valign': 'bottom',
263
297
  'text-margin-y': 4,
264
298
  'text-wrap': 'ellipsis',
265
- 'text-max-width': '110px',
266
- 'width': 14, 'height': 14,
299
+ 'text-max-width': '140px',
300
+ 'width': 16, 'height': 16,
267
301
  'border-width': 1.5,
268
302
  'border-color': theme.nodeBorder,
269
303
  'transition-property': 'background-color, border-color, width, height',
@@ -319,6 +353,8 @@
319
353
  }
320
354
  });
321
355
 
356
+ container.__obsidianLocalGraph = cy;
357
+
322
358
  cy.on('tap', 'node', function (evt) {
323
359
  var d = evt.target.data();
324
360
  if (!d.url || d.broken) return;
@@ -336,12 +372,30 @@
336
372
  evt.target.style('z-index', 99);
337
373
  });
338
374
 
375
+ requestAnimationFrame(function () {
376
+ resizeGraph(container);
377
+ });
378
+
339
379
  return cy;
340
380
  }
341
381
 
342
382
  function init() {
343
383
  var container = document.getElementById(CONTAINER_ID);
344
384
  if (!container) return;
385
+ setPanelAvailable(container, true);
386
+ setStatus(container, 'Loading graph...', false);
387
+
388
+ var panel = container.closest(PANEL_SELECTOR);
389
+ if (panel) {
390
+ panel.addEventListener('shown.bs.offcanvas', function () {
391
+ resizeGraph(container);
392
+ });
393
+ }
394
+
395
+ window.addEventListener('resize', function () {
396
+ resizeGraph(container);
397
+ });
398
+
345
399
  var depth = parseInt(container.getAttribute('data-depth') || '1', 10);
346
400
  if (!isFinite(depth) || depth < 1) depth = 1;
347
401
  var indexUrl = container.getAttribute('data-index-url') ||
@@ -352,25 +406,22 @@
352
406
  .then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
353
407
  .then(function (data) {
354
408
  var entries = Array.isArray(data && data.entries) ? data.entries : [];
355
- if (!entries.length) { container.style.display = 'none'; return; }
409
+ if (!entries.length) { setPanelAvailable(container, false); return; }
356
410
  var lookup = buildLookup(entries);
357
411
  var current = findCurrentEntry(lookup);
358
- if (!current) { container.style.display = 'none'; return; }
412
+ if (!current) { setPanelAvailable(container, false); return; }
359
413
  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
414
  loadCytoscape(function () {
367
415
  render(container, elements, current.url);
416
+ var nodeCount = elements.filter(function (element) { return element.group === 'nodes'; }).length;
417
+ var edgeCount = elements.filter(function (element) { return element.group === 'edges'; }).length;
418
+ setStatus(container, nodeCount + ' pages · ' + edgeCount + ' links', false);
368
419
  });
369
420
  })
370
421
  .catch(function (err) {
371
- // Sidebar widget failing is non-fatal — hide and stay quiet.
422
+ // Sidebar panel failing is non-fatal — hide and stay quiet.
372
423
  console.warn('[obsidian-local-graph] init failed:', err);
373
- container.style.display = 'none';
424
+ setPanelAvailable(container, false);
374
425
  });
375
426
  }
376
427
 
@@ -26,11 +26,28 @@
26
26
  return;
27
27
  }
28
28
 
29
+ var RESOLVER_SCRIPT_PATH = 'assets/js/obsidian-wiki-links.js';
30
+ var OBSIDIAN_CONFIG = window.OBSIDIAN_CONFIG || {};
31
+
32
+ function trimTrailingSlash(value) {
33
+ return (value === null || value === undefined ? '' : String(value)).replace(/\/$/, '');
34
+ }
35
+
36
+ function assetPath(path) {
37
+ var script = document.currentScript || document.querySelector('script[src*="obsidian-wiki-links.js"]');
38
+ var src = script && script.getAttribute('src');
39
+ var escapedScriptPath = RESOLVER_SCRIPT_PATH.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+ var match = src && src.match(new RegExp('^(.*?)' + escapedScriptPath + '(?:[?#].*)?$'));
41
+ if (match) return trimTrailingSlash(match[1]) + path;
42
+
43
+ var baseHref = (document.querySelector('base') || {}).href;
44
+ return trimTrailingSlash(baseHref) + path;
45
+ }
46
+
29
47
  var CONFIG = {
30
- indexUrl: (window.OBSIDIAN_WIKI_INDEX_URL ||
31
- ((document.querySelector('base') || {}).href || '/') + 'assets/data/wiki-index.json'),
32
- attachmentsPath: window.OBSIDIAN_ATTACHMENTS_PATH || '/assets/images/notes',
33
- tagBase: window.OBSIDIAN_TAG_BASE || '/tags/',
48
+ indexUrl: OBSIDIAN_CONFIG.wikiIndexUrl || window.OBSIDIAN_WIKI_INDEX_URL || assetPath('/assets/data/wiki-index.json'),
49
+ attachmentsPath: OBSIDIAN_CONFIG.attachmentsPath || window.OBSIDIAN_ATTACHMENTS_PATH || assetPath('/assets/images/notes'),
50
+ tagBase: OBSIDIAN_CONFIG.tagBase || window.OBSIDIAN_TAG_BASE || assetPath('/tags/'),
34
51
  wikiLinkClass: 'wiki-link',
35
52
  brokenLinkClass: 'wiki-link wiki-link-broken'
36
53
  };
data/scripts/README.md CHANGED
@@ -8,6 +8,7 @@ This directory contains automation scripts for managing the `jekyll-theme-zer0`
8
8
  scripts/
9
9
  ├── bin/ # Entry point commands (use these!)
10
10
  │ ├── build # Build gem without releasing
11
+ │ ├── validate # Preflight checks for local/CI validation
11
12
  │ ├── release # Full release workflow
12
13
  │ └── test # Run all test suites
13
14
  ├── lib/ # Shared libraries (sourced, not executed)
@@ -39,6 +40,10 @@ scripts/
39
40
  # Build gem
40
41
  ./scripts/bin/build
41
42
 
43
+ # Preflight validation
44
+ ./scripts/bin/validate --quick
45
+ ./scripts/bin/validate --start-docker
46
+
42
47
  # Full release workflow
43
48
  ./scripts/bin/release patch # or minor/major
44
49
 
@@ -57,6 +62,23 @@ Build the gem without the full release workflow.
57
62
  ./scripts/bin/build [--dry-run] [--verbose]
58
63
  ```
59
64
 
65
+ #### `bin/validate`
66
+ Run preflight validation before refactors, pull requests, and releases. The
67
+ quick path validates repository files, version consistency, YAML parsing, active
68
+ configuration contracts, config-file classification, and navigation data before
69
+ the Docker/local build stages run.
70
+
71
+ ```bash
72
+ ./scripts/bin/validate [options]
73
+
74
+ Options:
75
+ --quick Host-only checks for CI fast feedback
76
+ --full Include tests, Obsidian tests, and HTMLProofer
77
+ --start-docker Start the jekyll Docker Compose service if needed
78
+ --docker Require Docker Compose for Jekyll commands
79
+ --local Require local bundle exec for Jekyll commands
80
+ ```
81
+
60
82
  #### `bin/release`
61
83
  Full release workflow with changelog, version bump, and publishing.
62
84
 
data/scripts/bin/test CHANGED
@@ -12,6 +12,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  SCRIPTS_ROOT="$SCRIPT_DIR/.."
13
13
  LIB_DIR="$SCRIPTS_ROOT/lib"
14
14
  TEST_DIR="$SCRIPTS_ROOT/test"
15
+ REPO_ROOT="$(cd "$SCRIPTS_ROOT/.." && pwd)"
16
+ THEME_TEST_RUNNER="$REPO_ROOT/test/test_runner.sh"
15
17
 
16
18
  # Source common library for logging
17
19
  source "$LIB_DIR/common.sh"
@@ -37,6 +39,10 @@ TEST_SUITES:
37
39
  integration Run integration tests only
38
40
  install Run installer e2e suites only (test/test_install_*.sh)
39
41
 
42
+ THEME SUITES:
43
+ core, deployment, quality, installation, site_generation, styling,
44
+ visual, full, or --suites <list> are forwarded to test/test_runner.sh.
45
+
40
46
  OPTIONS:
41
47
  --verbose, -v Show detailed test output
42
48
  --dry-run Preview what tests would run
@@ -54,6 +60,62 @@ TEST LOCATIONS:
54
60
  EOF
55
61
  }
56
62
 
63
+ # Pass the repository-level theme runner's interface through this canonical
64
+ # command so CI, docs, and local users can converge on scripts/bin/test without
65
+ # breaking existing --suites calls.
66
+ maybe_delegate_to_theme_runner() {
67
+ [[ -f "$THEME_TEST_RUNNER" ]] || return 0
68
+
69
+ local should_delegate=false
70
+ local positional_suites=""
71
+ local delegated_args=()
72
+
73
+ while [[ $# -gt 0 ]]; do
74
+ case "$1" in
75
+ --suites|-s)
76
+ should_delegate=true
77
+ delegated_args+=("$1")
78
+ shift
79
+ [[ $# -gt 0 ]] || error "--suites requires a value"
80
+ delegated_args+=("$1")
81
+ ;;
82
+ core|deployment|quality|installation|site_generation|styling|visual|full)
83
+ should_delegate=true
84
+ if [[ -n "$positional_suites" ]]; then
85
+ positional_suites="${positional_suites},$1"
86
+ else
87
+ positional_suites="$1"
88
+ fi
89
+ ;;
90
+ --skip-docker|--skip-remote|--coverage|-c|--parallel|-p|--retry-failed|-r|--fail-fast|--baseline-compare)
91
+ should_delegate=true
92
+ delegated_args+=("$1")
93
+ ;;
94
+ --format|-f|--timeout|-t|--environment|-e)
95
+ should_delegate=true
96
+ local option_name="$1"
97
+ delegated_args+=("$1")
98
+ shift
99
+ [[ $# -gt 0 ]] || error "$option_name requires a value"
100
+ delegated_args+=("$1")
101
+ ;;
102
+ --verbose|-v)
103
+ delegated_args+=("$1")
104
+ ;;
105
+ esac
106
+ shift
107
+ done
108
+
109
+ if [[ "$should_delegate" == "true" ]]; then
110
+ if [[ -n "$positional_suites" ]]; then
111
+ delegated_args+=("--suites" "$positional_suites")
112
+ fi
113
+ exec "$THEME_TEST_RUNNER" "${delegated_args[@]}"
114
+ fi
115
+ }
116
+
117
+ maybe_delegate_to_theme_runner "$@"
118
+
57
119
  # Parse arguments
58
120
  TEST_SUITE="all"
59
121
  for arg in "$@"; do