jekyll-theme-zer0 1.2.1 → 1.3.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.
@@ -0,0 +1,341 @@
1
+ /*
2
+ * obsidian-wiki-links.js
3
+ *
4
+ * Client-side fallback that resolves Obsidian-style wiki-links / embeds /
5
+ * inline tags on the rendered page. Used when the Jekyll plugin
6
+ * (_plugins/obsidian_links.rb) is NOT available — most importantly the
7
+ * default GitHub Pages remote_theme build, which only allows whitelisted
8
+ * Jekyll plugins.
9
+ *
10
+ * The plugin (when it runs) emits ready HTML and this script becomes a
11
+ * no-op for already-rewritten content. When the plugin is absent, the raw
12
+ * `[[Page]]` / `![[image.png]]` syntax survives kramdown and we rewrite it
13
+ * here in the DOM using assets/data/wiki-index.json.
14
+ *
15
+ * Lookup keys are normalized identically to the Ruby plugin:
16
+ * value.toLowerCase().trim().replace(/\s+/g, ' ')
17
+ *
18
+ * Loaded from _includes/components/js-cdn.html (deferred). Skips when
19
+ * `window.__OBSIDIAN_DISABLE_CLIENT__` is true (set this in dev to debug
20
+ * the server-side plugin output).
21
+ */
22
+ (function () {
23
+ 'use strict';
24
+
25
+ if (window.__OBSIDIAN_DISABLE_CLIENT__) {
26
+ return;
27
+ }
28
+
29
+ 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/',
34
+ wikiLinkClass: 'wiki-link',
35
+ brokenLinkClass: 'wiki-link wiki-link-broken'
36
+ };
37
+
38
+ var IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.bmp'];
39
+
40
+ // Patterns kept conservative: we only mutate text nodes inside the main
41
+ // content container so navigation chrome / code samples stay untouched.
42
+ var EMBED_RE = /!\[\[([^\]\n|]+?)(?:\|([^\]\n]+))?\]\]/g;
43
+ var LINK_RE = /\[\[([^\]\n|]+?)(?:\|([^\]\n]+))?\]\]/g;
44
+ var TAG_RE = /(^|[^\w/#&])#([A-Za-z][\w/-]{0,63})/g;
45
+
46
+ function normalize(value) {
47
+ return String(value || '').toLowerCase().trim().replace(/\s+/g, ' ');
48
+ }
49
+
50
+ function escapeHtml(value) {
51
+ return String(value == null ? '' : value)
52
+ .replace(/&/g, '&')
53
+ .replace(/</g, '&lt;')
54
+ .replace(/>/g, '&gt;')
55
+ .replace(/"/g, '&quot;')
56
+ .replace(/'/g, '&#39;');
57
+ }
58
+
59
+ function splitAnchor(target) {
60
+ var blockMatch = target.match(/^(.+?)\^([\w-]+)$/);
61
+ if (blockMatch) return { page: blockMatch[1].trim(), anchor: blockMatch[2].trim() };
62
+ var headingMatch = target.match(/^(.+?)#(.+)$/);
63
+ if (headingMatch) return { page: headingMatch[1].trim(), anchor: headingMatch[2].trim() };
64
+ return { page: target.trim(), anchor: null };
65
+ }
66
+
67
+ function buildIndex(payload) {
68
+ var byKey = Object.create(null);
69
+ if (!payload || !Array.isArray(payload.entries)) return byKey;
70
+
71
+ payload.entries.forEach(function (entry) {
72
+ if (!entry || !entry.url) return;
73
+ var keys = [];
74
+ if (entry.title) keys.push(entry.title);
75
+ if (entry.basename) keys.push(entry.basename);
76
+ (entry.aliases || []).forEach(function (a) { if (a) keys.push(a); });
77
+ keys.forEach(function (k) {
78
+ var nk = normalize(k);
79
+ if (!nk || byKey[nk]) return; // first wins, mirrors plugin behaviour
80
+ byKey[nk] = entry;
81
+ });
82
+ });
83
+ return byKey;
84
+ }
85
+
86
+ function renderImageEmbed(target, modifier) {
87
+ var widthAttr = '';
88
+ var alt = target;
89
+ if (modifier) {
90
+ if (/^\d+$/.test(modifier)) widthAttr = ' width="' + modifier + '"';
91
+ else alt = modifier;
92
+ }
93
+ var src = target.charAt(0) === '/' ? target : (CONFIG.attachmentsPath.replace(/\/$/, '') + '/' + target);
94
+ return '<img src="' + escapeHtml(src) + '" alt="' + escapeHtml(alt) +
95
+ '" loading="lazy" class="obsidian-embed obsidian-embed-image"' + widthAttr + ' />';
96
+ }
97
+
98
+ function renderNoteEmbed(target, byKey) {
99
+ var parts = splitAnchor(target);
100
+ var info = byKey[normalize(parts.page)];
101
+ if (!info) {
102
+ return '<div class="obsidian-embed obsidian-embed-broken alert alert-warning" role="alert">' +
103
+ 'Embed not found: <code>' + escapeHtml(target) + '</code></div>';
104
+ }
105
+ var url = info.url + (parts.anchor ? '#' + parts.anchor.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-') : '');
106
+ return '<div class="obsidian-embed obsidian-embed-note">' +
107
+ '<div class="obsidian-embed-header"><a href="' + escapeHtml(url) + '">' +
108
+ escapeHtml(info.title || parts.page) + '</a></div>' +
109
+ '<div class="obsidian-embed-excerpt">' + escapeHtml(info.excerpt || '') + '</div>' +
110
+ '</div>';
111
+ }
112
+
113
+ function renderWikiLink(target, aliasText, byKey, currentUrl) {
114
+ var parts = splitAnchor(target);
115
+ var display = aliasText || (parts.anchor ? parts.page + ' \u203A ' + parts.anchor : parts.page);
116
+ var info = byKey[normalize(parts.page)];
117
+ if (!info) {
118
+ return '<a href="#" class="' + CONFIG.brokenLinkClass +
119
+ '" data-wiki-target="' + escapeHtml(parts.page) +
120
+ '" title="Unresolved wiki-link: ' + escapeHtml(parts.page) + '">' +
121
+ escapeHtml(display) + '</a>';
122
+ }
123
+ var url = info.url + (parts.anchor ? '#' + parts.anchor.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-') : '');
124
+ var currentAttr = currentUrl && info.url === currentUrl ? ' aria-current="page"' : '';
125
+ return '<a href="' + escapeHtml(url) + '" class="' + CONFIG.wikiLinkClass +
126
+ '" data-wiki-target="' + escapeHtml(parts.page) + '"' + currentAttr + '>' +
127
+ escapeHtml(display) + '</a>';
128
+ }
129
+
130
+ function rewriteHtml(html, byKey, currentUrl) {
131
+ // Embeds first (longer match `![[ … ]]` would otherwise be eaten by [[ … ]]).
132
+ html = html.replace(EMBED_RE, function (_match, target, modifier) {
133
+ target = target.trim();
134
+ modifier = (modifier || '').trim();
135
+ var ext = (target.match(/\.[A-Za-z0-9]+$/) || [''])[0].toLowerCase();
136
+ if (IMAGE_EXTENSIONS.indexOf(ext) !== -1) return renderImageEmbed(target, modifier);
137
+ return renderNoteEmbed(target, byKey);
138
+ });
139
+ html = html.replace(LINK_RE, function (_match, target, alias) {
140
+ return renderWikiLink(target.trim(), (alias || '').trim(), byKey, currentUrl);
141
+ });
142
+ html = html.replace(TAG_RE, function (_match, lead, tag) {
143
+ var url = (CONFIG.tagBase.replace(/\/$/, '') + '/#' + tag.toLowerCase().replace(/\//g, '-'));
144
+ return lead + '<a href="' + escapeHtml(url) + '" class="obsidian-tag">#' + escapeHtml(tag) + '</a>';
145
+ });
146
+ return html;
147
+ }
148
+
149
+ function eligibleNode(node) {
150
+ // Only walk text nodes whose parent isn't code / pre / a / script / style
151
+ // and that contain at least one of our markers.
152
+ if (node.nodeType !== Node.TEXT_NODE) return false;
153
+ var parent = node.parentNode;
154
+ while (parent && parent !== document.body) {
155
+ var tag = parent.nodeName;
156
+ if (tag === 'CODE' || tag === 'PRE' || tag === 'A' || tag === 'SCRIPT' || tag === 'STYLE') return false;
157
+ if (parent.classList && (parent.classList.contains('mermaid') || parent.classList.contains('obsidian-embed'))) return false;
158
+ parent = parent.parentNode;
159
+ }
160
+ var text = node.nodeValue;
161
+ return text && (text.indexOf('[[') !== -1 || text.indexOf('![[') !== -1 || /(^|[^\w/#&])#[A-Za-z]/.test(text));
162
+ }
163
+
164
+ function rewriteContainer(container, byKey, currentUrl) {
165
+ if (!container) return 0;
166
+ var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
167
+ var batch = [];
168
+ var node;
169
+ while ((node = walker.nextNode())) {
170
+ if (eligibleNode(node)) batch.push(node);
171
+ }
172
+ var rewrites = 0;
173
+ batch.forEach(function (textNode) {
174
+ var original = textNode.nodeValue;
175
+ var rewritten = rewriteHtml(original, byKey, currentUrl);
176
+ if (rewritten === original) return;
177
+
178
+ // The rewriter only inserts HTML for matched markers; everything else
179
+ // is the raw text-node content. Wrap the bits between matches in
180
+ // escaped fragments so injected `<` / `&` from the source stays inert.
181
+ var safe = '';
182
+ var lastIndex = 0;
183
+ var combined = /(!\[\[[^\]\n|]+(?:\|[^\]\n]+)?\]\])|(\[\[[^\]\n|]+(?:\|[^\]\n]+)?\]\])|((?:^|[^\w/#&])#[A-Za-z][\w/-]{0,63})/g;
184
+ var match;
185
+ while ((match = combined.exec(original)) !== null) {
186
+ safe += escapeHtml(original.slice(lastIndex, match.index));
187
+ safe += rewriteHtml(match[0], byKey, currentUrl);
188
+ lastIndex = combined.lastIndex;
189
+ }
190
+ safe += escapeHtml(original.slice(lastIndex));
191
+
192
+ var template = document.createElement('template');
193
+ template.innerHTML = safe;
194
+ textNode.parentNode.replaceChild(template.content, textNode);
195
+ rewrites += 1;
196
+ });
197
+ return rewrites;
198
+ }
199
+
200
+ function getCurrentUrl() {
201
+ return window.location.pathname;
202
+ }
203
+
204
+ // ---- Callouts (DOM-level, post-kramdown) -----------------------------
205
+ // Kramdown turns `> [!type] Title\n> body` into:
206
+ // <blockquote><p>[!type] Title\nbody</p></blockquote>
207
+ // We detect that pattern and rewrite the blockquote into a Bootstrap alert.
208
+ var CALLOUT_TYPES = {
209
+ note: { alert: 'primary', icon: 'bi-pencil-square' },
210
+ abstract: { alert: 'secondary', icon: 'bi-card-text' },
211
+ summary: { alert: 'secondary', icon: 'bi-card-text' },
212
+ tldr: { alert: 'secondary', icon: 'bi-card-text' },
213
+ info: { alert: 'info', icon: 'bi-info-circle' },
214
+ todo: { alert: 'info', icon: 'bi-check2-square' },
215
+ tip: { alert: 'success', icon: 'bi-lightbulb' },
216
+ hint: { alert: 'success', icon: 'bi-lightbulb' },
217
+ important: { alert: 'warning', icon: 'bi-exclamation-circle' },
218
+ success: { alert: 'success', icon: 'bi-check-circle' },
219
+ check: { alert: 'success', icon: 'bi-check-circle' },
220
+ done: { alert: 'success', icon: 'bi-check-circle' },
221
+ question: { alert: 'info', icon: 'bi-question-circle' },
222
+ help: { alert: 'info', icon: 'bi-question-circle' },
223
+ faq: { alert: 'info', icon: 'bi-question-circle' },
224
+ warning: { alert: 'warning', icon: 'bi-exclamation-triangle' },
225
+ caution: { alert: 'warning', icon: 'bi-exclamation-triangle' },
226
+ attention: { alert: 'warning', icon: 'bi-exclamation-triangle' },
227
+ failure: { alert: 'danger', icon: 'bi-x-octagon' },
228
+ fail: { alert: 'danger', icon: 'bi-x-octagon' },
229
+ missing: { alert: 'danger', icon: 'bi-x-octagon' },
230
+ danger: { alert: 'danger', icon: 'bi-shield-exclamation' },
231
+ error: { alert: 'danger', icon: 'bi-shield-exclamation' },
232
+ bug: { alert: 'danger', icon: 'bi-bug' },
233
+ example: { alert: 'secondary', icon: 'bi-code-slash' },
234
+ quote: { alert: 'secondary', icon: 'bi-chat-quote' },
235
+ cite: { alert: 'secondary', icon: 'bi-chat-quote' }
236
+ };
237
+
238
+ // Match the first line of the first <p> inside a blockquote, e.g.
239
+ // `[!warning]+ Foldable warning`.
240
+ var CALLOUT_HEAD_RE = /^\s*\[!([A-Za-z]+)\]([+-]?)\s*([^\n]*)/;
241
+
242
+ function rewriteCallouts(container) {
243
+ if (!container) return 0;
244
+ var quotes = container.querySelectorAll('blockquote');
245
+ var count = 0;
246
+ quotes.forEach(function (bq) {
247
+ if (bq.dataset.obsidianCallout) return; // already processed
248
+ var firstChild = bq.firstElementChild;
249
+ // Walk past whitespace text nodes
250
+ while (firstChild && firstChild.nodeName !== 'P' && firstChild.nodeType !== 1) {
251
+ firstChild = firstChild.nextElementSibling;
252
+ }
253
+ if (!firstChild || firstChild.nodeName !== 'P') return;
254
+
255
+ var rawText = firstChild.textContent || '';
256
+ var m = rawText.match(CALLOUT_HEAD_RE);
257
+ if (!m) return;
258
+
259
+ var type = m[1].toLowerCase();
260
+ var spec = CALLOUT_TYPES[type] || CALLOUT_TYPES.note;
261
+ var fold = m[2];
262
+ var titleText = (m[3] || '').trim() || (type.charAt(0).toUpperCase() + type.slice(1));
263
+
264
+ // Strip the "[!type]…" head from the first paragraph (keep any trailing text)
265
+ var headLength = m[0].length;
266
+ var trailing = rawText.slice(headLength).replace(/^\s*\n?/, '');
267
+ if (trailing) {
268
+ firstChild.textContent = trailing;
269
+ } else {
270
+ firstChild.remove();
271
+ }
272
+
273
+ var wrapper = document.createElement('div');
274
+ wrapper.className = 'alert alert-' + spec.alert + ' obsidian-callout obsidian-callout-' + type;
275
+ wrapper.setAttribute('role', 'alert');
276
+ wrapper.dataset.obsidianCallout = type;
277
+ if (fold === '-') wrapper.dataset.collapsed = 'true';
278
+
279
+ var titleEl = document.createElement('div');
280
+ titleEl.className = 'obsidian-callout-title';
281
+ titleEl.innerHTML = '<i class="bi ' + spec.icon + ' me-2" aria-hidden="true"></i>' + escapeHtml(titleText);
282
+ wrapper.appendChild(titleEl);
283
+
284
+ var bodyEl = document.createElement('div');
285
+ bodyEl.className = 'obsidian-callout-body';
286
+ // Move blockquote children into the body, preserving inner HTML
287
+ while (bq.firstChild) {
288
+ bodyEl.appendChild(bq.firstChild);
289
+ }
290
+ wrapper.appendChild(bodyEl);
291
+
292
+ bq.parentNode.replaceChild(wrapper, bq);
293
+ count += 1;
294
+ });
295
+ return count;
296
+ }
297
+
298
+ function init() {
299
+ var container = document.querySelector('#main-content, .bd-content, main, article') || document.body;
300
+ if (!container) return;
301
+
302
+ fetch(CONFIG.indexUrl, { credentials: 'same-origin', cache: 'force-cache' })
303
+ .then(function (r) { return r.ok ? r.json() : null; })
304
+ .then(function (payload) {
305
+ if (!payload) return;
306
+ var byKey = buildIndex(payload);
307
+ window.__OBSIDIAN_INDEX__ = byKey;
308
+ var rewrites = rewriteContainer(container, byKey, getCurrentUrl());
309
+ var calloutCount = rewriteCallouts(container);
310
+ if ((rewrites > 0 || calloutCount > 0) && window.console && console.debug) {
311
+ console.debug('[obsidian] rewrote', rewrites, 'text nodes,', calloutCount, 'callouts');
312
+ }
313
+ document.dispatchEvent(new CustomEvent('obsidian:ready', { detail: { count: payload.count || 0, calloutCount: calloutCount } }));
314
+ })
315
+ .catch(function (err) {
316
+ // Even if the index fails, we can still convert callouts (no index needed).
317
+ try {
318
+ var calloutCount = rewriteCallouts(container);
319
+ if (calloutCount > 0 && window.console && console.debug) {
320
+ console.debug('[obsidian] rewrote', calloutCount, 'callouts (index unavailable)');
321
+ }
322
+ } catch (e) { /* swallow */ }
323
+ if (window.console && console.warn) console.warn('[obsidian] wiki-index fetch failed:', err);
324
+ });
325
+ }
326
+
327
+ // Expose for testing / programmatic use
328
+ window.ObsidianResolver = {
329
+ rewriteHtml: rewriteHtml,
330
+ rewriteCallouts: rewriteCallouts,
331
+ rewriteContainer: rewriteContainer,
332
+ buildIndex: buildIndex,
333
+ normalize: normalize
334
+ };
335
+
336
+ if (document.readyState === 'loading') {
337
+ document.addEventListener('DOMContentLoaded', init);
338
+ } else {
339
+ init();
340
+ }
341
+ })();
data/scripts/lint-pages CHANGED
@@ -400,6 +400,12 @@ scan_collection() {
400
400
 
401
401
  if [[ -d "$REPO_ROOT/$dir_part" ]]; then
402
402
  while IFS= read -r -d '' filepath; do
403
+ # Skip Obsidian/Jekyll template directories — these contain
404
+ # placeholder front matter ({{title}}, {{date}}) that is never
405
+ # rendered as a real page.
406
+ case "$filepath" in
407
+ */_templates/*) continue ;;
408
+ esac
403
409
  validate_file "$filepath" "$collection"
404
410
  files_found=$((files_found + 1))
405
411
  # shellcheck disable=SC2086
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.2.1
4
+ version: 1.3.0
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-22 00:00:00.000000000 Z
11
+ date: 2026-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -142,6 +142,7 @@ files:
142
142
  - _includes/components/theme-customizer.html
143
143
  - _includes/components/theme-info.html
144
144
  - _includes/components/zer0-env-var.html
145
+ - _includes/content/backlinks.html
145
146
  - _includes/content/giscus.html
146
147
  - _includes/content/intro.html
147
148
  - _includes/content/jsonld-faq.html
@@ -149,6 +150,7 @@ files:
149
150
  - _includes/content/seo.html
150
151
  - _includes/content/sitemap.html
151
152
  - _includes/content/toc.html
153
+ - _includes/content/transclude.html
152
154
  - _includes/core/branding.html
153
155
  - _includes/core/footer.html
154
156
  - _includes/core/head.html
@@ -158,6 +160,7 @@ files:
158
160
  - _includes/landing/landing-quick-links.html
159
161
  - _includes/navigation/admin-nav.html
160
162
  - _includes/navigation/breadcrumbs.html
163
+ - _includes/navigation/local-graph.html
161
164
  - _includes/navigation/nav-tree.html
162
165
  - _includes/navigation/nav_list.html
163
166
  - _includes/navigation/navbar.html
@@ -194,11 +197,13 @@ files:
194
197
  - _layouts/stats.html
195
198
  - _layouts/tag.html
196
199
  - _layouts/welcome.html
200
+ - _plugins/obsidian_links.rb
197
201
  - _plugins/preview_image_generator.rb
198
202
  - _plugins/theme_version.rb
199
203
  - _sass/core/_docs-layout.scss
200
204
  - _sass/core/_nav-tree.scss
201
205
  - _sass/core/_navbar.scss
206
+ - _sass/core/_obsidian.scss
202
207
  - _sass/core/_offcanvas-panels.scss
203
208
  - _sass/core/_syntax.scss
204
209
  - _sass/core/_theme.scss
@@ -247,6 +252,7 @@ files:
247
252
  - assets/data/notebooks/sales_data.csv
248
253
  - assets/data/notebooks/survey_responses.csv
249
254
  - assets/data/notebooks/weather_data.csv
255
+ - assets/data/wiki-index.json
250
256
  - assets/images/authors/bamr87.png
251
257
  - assets/images/favicon_gpt_computer_retro.png
252
258
  - assets/images/gravatar-small.png
@@ -298,6 +304,9 @@ files:
298
304
  - assets/js/nanobar.min.js
299
305
  - assets/js/nav-editor.js
300
306
  - assets/js/navigation.js
307
+ - assets/js/obsidian-graph.js
308
+ - assets/js/obsidian-local-graph.js
309
+ - assets/js/obsidian-wiki-links.js
301
310
  - assets/js/palette-generator.js
302
311
  - assets/js/particles-source.js
303
312
  - assets/js/particles.js