jekyll-theme-zer0 1.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +4 -4
- data/_includes/components/cookie-consent.html +2 -3
- data/_includes/components/info-section.html +9 -4
- data/_includes/core/footer.html +9 -8
- data/_includes/navigation/sidebar-left.html +0 -6
- data/_includes/obsidian/full-graph.html +170 -0
- data/_plugins/obsidian_links.rb +5 -0
- data/_plugins/preview_image_generator.rb +82 -25
- data/assets/data/wiki-index.json +44 -6
- data/assets/js/obsidian-graph.js +0 -1
- data/assets/js/obsidian-local-graph.js +0 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ae8ec5b12734cd267a46b1b5c26ed95f7d55b004e6dd9a52111a477f6bbc395
|
|
4
|
+
data.tar.gz: 84ff1429cdd6504bef5977c44ff88ff9078ffdb2dcd98ea39304c10f939daceb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c194515ed9cbf3590d46b6c887b9a27c94bbdfb4a5e841417ae759e25b59bd5bf0612846ce20927361daefa42b23459152336db6fb22874ce56b05c87d0203b
|
|
7
|
+
data.tar.gz: d0469bd560f3ddfc264b22e0fd4b7f7ffed2b9c1a7f524f3975b5a574a7a2ae0c5c8bf53c25db53eec1bb4201e21078f95a45de46e3dd742569171ca15d29421
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.1] - 2026-04-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Version bump: patch release
|
|
7
|
+
|
|
8
|
+
### Commits in this release
|
|
9
|
+
- 9830d3d refactor(obsidian): extract full graph include and sync docs (#82)
|
|
10
|
+
|
|
11
|
+
|
|
3
12
|
## [1.4.0] - 2026-04-25
|
|
4
13
|
|
|
5
14
|
### Changed
|
|
@@ -48,6 +57,9 @@
|
|
|
48
57
|
|
|
49
58
|
## [Unreleased]
|
|
50
59
|
|
|
60
|
+
### Changed
|
|
61
|
+
- **Docker/Jekyll build performance** — Reduced repeated full-page Liquid scans in the footer, settings offcanvas, and cookie consent includes; cached preview image checks during generation; skipped server-side Obsidian rewrites for documents without Obsidian syntax; and changed Docker dev startup to run `bundle install` only when `bundle check` reports missing dependencies. The profiled Docker build improved from 119.2s to 86.8s in local validation.
|
|
62
|
+
|
|
51
63
|
### Added
|
|
52
64
|
- **Development Automation**: Added `scripts/bin/validate` and `scripts/validate`
|
|
53
65
|
as the canonical preflight validation command for repository files, version
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: zer0-mistakes
|
|
3
3
|
sub-title: AI-Native Jekyll Theme
|
|
4
4
|
description: AI-native Jekyll theme for GitHub Pages — Docker-first development, AI-powered installation, multi-agent integration (Copilot, Codex, Cursor, Claude), AI preview-image generation, and AIEO content optimization with Bootstrap 5.3.
|
|
5
|
-
version: 1.4.
|
|
5
|
+
version: 1.4.1
|
|
6
6
|
layout: landing
|
|
7
7
|
tags:
|
|
8
8
|
- jekyll
|
|
@@ -20,7 +20,7 @@ categories:
|
|
|
20
20
|
- bootstrap
|
|
21
21
|
- ai-tooling
|
|
22
22
|
created: 2024-02-10T23:51:11.480Z
|
|
23
|
-
lastmod: 2026-04-
|
|
23
|
+
lastmod: 2026-04-28T21:04:09.000Z
|
|
24
24
|
draft: false
|
|
25
25
|
permalink: /
|
|
26
26
|
slug: zer0
|
|
@@ -1138,7 +1138,7 @@ git push origin feature/awesome-feature
|
|
|
1138
1138
|
|
|
1139
1139
|
| Metric | Value |
|
|
1140
1140
|
|--------|-------|
|
|
1141
|
-
| **Current Version** | 1.4.
|
|
1141
|
+
| **Current Version** | 1.4.1 ([RubyGems](https://rubygems.org/gems/jekyll-theme-zer0), [CHANGELOG](/CHANGELOG)) |
|
|
1142
1142
|
| **Documented Features** | 43 ([Feature Registry](https://github.com/bamr87/zer0-mistakes/blob/main/_data/features.yml)) |
|
|
1143
1143
|
| **Setup Time** | 2-5 minutes ([install.sh benchmarks](https://github.com/bamr87/zer0-mistakes/blob/main/install.sh)) |
|
|
1144
1144
|
| **Documentation Pages** | 70+ ([browse docs](/pages/)) |
|
|
@@ -1189,6 +1189,6 @@ And these AI partners that make zer0-mistakes truly AI-native:
|
|
|
1189
1189
|
|
|
1190
1190
|
**Built with ❤️ — and a little help from our AI partners — for the Jekyll community**
|
|
1191
1191
|
|
|
1192
|
-
**v1.4.
|
|
1192
|
+
**v1.4.1** • [Changelog](CHANGELOG.md) • [License](LICENSE) • [Contributing](CONTRIBUTING.md) • [AI Agent Guide](AGENTS.md)
|
|
1193
1193
|
|
|
1194
1194
|
|
|
@@ -59,9 +59,8 @@ Configuration: Uses site.posthog settings from _config.yml
|
|
|
59
59
|
</p>
|
|
60
60
|
<p class="mb-2 mb-lg-0 small text-white-50">
|
|
61
61
|
This website uses cookies and similar technologies to enhance your browsing experience, analyze traffic, and provide personalized content.
|
|
62
|
-
{% assign
|
|
63
|
-
{%
|
|
64
|
-
{% if _pp or site.privacy_policy_url %}
|
|
62
|
+
{% assign cookie_page_urls = site.html_pages | map: "url" | join: "|" | prepend: "|" | append: "|" %}
|
|
63
|
+
{% if site.privacy_policy_url or cookie_page_urls contains "|/privacy-policy/|" or cookie_page_urls contains "|/privacy-policy|" %}
|
|
65
64
|
<a href="{{ site.privacy_policy_url | default: '/privacy-policy/' | relative_url }}" class="text-white text-decoration-underline">Learn more in our Privacy Policy</a>.
|
|
66
65
|
{% endif %}
|
|
67
66
|
</p>
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
-->
|
|
23
23
|
|
|
24
24
|
{% include components/env-detect.html %}
|
|
25
|
+
{% assign info_section_page_urls = site.html_pages | map: "url" | join: "|" | prepend: "|" | append: "|" %}
|
|
25
26
|
<!-- Settings Offcanvas -->
|
|
26
27
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="info-section" aria-labelledby="infoSectionLabel">
|
|
27
28
|
|
|
@@ -111,9 +112,12 @@
|
|
|
111
112
|
<!-- Admin Quick Links — only render links to pages that actually exist
|
|
112
113
|
in the build (prevents 404s on bare-minimum / remote-theme sites
|
|
113
114
|
that haven't created the admin pages). -->
|
|
114
|
-
{% assign _cfg_page =
|
|
115
|
-
{% assign _theme_page =
|
|
116
|
-
{% assign _nav_page =
|
|
115
|
+
{% assign _cfg_page = false %}
|
|
116
|
+
{% assign _theme_page = false %}
|
|
117
|
+
{% assign _nav_page = false %}
|
|
118
|
+
{% if info_section_page_urls contains "|/about/config/|" %}{% assign _cfg_page = true %}{% endif %}
|
|
119
|
+
{% if info_section_page_urls contains "|/about/settings/theme/|" %}{% assign _theme_page = true %}{% endif %}
|
|
120
|
+
{% if info_section_page_urls contains "|/about/settings/navigation/|" %}{% assign _nav_page = true %}{% endif %}
|
|
117
121
|
{% if _cfg_page or _theme_page or _nav_page %}
|
|
118
122
|
<div class="mb-3">
|
|
119
123
|
<h6 class="text-body-secondary small text-uppercase fw-semibold mb-2">
|
|
@@ -145,7 +149,8 @@
|
|
|
145
149
|
{% include components/env-switcher.html %}
|
|
146
150
|
|
|
147
151
|
<!-- Admin Link — only when target page exists -->
|
|
148
|
-
{% assign _env_page =
|
|
152
|
+
{% assign _env_page = false %}
|
|
153
|
+
{% if info_section_page_urls contains "|/about/settings/environment/|" %}{% assign _env_page = true %}{% endif %}
|
|
149
154
|
{% if _env_page %}
|
|
150
155
|
<div class="mt-4 pt-3 border-top">
|
|
151
156
|
<a href="{{ '/about/settings/environment/' | relative_url }}" class="btn btn-outline-secondary btn-sm w-100">
|
data/_includes/core/footer.html
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
-->
|
|
32
32
|
|
|
33
33
|
<footer class="bd-footer border-top" role="contentinfo">
|
|
34
|
+
{% assign footer_page_urls = site.html_pages | map: "url" | join: "|" | prepend: "|" | append: "|" %}
|
|
34
35
|
<!-- Powered by Row -->
|
|
35
36
|
<div class="container-xl my-3">
|
|
36
37
|
<ul class="nav justify-content-end list-unstyled d-flex align-items-center flex-wrap" aria-label="Powered by technologies">
|
|
@@ -100,8 +101,8 @@
|
|
|
100
101
|
{% assign _parts = entry | split: "," %}
|
|
101
102
|
{% assign _label = _parts[0] %}
|
|
102
103
|
{% assign _url = _parts[1] %}
|
|
103
|
-
{% assign
|
|
104
|
-
{% if
|
|
104
|
+
{% assign _needle = "|" | append: _url | append: "|" %}
|
|
105
|
+
{% if footer_page_urls contains _needle %}
|
|
105
106
|
<li><a href="{{ _url | relative_url }}" class="text-light text-decoration-none">{{ _label }}</a></li>
|
|
106
107
|
{% endif %}
|
|
107
108
|
{% endfor %}
|
|
@@ -169,14 +170,14 @@
|
|
|
169
170
|
<div class="col-md-4 text-md-end">
|
|
170
171
|
<ul class="list-inline mb-0">
|
|
171
172
|
{% comment %} Only render policy links whose pages exist (or that are explicitly configured). {% endcomment %}
|
|
172
|
-
{% assign
|
|
173
|
-
{%
|
|
174
|
-
{% if
|
|
173
|
+
{% assign _privacy_needle = "|/privacy-policy/|" %}
|
|
174
|
+
{% assign _privacy_alt_needle = "|/privacy-policy|" %}
|
|
175
|
+
{% if site.privacy_policy_url or footer_page_urls contains _privacy_needle or footer_page_urls contains _privacy_alt_needle %}
|
|
175
176
|
<li class="list-inline-item"><a href="{{ site.privacy_policy_url | default: '/privacy-policy' | relative_url }}" class="text-light text-decoration-none">Privacy Policy</a></li>
|
|
176
177
|
{% endif %}
|
|
177
|
-
{% assign
|
|
178
|
-
{%
|
|
179
|
-
{% if
|
|
178
|
+
{% assign _terms_needle = "|/terms-of-service/|" %}
|
|
179
|
+
{% assign _terms_alt_needle = "|/terms-of-service|" %}
|
|
180
|
+
{% if site.terms_of_service_url or footer_page_urls contains _terms_needle or footer_page_urls contains _terms_alt_needle %}
|
|
180
181
|
<li class="list-inline-item"><a href="{{ site.terms_of_service_url | default: '/terms-of-service' | relative_url }}" class="text-light text-decoration-none">Terms of Service</a></li>
|
|
181
182
|
{% endif %}
|
|
182
183
|
<li class="list-inline-item">
|
|
@@ -48,12 +48,6 @@
|
|
|
48
48
|
<!-- Scrollable content area with dynamic navigation options -->
|
|
49
49
|
<div class="offcanvas-body overflow-auto">
|
|
50
50
|
|
|
51
|
-
<!-- ========================== -->
|
|
52
|
-
<!-- LOCAL GRAPH WIDGET -->
|
|
53
|
-
<!-- ========================== -->
|
|
54
|
-
<!-- Mini Obsidian-style local graph of the current page + neighbors -->
|
|
55
|
-
{% include navigation/local-graph.html %}
|
|
56
|
-
|
|
57
51
|
<!-- ========================== -->
|
|
58
52
|
<!-- AUTO MODE: Collection Docs -->
|
|
59
53
|
<!-- ========================== -->
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
/* Scoped to the graph page so we don't leak into other docs. */
|
|
3
|
+
#obsidian-graph {
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 82vh;
|
|
6
|
+
min-height: 620px;
|
|
7
|
+
border: 1px solid var(--bs-border-color, #dee2e6);
|
|
8
|
+
border-radius: var(--bs-border-radius-lg, .5rem);
|
|
9
|
+
background: var(--bs-tertiary-bg, #f8f9fa);
|
|
10
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 4px 12px rgba(0, 0, 0, .04);
|
|
11
|
+
position: relative;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
#obsidian-graph-stats {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-wrap: wrap;
|
|
17
|
+
gap: .375rem;
|
|
18
|
+
}
|
|
19
|
+
#obsidian-graph-stats .badge {
|
|
20
|
+
font-size: .8125rem;
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
padding: .35rem .6rem;
|
|
23
|
+
}
|
|
24
|
+
.obsidian-graph-toolbar {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-wrap: wrap;
|
|
27
|
+
gap: .5rem .75rem;
|
|
28
|
+
align-items: center;
|
|
29
|
+
margin: .75rem 0;
|
|
30
|
+
padding: .625rem .75rem;
|
|
31
|
+
background: var(--bs-tertiary-bg, #f8f9fa);
|
|
32
|
+
border: 1px solid var(--bs-border-color, #dee2e6);
|
|
33
|
+
border-radius: var(--bs-border-radius, .375rem);
|
|
34
|
+
}
|
|
35
|
+
.obsidian-graph-toolbar .form-control {
|
|
36
|
+
max-width: 280px;
|
|
37
|
+
flex: 1 1 200px;
|
|
38
|
+
}
|
|
39
|
+
.obsidian-graph-toolbar .form-check {
|
|
40
|
+
margin-bottom: 0;
|
|
41
|
+
}
|
|
42
|
+
#obsidian-graph-status {
|
|
43
|
+
flex-basis: 100%;
|
|
44
|
+
font-size: .8125rem;
|
|
45
|
+
color: var(--bs-secondary-color, #6c757d);
|
|
46
|
+
min-height: 1.25rem;
|
|
47
|
+
}
|
|
48
|
+
.obsidian-graph-legend {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
gap: .5rem;
|
|
52
|
+
margin-top: .75rem;
|
|
53
|
+
font-size: .8125rem;
|
|
54
|
+
}
|
|
55
|
+
.obsidian-graph-legend > span {
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: .375rem;
|
|
59
|
+
padding: .25rem .55rem;
|
|
60
|
+
background: var(--bs-body-bg, #fff);
|
|
61
|
+
border: 1px solid var(--bs-border-color, #dee2e6);
|
|
62
|
+
border-radius: 999px;
|
|
63
|
+
color: var(--bs-body-color, #212529);
|
|
64
|
+
}
|
|
65
|
+
.obsidian-graph-legend .swatch {
|
|
66
|
+
display: inline-block;
|
|
67
|
+
width: .7rem;
|
|
68
|
+
height: .7rem;
|
|
69
|
+
border-radius: 50%;
|
|
70
|
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
|
|
71
|
+
}
|
|
72
|
+
.obsidian-graph-legend .swatch-broken {
|
|
73
|
+
border: 1.5px dashed #dc3545;
|
|
74
|
+
background: transparent !important;
|
|
75
|
+
box-shadow: none;
|
|
76
|
+
}
|
|
77
|
+
.obsidian-graph-tips {
|
|
78
|
+
margin-top: .75rem;
|
|
79
|
+
padding: .625rem .75rem;
|
|
80
|
+
font-size: .8125rem;
|
|
81
|
+
color: var(--bs-secondary-color, #6c757d);
|
|
82
|
+
background: var(--bs-tertiary-bg, #f8f9fa);
|
|
83
|
+
border-left: 3px solid var(--bs-primary, #0d6efd);
|
|
84
|
+
border-radius: .25rem;
|
|
85
|
+
}
|
|
86
|
+
.obsidian-graph-tips strong {
|
|
87
|
+
color: var(--bs-body-color, #212529);
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
90
|
+
|
|
91
|
+
# Obsidian Graph View
|
|
92
|
+
|
|
93
|
+
A live, force-directed map of every page on this site and every
|
|
94
|
+
`[[wiki-link]]` between them. This is the rendered-site equivalent of
|
|
95
|
+
Obsidian's local graph view — built from the same
|
|
96
|
+
[`assets/data/wiki-index.json`]({{ "/assets/data/wiki-index.json" | relative_url }})
|
|
97
|
+
that powers the [client-side resolver]({{ "/docs/obsidian/syntax-reference/" | relative_url }})
|
|
98
|
+
and [backlinks panel]({{ "/docs/obsidian/syntax-reference/#backlinks-panel" | relative_url }}).
|
|
99
|
+
|
|
100
|
+
<div id="obsidian-graph-stats" class="mb-2" aria-live="polite"></div>
|
|
101
|
+
|
|
102
|
+
<div class="obsidian-graph-toolbar">
|
|
103
|
+
<input type="search"
|
|
104
|
+
class="form-control form-control-sm"
|
|
105
|
+
id="obsidian-graph-search"
|
|
106
|
+
placeholder="Filter nodes by title…"
|
|
107
|
+
aria-label="Filter graph nodes by title" />
|
|
108
|
+
<button type="button"
|
|
109
|
+
class="btn btn-outline-secondary btn-sm"
|
|
110
|
+
id="obsidian-graph-fit">
|
|
111
|
+
<i class="bi bi-arrows-fullscreen" aria-hidden="true"></i>
|
|
112
|
+
Reset view
|
|
113
|
+
</button>
|
|
114
|
+
<div class="form-check form-switch">
|
|
115
|
+
<input class="form-check-input"
|
|
116
|
+
type="checkbox"
|
|
117
|
+
role="switch"
|
|
118
|
+
id="obsidian-graph-orphans" />
|
|
119
|
+
<label class="form-check-label" for="obsidian-graph-orphans">
|
|
120
|
+
Show orphans
|
|
121
|
+
</label>
|
|
122
|
+
</div>
|
|
123
|
+
<span id="obsidian-graph-status" role="status"></span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div id="obsidian-graph" role="img" aria-label="Site knowledge graph"></div>
|
|
127
|
+
|
|
128
|
+
<div class="obsidian-graph-legend" aria-label="Graph legend">
|
|
129
|
+
<span><span class="swatch" style="background:#0d6efd"></span>Posts</span>
|
|
130
|
+
<span><span class="swatch" style="background:#198754"></span>Docs</span>
|
|
131
|
+
<span><span class="swatch" style="background:#6f42c1"></span>Notes</span>
|
|
132
|
+
<span><span class="swatch" style="background:#d63384"></span>Notebooks</span>
|
|
133
|
+
<span><span class="swatch" style="background:#fd7e14"></span>Quickstart</span>
|
|
134
|
+
<span><span class="swatch" style="background:#6c757d"></span>Pages</span>
|
|
135
|
+
<span><span class="swatch swatch-broken"></span>Broken links</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="obsidian-graph-tips">
|
|
139
|
+
<strong>Tips:</strong> click a node to open the page · ⌘/Ctrl-click to
|
|
140
|
+
open in a new tab · drag to reposition · scroll to zoom · hover to
|
|
141
|
+
highlight a node's neighborhood · type in the search box to filter.
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
## How it's built
|
|
145
|
+
|
|
146
|
+
| Piece | File |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| Build-time index (nodes + outgoing edges) | [`assets/data/wiki-index.json`]({{ "/assets/data/wiki-index.json" | relative_url }}) (Liquid template at [`assets/data/wiki-index.json`](https://github.com/bamr87/zer0-mistakes/blob/main/assets/data/wiki-index.json)) |
|
|
149
|
+
| Renderer | `assets/js/obsidian-graph.js` |
|
|
150
|
+
| Layout engine | [cytoscape.js](https://js.cytoscape.org/) (loaded from CDN, only on this page) |
|
|
151
|
+
|
|
152
|
+
Outgoing edges come from the same `[[…]]` syntax the resolver handles —
|
|
153
|
+
unresolved targets show up as dashed red nodes so you can find dangling
|
|
154
|
+
links at a glance. The graph is regenerated every Jekyll build; nothing
|
|
155
|
+
runs client-side except cytoscape's force layout.
|
|
156
|
+
|
|
157
|
+
<!-- Cytoscape.js (only loaded on this page). -->
|
|
158
|
+
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.30.0/dist/cytoscape.min.js"
|
|
159
|
+
integrity="sha384-kpMsYllYzyaWU69Piok08rPNktpnjqAoDMdB00fjqUkEk3lkuUbSuwJ+oXrjvN6B"
|
|
160
|
+
crossorigin="anonymous"
|
|
161
|
+
defer></script>
|
|
162
|
+
<script src="{{ '/assets/js/obsidian-graph.js' | relative_url }}" defer></script>
|
|
163
|
+
|
|
164
|
+
## See also
|
|
165
|
+
|
|
166
|
+
- [[Obsidian Vault Integration]]
|
|
167
|
+
- [[Obsidian Syntax Reference]]
|
|
168
|
+
- [[Obsidian Authoring Workflow]]
|
|
169
|
+
- [[Getting Started with the Obsidian Vault]]
|
|
170
|
+
- [[Obsidian Integration Troubleshooting]]
|
data/_plugins/obsidian_links.rb
CHANGED
|
@@ -416,10 +416,15 @@ module Jekyll
|
|
|
416
416
|
|
|
417
417
|
def rewrite_document(doc)
|
|
418
418
|
return unless index
|
|
419
|
+
return unless needs_rewrite?(doc.content)
|
|
419
420
|
|
|
420
421
|
converter = Converter.new(nil, index, config)
|
|
421
422
|
doc.content = converter.convert(doc.content, current_url: doc.url)
|
|
422
423
|
end
|
|
424
|
+
|
|
425
|
+
def needs_rewrite?(content)
|
|
426
|
+
content.include?('[[') || content.include?('> [!') || content.match?(Converter::INLINE_TAG_RE)
|
|
427
|
+
end
|
|
423
428
|
end
|
|
424
429
|
end
|
|
425
430
|
end
|
|
@@ -47,6 +47,9 @@ module Jekyll
|
|
|
47
47
|
|
|
48
48
|
# Check if a document has a preview image defined
|
|
49
49
|
def self.has_preview?(doc)
|
|
50
|
+
cached = cached_preview_entry(doc)
|
|
51
|
+
return cached['has_preview'] unless cached.nil?
|
|
52
|
+
|
|
50
53
|
preview = doc.data['preview']
|
|
51
54
|
return false if preview.nil? || preview.to_s.strip.empty?
|
|
52
55
|
|
|
@@ -95,6 +98,9 @@ module Jekyll
|
|
|
95
98
|
|
|
96
99
|
# Get the preview image path for a document
|
|
97
100
|
def self.preview_path(doc)
|
|
101
|
+
cached = cached_preview_entry(doc)
|
|
102
|
+
return cached['path'] unless cached.nil?
|
|
103
|
+
|
|
98
104
|
preview = doc.data['preview']
|
|
99
105
|
return nil if preview.nil? || preview.to_s.strip.empty?
|
|
100
106
|
|
|
@@ -113,36 +119,86 @@ module Jekyll
|
|
|
113
119
|
|
|
114
120
|
# Get list of documents missing preview images
|
|
115
121
|
def self.missing_previews(site)
|
|
122
|
+
build_index(site)['missing']
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.build_index(site)
|
|
126
|
+
cached = site.data['preview_image_index']
|
|
127
|
+
return cached if cached
|
|
128
|
+
|
|
116
129
|
config = self.config(site)
|
|
130
|
+
index = {}
|
|
117
131
|
missing = []
|
|
118
|
-
|
|
119
|
-
config
|
|
132
|
+
|
|
133
|
+
preview_documents(site, config).each do |doc, collection_name|
|
|
134
|
+
key = preview_cache_key(doc)
|
|
135
|
+
next if index.key?(key)
|
|
136
|
+
|
|
137
|
+
preview = doc.data['preview']
|
|
138
|
+
normalized_path = nil
|
|
139
|
+
has_preview = false
|
|
140
|
+
|
|
141
|
+
if preview && !preview.to_s.strip.empty? && (preview.match?(/\.(png|jpe?g|gif|svg|webp)$/i) || preview.start_with?('http'))
|
|
142
|
+
normalized_path = preview.start_with?('http') ? preview : normalize_preview_path(preview, config)
|
|
143
|
+
has_preview = preview.start_with?('http') || preview_file_exists?(site, normalized_path, config)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
index[key] = {
|
|
147
|
+
'has_preview' => has_preview,
|
|
148
|
+
'path' => normalized_path,
|
|
149
|
+
'collection' => collection_name
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
next if has_preview
|
|
153
|
+
|
|
154
|
+
missing << {
|
|
155
|
+
'path' => doc.relative_path,
|
|
156
|
+
'title' => doc.data['title'] || File.basename(doc.relative_path),
|
|
157
|
+
'collection' => collection_name
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
site.data['preview_image_index'] = {
|
|
162
|
+
'documents' => index,
|
|
163
|
+
'missing' => missing
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.cached_preview_entry(doc)
|
|
168
|
+
return nil unless doc.respond_to?(:site) && doc.site && doc.site.data['preview_image_index']
|
|
169
|
+
|
|
170
|
+
doc.site.data['preview_image_index']['documents'][preview_cache_key(doc)]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.preview_documents(site, config)
|
|
174
|
+
docs = []
|
|
175
|
+
Array(config['collections']).each do |collection_name|
|
|
120
176
|
collection = site.collections[collection_name]
|
|
121
177
|
next unless collection
|
|
122
|
-
|
|
123
|
-
collection.docs.each
|
|
124
|
-
unless has_preview?(doc)
|
|
125
|
-
missing << {
|
|
126
|
-
'path' => doc.relative_path,
|
|
127
|
-
'title' => doc.data['title'] || File.basename(doc.relative_path),
|
|
128
|
-
'collection' => collection_name
|
|
129
|
-
}
|
|
130
|
-
end
|
|
131
|
-
end
|
|
178
|
+
|
|
179
|
+
collection.docs.each { |doc| docs << [doc, collection_name] }
|
|
132
180
|
end
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
181
|
+
|
|
182
|
+
site.posts.docs.each { |doc| docs << [doc, 'posts'] } if site.respond_to?(:posts) && site.posts
|
|
183
|
+
docs
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.preview_cache_key(doc)
|
|
187
|
+
doc.respond_to?(:relative_path) ? doc.relative_path : doc.path
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.preview_file_exists?(site, normalized_preview, config)
|
|
191
|
+
candidates = []
|
|
192
|
+
if normalized_preview.start_with?('/')
|
|
193
|
+
candidates << File.join(site.source, normalized_preview.sub(/^\//, ''))
|
|
194
|
+
candidates << File.join(site.source, 'assets', normalized_preview.sub(/^\//, ''))
|
|
195
|
+
else
|
|
196
|
+
candidates << File.join(site.source, 'assets', normalized_preview)
|
|
197
|
+
candidates << File.join(site.source, normalized_preview)
|
|
198
|
+
candidates << File.join(site.source, config['output_dir'], normalized_preview)
|
|
143
199
|
end
|
|
144
|
-
|
|
145
|
-
|
|
200
|
+
|
|
201
|
+
candidates.any? { |path| File.exist?(path) }
|
|
146
202
|
end
|
|
147
203
|
|
|
148
204
|
# Generate a preview image filename from document
|
|
@@ -264,7 +320,8 @@ module Jekyll
|
|
|
264
320
|
return unless config['enabled']
|
|
265
321
|
|
|
266
322
|
# Store missing previews in site data for access in templates
|
|
267
|
-
|
|
323
|
+
preview_index = PreviewImageGenerator.build_index(site)
|
|
324
|
+
site.data['preview_images_missing'] = preview_index['missing']
|
|
268
325
|
site.data['preview_images_config'] = config
|
|
269
326
|
|
|
270
327
|
# Log status
|
data/assets/data/wiki-index.json
CHANGED
|
@@ -36,14 +36,38 @@ sitemap: false
|
|
|
36
36
|
"entries": [
|
|
37
37
|
{%- for doc in all_docs -%}
|
|
38
38
|
{%- assign basename = doc.path | split: "/" | last | replace: ".md", "" | replace: ".markdown", "" | replace: ".html", "" -%}
|
|
39
|
-
{%- assign
|
|
39
|
+
{%- assign body_source = doc.content | default: "" -%}
|
|
40
|
+
{%- assign body_without_fences = "" -%}
|
|
41
|
+
{%- assign fence_parts = body_source | split: "```" -%}
|
|
42
|
+
{%- for fence_part in fence_parts -%}
|
|
43
|
+
{%- assign fence_mod = forloop.index0 | modulo: 2 -%}
|
|
44
|
+
{%- if fence_mod == 0 -%}
|
|
45
|
+
{%- assign body_without_fences = body_without_fences | append: fence_part -%}
|
|
46
|
+
{%- endif -%}
|
|
47
|
+
{%- endfor -%}
|
|
48
|
+
{%- assign body_without_code = "" -%}
|
|
49
|
+
{%- assign inline_code_parts = body_without_fences | split: "`" -%}
|
|
50
|
+
{%- for inline_code_part in inline_code_parts -%}
|
|
51
|
+
{%- assign inline_code_mod = forloop.index0 | modulo: 2 -%}
|
|
52
|
+
{%- if inline_code_mod == 0 -%}
|
|
53
|
+
{%- assign body_without_code = body_without_code | append: inline_code_part -%}
|
|
54
|
+
{%- endif -%}
|
|
55
|
+
{%- endfor -%}
|
|
56
|
+
{%- assign body = body_without_code | strip_html -%}
|
|
40
57
|
{%- assign body_excerpt = body | strip_newlines | truncate: 240 -%}
|
|
58
|
+
{%- assign aliases = "" | split: "," -%}
|
|
59
|
+
{%- if doc.aliases -%}
|
|
60
|
+
{%- assign aliases_blob = doc.aliases | join: "||" -%}
|
|
61
|
+
{%- assign aliases = aliases_blob | split: "||" -%}
|
|
62
|
+
{%- endif -%}
|
|
41
63
|
{%- comment -%}
|
|
42
64
|
Extract outgoing wiki-link targets so the graph view
|
|
43
65
|
(assets/js/obsidian-graph.js) can render edges without re-parsing
|
|
44
|
-
every page client-side.
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
every page client-side. Fenced and inline code are removed first so
|
|
67
|
+
Bash `[[ ... ]]` tests and literal examples don't become graph edges.
|
|
68
|
+
We mask `![[…]]` embeds so they don't also get split as `[[…]]`, then
|
|
69
|
+
strip optional `|alias` and `#anchor` / `^block` parts. Operator-heavy
|
|
70
|
+
targets are discarded as table examples rather than Obsidian page names.
|
|
47
71
|
{%- endcomment -%}
|
|
48
72
|
{%- assign masked = body | replace: "![[", "@@OBSEMBED@@" -%}
|
|
49
73
|
{%- assign chunks = masked | split: "[[" -%}
|
|
@@ -52,7 +76,21 @@ sitemap: false
|
|
|
52
76
|
{%- assign target_raw = chunk | split: "]]" | first -%}
|
|
53
77
|
{%- if target_raw and target_raw != "" -%}
|
|
54
78
|
{%- assign target = target_raw | split: "|" | first | split: "#" | first | split: "^" | first | strip | downcase -%}
|
|
55
|
-
{%-
|
|
79
|
+
{%- assign target_is_code = false -%}
|
|
80
|
+
{%- assign first_char = target | slice: 0 -%}
|
|
81
|
+
{%- if target contains "$" or target contains "==" or target contains "!=" or target contains "=~" -%}
|
|
82
|
+
{%- assign target_is_code = true -%}
|
|
83
|
+
{%- endif -%}
|
|
84
|
+
{%- if target contains "&&" or target contains "||" or target contains "&&" or target contains "<" or target contains ">" -%}
|
|
85
|
+
{%- assign target_is_code = true -%}
|
|
86
|
+
{%- endif -%}
|
|
87
|
+
{%- if target contains " -eq " or target contains " -ne " or target contains " -lt " or target contains " -le " or target contains " -gt " or target contains " -ge " -%}
|
|
88
|
+
{%- assign target_is_code = true -%}
|
|
89
|
+
{%- endif -%}
|
|
90
|
+
{%- if first_char == "-" or target contains "{" or target contains "}" or target contains "(" or target contains ")" or target contains '"' or target contains "'" -%}
|
|
91
|
+
{%- assign target_is_code = true -%}
|
|
92
|
+
{%- endif -%}
|
|
93
|
+
{%- if target != "" and target.size < 200 and target_is_code == false -%}
|
|
56
94
|
{%- assign outgoing = outgoing | push: target -%}
|
|
57
95
|
{%- endif -%}
|
|
58
96
|
{%- endif -%}
|
|
@@ -65,7 +103,7 @@ sitemap: false
|
|
|
65
103
|
"collection": {{ doc.collection | default: nil | jsonify }},
|
|
66
104
|
"tags": {{ doc.tags | default: empty | jsonify }},
|
|
67
105
|
"categories": {{ doc.categories | default: empty | jsonify }},
|
|
68
|
-
"aliases": {{
|
|
106
|
+
"aliases": {{ aliases | jsonify }},
|
|
69
107
|
"outgoing": {{ outgoing | jsonify }},
|
|
70
108
|
"excerpt": {{ body_excerpt | jsonify }}
|
|
71
109
|
}{% unless forloop.last %},{% endunless %}
|
data/assets/js/obsidian-graph.js
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-theme-zer0
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amr Abdel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -169,6 +169,7 @@ files:
|
|
|
169
169
|
- _includes/navigation/sidebar-folders.html
|
|
170
170
|
- _includes/navigation/sidebar-left.html
|
|
171
171
|
- _includes/navigation/sidebar-right.html
|
|
172
|
+
- _includes/obsidian/full-graph.html
|
|
172
173
|
- _includes/search-data.json
|
|
173
174
|
- _includes/setup/wizard.html
|
|
174
175
|
- _includes/stats/README.md
|