jekyll-theme-zer0 1.2.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +28 -4
- data/_includes/components/js-cdn.html +12 -1
- data/_includes/content/backlinks.html +107 -0
- data/_includes/content/transclude.html +62 -0
- data/_includes/navigation/local-graph.html +79 -0
- data/_includes/navigation/sidebar-left.html +6 -0
- data/_layouts/default.html +104 -91
- data/_layouts/note.html +7 -1
- data/_plugins/obsidian_links.rb +427 -0
- data/_sass/core/_obsidian.scss +242 -0
- data/_sass/core/_offcanvas-panels.scss +8 -4
- data/_sass/custom.scss +3 -0
- data/assets/data/wiki-index.json +74 -0
- data/assets/js/obsidian-graph.js +475 -0
- data/assets/js/obsidian-local-graph.js +434 -0
- data/assets/js/obsidian-wiki-links.js +358 -0
- data/scripts/README.md +22 -0
- data/scripts/bin/test +62 -0
- data/scripts/bin/validate +596 -0
- data/scripts/lib/README.md +16 -0
- data/scripts/lint-pages +6 -0
- data/scripts/validate +11 -0
- metadata +13 -2
|
@@ -0,0 +1,358 @@
|
|
|
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 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
|
+
|
|
47
|
+
var CONFIG = {
|
|
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/'),
|
|
51
|
+
wikiLinkClass: 'wiki-link',
|
|
52
|
+
brokenLinkClass: 'wiki-link wiki-link-broken'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
var IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.bmp'];
|
|
56
|
+
|
|
57
|
+
// Patterns kept conservative: we only mutate text nodes inside the main
|
|
58
|
+
// content container so navigation chrome / code samples stay untouched.
|
|
59
|
+
var EMBED_RE = /!\[\[([^\]\n|]+?)(?:\|([^\]\n]+))?\]\]/g;
|
|
60
|
+
var LINK_RE = /\[\[([^\]\n|]+?)(?:\|([^\]\n]+))?\]\]/g;
|
|
61
|
+
var TAG_RE = /(^|[^\w/#&])#([A-Za-z][\w/-]{0,63})/g;
|
|
62
|
+
|
|
63
|
+
function normalize(value) {
|
|
64
|
+
return String(value || '').toLowerCase().trim().replace(/\s+/g, ' ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function escapeHtml(value) {
|
|
68
|
+
return String(value == null ? '' : value)
|
|
69
|
+
.replace(/&/g, '&')
|
|
70
|
+
.replace(/</g, '<')
|
|
71
|
+
.replace(/>/g, '>')
|
|
72
|
+
.replace(/"/g, '"')
|
|
73
|
+
.replace(/'/g, ''');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function splitAnchor(target) {
|
|
77
|
+
var blockMatch = target.match(/^(.+?)\^([\w-]+)$/);
|
|
78
|
+
if (blockMatch) return { page: blockMatch[1].trim(), anchor: blockMatch[2].trim() };
|
|
79
|
+
var headingMatch = target.match(/^(.+?)#(.+)$/);
|
|
80
|
+
if (headingMatch) return { page: headingMatch[1].trim(), anchor: headingMatch[2].trim() };
|
|
81
|
+
return { page: target.trim(), anchor: null };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildIndex(payload) {
|
|
85
|
+
var byKey = Object.create(null);
|
|
86
|
+
if (!payload || !Array.isArray(payload.entries)) return byKey;
|
|
87
|
+
|
|
88
|
+
payload.entries.forEach(function (entry) {
|
|
89
|
+
if (!entry || !entry.url) return;
|
|
90
|
+
var keys = [];
|
|
91
|
+
if (entry.title) keys.push(entry.title);
|
|
92
|
+
if (entry.basename) keys.push(entry.basename);
|
|
93
|
+
(entry.aliases || []).forEach(function (a) { if (a) keys.push(a); });
|
|
94
|
+
keys.forEach(function (k) {
|
|
95
|
+
var nk = normalize(k);
|
|
96
|
+
if (!nk || byKey[nk]) return; // first wins, mirrors plugin behaviour
|
|
97
|
+
byKey[nk] = entry;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return byKey;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderImageEmbed(target, modifier) {
|
|
104
|
+
var widthAttr = '';
|
|
105
|
+
var alt = target;
|
|
106
|
+
if (modifier) {
|
|
107
|
+
if (/^\d+$/.test(modifier)) widthAttr = ' width="' + modifier + '"';
|
|
108
|
+
else alt = modifier;
|
|
109
|
+
}
|
|
110
|
+
var src = target.charAt(0) === '/' ? target : (CONFIG.attachmentsPath.replace(/\/$/, '') + '/' + target);
|
|
111
|
+
return '<img src="' + escapeHtml(src) + '" alt="' + escapeHtml(alt) +
|
|
112
|
+
'" loading="lazy" class="obsidian-embed obsidian-embed-image"' + widthAttr + ' />';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderNoteEmbed(target, byKey) {
|
|
116
|
+
var parts = splitAnchor(target);
|
|
117
|
+
var info = byKey[normalize(parts.page)];
|
|
118
|
+
if (!info) {
|
|
119
|
+
return '<div class="obsidian-embed obsidian-embed-broken alert alert-warning" role="alert">' +
|
|
120
|
+
'Embed not found: <code>' + escapeHtml(target) + '</code></div>';
|
|
121
|
+
}
|
|
122
|
+
var url = info.url + (parts.anchor ? '#' + parts.anchor.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-') : '');
|
|
123
|
+
return '<div class="obsidian-embed obsidian-embed-note">' +
|
|
124
|
+
'<div class="obsidian-embed-header"><a href="' + escapeHtml(url) + '">' +
|
|
125
|
+
escapeHtml(info.title || parts.page) + '</a></div>' +
|
|
126
|
+
'<div class="obsidian-embed-excerpt">' + escapeHtml(info.excerpt || '') + '</div>' +
|
|
127
|
+
'</div>';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderWikiLink(target, aliasText, byKey, currentUrl) {
|
|
131
|
+
var parts = splitAnchor(target);
|
|
132
|
+
var display = aliasText || (parts.anchor ? parts.page + ' \u203A ' + parts.anchor : parts.page);
|
|
133
|
+
var info = byKey[normalize(parts.page)];
|
|
134
|
+
if (!info) {
|
|
135
|
+
return '<a href="#" class="' + CONFIG.brokenLinkClass +
|
|
136
|
+
'" data-wiki-target="' + escapeHtml(parts.page) +
|
|
137
|
+
'" title="Unresolved wiki-link: ' + escapeHtml(parts.page) + '">' +
|
|
138
|
+
escapeHtml(display) + '</a>';
|
|
139
|
+
}
|
|
140
|
+
var url = info.url + (parts.anchor ? '#' + parts.anchor.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-') : '');
|
|
141
|
+
var currentAttr = currentUrl && info.url === currentUrl ? ' aria-current="page"' : '';
|
|
142
|
+
return '<a href="' + escapeHtml(url) + '" class="' + CONFIG.wikiLinkClass +
|
|
143
|
+
'" data-wiki-target="' + escapeHtml(parts.page) + '"' + currentAttr + '>' +
|
|
144
|
+
escapeHtml(display) + '</a>';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function rewriteHtml(html, byKey, currentUrl) {
|
|
148
|
+
// Embeds first (longer match `![[ … ]]` would otherwise be eaten by [[ … ]]).
|
|
149
|
+
html = html.replace(EMBED_RE, function (_match, target, modifier) {
|
|
150
|
+
target = target.trim();
|
|
151
|
+
modifier = (modifier || '').trim();
|
|
152
|
+
var ext = (target.match(/\.[A-Za-z0-9]+$/) || [''])[0].toLowerCase();
|
|
153
|
+
if (IMAGE_EXTENSIONS.indexOf(ext) !== -1) return renderImageEmbed(target, modifier);
|
|
154
|
+
return renderNoteEmbed(target, byKey);
|
|
155
|
+
});
|
|
156
|
+
html = html.replace(LINK_RE, function (_match, target, alias) {
|
|
157
|
+
return renderWikiLink(target.trim(), (alias || '').trim(), byKey, currentUrl);
|
|
158
|
+
});
|
|
159
|
+
html = html.replace(TAG_RE, function (_match, lead, tag) {
|
|
160
|
+
var url = (CONFIG.tagBase.replace(/\/$/, '') + '/#' + tag.toLowerCase().replace(/\//g, '-'));
|
|
161
|
+
return lead + '<a href="' + escapeHtml(url) + '" class="obsidian-tag">#' + escapeHtml(tag) + '</a>';
|
|
162
|
+
});
|
|
163
|
+
return html;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function eligibleNode(node) {
|
|
167
|
+
// Only walk text nodes whose parent isn't code / pre / a / script / style
|
|
168
|
+
// and that contain at least one of our markers.
|
|
169
|
+
if (node.nodeType !== Node.TEXT_NODE) return false;
|
|
170
|
+
var parent = node.parentNode;
|
|
171
|
+
while (parent && parent !== document.body) {
|
|
172
|
+
var tag = parent.nodeName;
|
|
173
|
+
if (tag === 'CODE' || tag === 'PRE' || tag === 'A' || tag === 'SCRIPT' || tag === 'STYLE') return false;
|
|
174
|
+
if (parent.classList && (parent.classList.contains('mermaid') || parent.classList.contains('obsidian-embed'))) return false;
|
|
175
|
+
parent = parent.parentNode;
|
|
176
|
+
}
|
|
177
|
+
var text = node.nodeValue;
|
|
178
|
+
return text && (text.indexOf('[[') !== -1 || text.indexOf('![[') !== -1 || /(^|[^\w/#&])#[A-Za-z]/.test(text));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function rewriteContainer(container, byKey, currentUrl) {
|
|
182
|
+
if (!container) return 0;
|
|
183
|
+
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
|
|
184
|
+
var batch = [];
|
|
185
|
+
var node;
|
|
186
|
+
while ((node = walker.nextNode())) {
|
|
187
|
+
if (eligibleNode(node)) batch.push(node);
|
|
188
|
+
}
|
|
189
|
+
var rewrites = 0;
|
|
190
|
+
batch.forEach(function (textNode) {
|
|
191
|
+
var original = textNode.nodeValue;
|
|
192
|
+
var rewritten = rewriteHtml(original, byKey, currentUrl);
|
|
193
|
+
if (rewritten === original) return;
|
|
194
|
+
|
|
195
|
+
// The rewriter only inserts HTML for matched markers; everything else
|
|
196
|
+
// is the raw text-node content. Wrap the bits between matches in
|
|
197
|
+
// escaped fragments so injected `<` / `&` from the source stays inert.
|
|
198
|
+
var safe = '';
|
|
199
|
+
var lastIndex = 0;
|
|
200
|
+
var combined = /(!\[\[[^\]\n|]+(?:\|[^\]\n]+)?\]\])|(\[\[[^\]\n|]+(?:\|[^\]\n]+)?\]\])|((?:^|[^\w/#&])#[A-Za-z][\w/-]{0,63})/g;
|
|
201
|
+
var match;
|
|
202
|
+
while ((match = combined.exec(original)) !== null) {
|
|
203
|
+
safe += escapeHtml(original.slice(lastIndex, match.index));
|
|
204
|
+
safe += rewriteHtml(match[0], byKey, currentUrl);
|
|
205
|
+
lastIndex = combined.lastIndex;
|
|
206
|
+
}
|
|
207
|
+
safe += escapeHtml(original.slice(lastIndex));
|
|
208
|
+
|
|
209
|
+
var template = document.createElement('template');
|
|
210
|
+
template.innerHTML = safe;
|
|
211
|
+
textNode.parentNode.replaceChild(template.content, textNode);
|
|
212
|
+
rewrites += 1;
|
|
213
|
+
});
|
|
214
|
+
return rewrites;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getCurrentUrl() {
|
|
218
|
+
return window.location.pathname;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- Callouts (DOM-level, post-kramdown) -----------------------------
|
|
222
|
+
// Kramdown turns `> [!type] Title\n> body` into:
|
|
223
|
+
// <blockquote><p>[!type] Title\nbody</p></blockquote>
|
|
224
|
+
// We detect that pattern and rewrite the blockquote into a Bootstrap alert.
|
|
225
|
+
var CALLOUT_TYPES = {
|
|
226
|
+
note: { alert: 'primary', icon: 'bi-pencil-square' },
|
|
227
|
+
abstract: { alert: 'secondary', icon: 'bi-card-text' },
|
|
228
|
+
summary: { alert: 'secondary', icon: 'bi-card-text' },
|
|
229
|
+
tldr: { alert: 'secondary', icon: 'bi-card-text' },
|
|
230
|
+
info: { alert: 'info', icon: 'bi-info-circle' },
|
|
231
|
+
todo: { alert: 'info', icon: 'bi-check2-square' },
|
|
232
|
+
tip: { alert: 'success', icon: 'bi-lightbulb' },
|
|
233
|
+
hint: { alert: 'success', icon: 'bi-lightbulb' },
|
|
234
|
+
important: { alert: 'warning', icon: 'bi-exclamation-circle' },
|
|
235
|
+
success: { alert: 'success', icon: 'bi-check-circle' },
|
|
236
|
+
check: { alert: 'success', icon: 'bi-check-circle' },
|
|
237
|
+
done: { alert: 'success', icon: 'bi-check-circle' },
|
|
238
|
+
question: { alert: 'info', icon: 'bi-question-circle' },
|
|
239
|
+
help: { alert: 'info', icon: 'bi-question-circle' },
|
|
240
|
+
faq: { alert: 'info', icon: 'bi-question-circle' },
|
|
241
|
+
warning: { alert: 'warning', icon: 'bi-exclamation-triangle' },
|
|
242
|
+
caution: { alert: 'warning', icon: 'bi-exclamation-triangle' },
|
|
243
|
+
attention: { alert: 'warning', icon: 'bi-exclamation-triangle' },
|
|
244
|
+
failure: { alert: 'danger', icon: 'bi-x-octagon' },
|
|
245
|
+
fail: { alert: 'danger', icon: 'bi-x-octagon' },
|
|
246
|
+
missing: { alert: 'danger', icon: 'bi-x-octagon' },
|
|
247
|
+
danger: { alert: 'danger', icon: 'bi-shield-exclamation' },
|
|
248
|
+
error: { alert: 'danger', icon: 'bi-shield-exclamation' },
|
|
249
|
+
bug: { alert: 'danger', icon: 'bi-bug' },
|
|
250
|
+
example: { alert: 'secondary', icon: 'bi-code-slash' },
|
|
251
|
+
quote: { alert: 'secondary', icon: 'bi-chat-quote' },
|
|
252
|
+
cite: { alert: 'secondary', icon: 'bi-chat-quote' }
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Match the first line of the first <p> inside a blockquote, e.g.
|
|
256
|
+
// `[!warning]+ Foldable warning`.
|
|
257
|
+
var CALLOUT_HEAD_RE = /^\s*\[!([A-Za-z]+)\]([+-]?)\s*([^\n]*)/;
|
|
258
|
+
|
|
259
|
+
function rewriteCallouts(container) {
|
|
260
|
+
if (!container) return 0;
|
|
261
|
+
var quotes = container.querySelectorAll('blockquote');
|
|
262
|
+
var count = 0;
|
|
263
|
+
quotes.forEach(function (bq) {
|
|
264
|
+
if (bq.dataset.obsidianCallout) return; // already processed
|
|
265
|
+
var firstChild = bq.firstElementChild;
|
|
266
|
+
// Walk past whitespace text nodes
|
|
267
|
+
while (firstChild && firstChild.nodeName !== 'P' && firstChild.nodeType !== 1) {
|
|
268
|
+
firstChild = firstChild.nextElementSibling;
|
|
269
|
+
}
|
|
270
|
+
if (!firstChild || firstChild.nodeName !== 'P') return;
|
|
271
|
+
|
|
272
|
+
var rawText = firstChild.textContent || '';
|
|
273
|
+
var m = rawText.match(CALLOUT_HEAD_RE);
|
|
274
|
+
if (!m) return;
|
|
275
|
+
|
|
276
|
+
var type = m[1].toLowerCase();
|
|
277
|
+
var spec = CALLOUT_TYPES[type] || CALLOUT_TYPES.note;
|
|
278
|
+
var fold = m[2];
|
|
279
|
+
var titleText = (m[3] || '').trim() || (type.charAt(0).toUpperCase() + type.slice(1));
|
|
280
|
+
|
|
281
|
+
// Strip the "[!type]…" head from the first paragraph (keep any trailing text)
|
|
282
|
+
var headLength = m[0].length;
|
|
283
|
+
var trailing = rawText.slice(headLength).replace(/^\s*\n?/, '');
|
|
284
|
+
if (trailing) {
|
|
285
|
+
firstChild.textContent = trailing;
|
|
286
|
+
} else {
|
|
287
|
+
firstChild.remove();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
var wrapper = document.createElement('div');
|
|
291
|
+
wrapper.className = 'alert alert-' + spec.alert + ' obsidian-callout obsidian-callout-' + type;
|
|
292
|
+
wrapper.setAttribute('role', 'alert');
|
|
293
|
+
wrapper.dataset.obsidianCallout = type;
|
|
294
|
+
if (fold === '-') wrapper.dataset.collapsed = 'true';
|
|
295
|
+
|
|
296
|
+
var titleEl = document.createElement('div');
|
|
297
|
+
titleEl.className = 'obsidian-callout-title';
|
|
298
|
+
titleEl.innerHTML = '<i class="bi ' + spec.icon + ' me-2" aria-hidden="true"></i>' + escapeHtml(titleText);
|
|
299
|
+
wrapper.appendChild(titleEl);
|
|
300
|
+
|
|
301
|
+
var bodyEl = document.createElement('div');
|
|
302
|
+
bodyEl.className = 'obsidian-callout-body';
|
|
303
|
+
// Move blockquote children into the body, preserving inner HTML
|
|
304
|
+
while (bq.firstChild) {
|
|
305
|
+
bodyEl.appendChild(bq.firstChild);
|
|
306
|
+
}
|
|
307
|
+
wrapper.appendChild(bodyEl);
|
|
308
|
+
|
|
309
|
+
bq.parentNode.replaceChild(wrapper, bq);
|
|
310
|
+
count += 1;
|
|
311
|
+
});
|
|
312
|
+
return count;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function init() {
|
|
316
|
+
var container = document.querySelector('#main-content, .bd-content, main, article') || document.body;
|
|
317
|
+
if (!container) return;
|
|
318
|
+
|
|
319
|
+
fetch(CONFIG.indexUrl, { credentials: 'same-origin', cache: 'force-cache' })
|
|
320
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
321
|
+
.then(function (payload) {
|
|
322
|
+
if (!payload) return;
|
|
323
|
+
var byKey = buildIndex(payload);
|
|
324
|
+
window.__OBSIDIAN_INDEX__ = byKey;
|
|
325
|
+
var rewrites = rewriteContainer(container, byKey, getCurrentUrl());
|
|
326
|
+
var calloutCount = rewriteCallouts(container);
|
|
327
|
+
if ((rewrites > 0 || calloutCount > 0) && window.console && console.debug) {
|
|
328
|
+
console.debug('[obsidian] rewrote', rewrites, 'text nodes,', calloutCount, 'callouts');
|
|
329
|
+
}
|
|
330
|
+
document.dispatchEvent(new CustomEvent('obsidian:ready', { detail: { count: payload.count || 0, calloutCount: calloutCount } }));
|
|
331
|
+
})
|
|
332
|
+
.catch(function (err) {
|
|
333
|
+
// Even if the index fails, we can still convert callouts (no index needed).
|
|
334
|
+
try {
|
|
335
|
+
var calloutCount = rewriteCallouts(container);
|
|
336
|
+
if (calloutCount > 0 && window.console && console.debug) {
|
|
337
|
+
console.debug('[obsidian] rewrote', calloutCount, 'callouts (index unavailable)');
|
|
338
|
+
}
|
|
339
|
+
} catch (e) { /* swallow */ }
|
|
340
|
+
if (window.console && console.warn) console.warn('[obsidian] wiki-index fetch failed:', err);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Expose for testing / programmatic use
|
|
345
|
+
window.ObsidianResolver = {
|
|
346
|
+
rewriteHtml: rewriteHtml,
|
|
347
|
+
rewriteCallouts: rewriteCallouts,
|
|
348
|
+
rewriteContainer: rewriteContainer,
|
|
349
|
+
buildIndex: buildIndex,
|
|
350
|
+
normalize: normalize
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (document.readyState === 'loading') {
|
|
354
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
355
|
+
} else {
|
|
356
|
+
init();
|
|
357
|
+
}
|
|
358
|
+
})();
|
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
|