jekyll-theme-zer0 1.3.0 → 1.4.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.
@@ -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
  }
@@ -252,7 +287,7 @@
252
287
  style: {
253
288
  'background-color': 'data(color)',
254
289
  'label': 'data(label)',
255
- 'font-size': '10px',
290
+ 'font-size': '11px',
256
291
  'font-weight': 500,
257
292
  'color': theme.label,
258
293
  'text-outline-color': theme.labelOutline,
@@ -262,8 +297,8 @@
262
297
  'text-valign': 'bottom',
263
298
  'text-margin-y': 4,
264
299
  'text-wrap': 'ellipsis',
265
- 'text-max-width': '110px',
266
- 'width': 14, 'height': 14,
300
+ 'text-max-width': '140px',
301
+ 'width': 16, 'height': 16,
267
302
  'border-width': 1.5,
268
303
  'border-color': theme.nodeBorder,
269
304
  'transition-property': 'background-color, border-color, width, height',
@@ -319,6 +354,8 @@
319
354
  }
320
355
  });
321
356
 
357
+ container.__obsidianLocalGraph = cy;
358
+
322
359
  cy.on('tap', 'node', function (evt) {
323
360
  var d = evt.target.data();
324
361
  if (!d.url || d.broken) return;
@@ -336,12 +373,30 @@
336
373
  evt.target.style('z-index', 99);
337
374
  });
338
375
 
376
+ requestAnimationFrame(function () {
377
+ resizeGraph(container);
378
+ });
379
+
339
380
  return cy;
340
381
  }
341
382
 
342
383
  function init() {
343
384
  var container = document.getElementById(CONTAINER_ID);
344
385
  if (!container) return;
386
+ setPanelAvailable(container, true);
387
+ setStatus(container, 'Loading graph...', false);
388
+
389
+ var panel = container.closest(PANEL_SELECTOR);
390
+ if (panel) {
391
+ panel.addEventListener('shown.bs.offcanvas', function () {
392
+ resizeGraph(container);
393
+ });
394
+ }
395
+
396
+ window.addEventListener('resize', function () {
397
+ resizeGraph(container);
398
+ });
399
+
345
400
  var depth = parseInt(container.getAttribute('data-depth') || '1', 10);
346
401
  if (!isFinite(depth) || depth < 1) depth = 1;
347
402
  var indexUrl = container.getAttribute('data-index-url') ||
@@ -352,25 +407,22 @@
352
407
  .then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); })
353
408
  .then(function (data) {
354
409
  var entries = Array.isArray(data && data.entries) ? data.entries : [];
355
- if (!entries.length) { container.style.display = 'none'; return; }
410
+ if (!entries.length) { setPanelAvailable(container, false); return; }
356
411
  var lookup = buildLookup(entries);
357
412
  var current = findCurrentEntry(lookup);
358
- if (!current) { container.style.display = 'none'; return; }
413
+ if (!current) { setPanelAvailable(container, false); return; }
359
414
  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
415
  loadCytoscape(function () {
367
416
  render(container, elements, current.url);
417
+ var nodeCount = elements.filter(function (element) { return element.group === 'nodes'; }).length;
418
+ var edgeCount = elements.filter(function (element) { return element.group === 'edges'; }).length;
419
+ setStatus(container, nodeCount + ' pages · ' + edgeCount + ' links', false);
368
420
  });
369
421
  })
370
422
  .catch(function (err) {
371
- // Sidebar widget failing is non-fatal — hide and stay quiet.
423
+ // Sidebar panel failing is non-fatal — hide and stay quiet.
372
424
  console.warn('[obsidian-local-graph] init failed:', err);
373
- container.style.display = 'none';
425
+ setPanelAvailable(container, false);
374
426
  });
375
427
  }
376
428
 
@@ -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