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,100 +1,104 @@
1
- ---
2
- layout: root
3
- ---
4
- <!--
5
- ===================================================================
6
- DEFAULT LAYOUT - Standard content layout with sidebars
7
- ===================================================================
8
-
9
- File: default.html
10
- Path: _layouts/default.html
11
- Inherits: root.html
12
- Purpose: Primary content layout with sidebar navigation and table of contents
13
-
14
- Template Logic:
15
- - Creates a responsive three-column layout using Bootstrap grid
16
- - Left sidebar: Site navigation and content outline (collapsible on mobile)
17
- - Center: Main content area with page title and body content
18
- - Right sidebar: Table of contents and page shortcuts (hidden on mobile)
19
- - Implements scroll spy for navigation highlighting
20
- - Responsive design that stacks vertically on mobile devices
21
-
22
- Layout Structure:
23
- 1. Container wrapper with Bootstrap responsive classes
24
- 2. Left sidebar (bd-sidebar) - Navigation and outline
25
- 3. Main content area (bd-main) with:
26
- - Intro section (page title, metadata)
27
- - Right sidebar (bd-toc) - Table of contents
28
- - Content area (bd-content) - Main page content
29
-
30
- Dependencies:
31
- - sidebar-left.html: Site navigation and content outline
32
- - intro.html: Page title, breadcrumbs, and metadata
33
- - sidebar-right.html: Table of contents and page shortcuts
34
-
35
- Bootstrap Classes Used:
36
- - container-xxl: Extra large responsive container
37
- - bd-gutter: Custom Bootstrap spacing
38
- - bd-layout: Custom layout utility class
39
- - bd-sidebar: Custom sidebar styling
40
- - bd-main: Main content area
41
- - bd-toc: Table of contents styling
42
- - bd-content: Content area styling
43
- ===================================================================
44
- -->
45
-
46
- <!-- ================================================ -->
47
- <!-- MAIN LAYOUT CONTAINER -->
48
- <!-- Bootstrap responsive container with custom grid -->
49
- <!-- ================================================ -->
50
- <div class="container-xxl bd-gutter mt-3 my-md-4 bd-layout">
51
-
52
- <!-- ================================ -->
53
- <!-- LEFT SIDEBAR - Navigation -->
54
- <!-- ================================ -->
55
- <!-- Site navigation, content outline, and offcanvas menu for mobile -->
56
- <!-- Sidebar visibility controlled by page.sidebar front matter -->
57
- {% unless page.sidebar == false %}
58
- <aside class="bd-sidebar">
59
- {% include navigation/sidebar-left.html %}
60
- </aside>
61
- {% endunless %}
62
-
63
- <!-- ================================ -->
64
- <!-- MAIN CONTENT AREA -->
65
- <!-- ================================ -->
66
- <!-- Primary content section with scroll spy for table of contents navigation -->
67
- <main class="bd-main order-1" data-bs-spy="scroll" data-bs-target="#TableOfContents" data-bs-offset="100" data-bs-smooth-scroll="true">
68
-
69
- <!-- Page introduction: title, breadcrumbs, metadata -->
70
- {% include content/intro.html %}
71
-
72
- <!-- =============================== -->
73
- <!-- RIGHT SIDEBAR - Table of Contents -->
74
- <!-- =============================== -->
75
- <!-- Page outline, shortcuts, and related links (hidden on mobile) -->
76
- <!-- Right sidebar visibility also controlled by page.sidebar -->
77
- {% unless page.sidebar == false %}
78
- <div class="bd-toc text-body-secondary">
79
- {% include navigation/sidebar-right.html %}
80
- </div>
81
- {% endunless %}
82
-
83
- <!-- =============================== -->
84
- <!-- MAIN CONTENT BODY -->
85
- <!-- =============================== -->
86
- <!-- Where the actual page content is rendered -->
87
- <div class="bd-content ps-lg-2">
88
- {{ content }}
89
-
90
- {%- comment -%}
91
- Optional Obsidian-style backlinks panel. Opt-in for the generic
92
- default layout via `backlinks: true` in front matter (the `note`
93
- layout enables it by default).
94
- {%- endcomment -%}
95
- {% if page.backlinks == true %}
96
- {% include content/backlinks.html %}
97
- {% endif %}
98
- </div>
99
- </main>
100
- </div>
1
+ ---
2
+ layout: root
3
+ ---
4
+ <!--
5
+ ===================================================================
6
+ DEFAULT LAYOUT - Standard content layout with sidebars
7
+ ===================================================================
8
+
9
+ File: default.html
10
+ Path: _layouts/default.html
11
+ Inherits: root.html
12
+ Purpose: Primary content layout with sidebar navigation and table of contents
13
+
14
+ Template Logic:
15
+ - Creates a responsive three-column layout using Bootstrap grid
16
+ - Left sidebar: Site navigation and content outline (collapsible on mobile)
17
+ - Local graph: Independent Obsidian side panel with its own collapsible control
18
+ - Center: Main content area with page title and body content
19
+ - Right sidebar: Table of contents and page shortcuts (hidden on mobile)
20
+ - Implements scroll spy for navigation highlighting
21
+ - Responsive design that stacks vertically on mobile devices
22
+
23
+ Layout Structure:
24
+ 1. Container wrapper with Bootstrap responsive classes
25
+ 2. Left sidebar (bd-sidebar) - Navigation and outline
26
+ 3. Main content area (bd-main) with:
27
+ - Intro section (page title, metadata)
28
+ - Right sidebar (bd-toc) - Table of contents
29
+ - Content area (bd-content) - Main page content
30
+
31
+ Dependencies:
32
+ - sidebar-left.html: Site navigation and content outline
33
+ - intro.html: Page title, breadcrumbs, and metadata
34
+ - sidebar-right.html: Table of contents and page shortcuts
35
+
36
+ Bootstrap Classes Used:
37
+ - container-xxl: Extra large responsive container
38
+ - bd-gutter: Custom Bootstrap spacing
39
+ - bd-layout: Custom layout utility class
40
+ - bd-sidebar: Custom sidebar styling
41
+ - bd-main: Main content area
42
+ - bd-toc: Table of contents styling
43
+ - bd-content: Content area styling
44
+ ===================================================================
45
+ -->
46
+
47
+ <!-- ================================================ -->
48
+ <!-- MAIN LAYOUT CONTAINER -->
49
+ <!-- Bootstrap responsive container with custom grid -->
50
+ <!-- ================================================ -->
51
+ <div class="container-xxl bd-gutter mt-3 my-md-4 bd-layout">
52
+
53
+ <!-- ================================ -->
54
+ <!-- LEFT SIDEBAR - Navigation -->
55
+ <!-- ================================ -->
56
+ <!-- Site navigation, content outline, and offcanvas menu for mobile -->
57
+ <!-- Sidebar visibility controlled by page.sidebar front matter -->
58
+ {% unless page.sidebar == false %}
59
+ <aside class="bd-sidebar">
60
+ {% include navigation/sidebar-left.html %}
61
+ </aside>
62
+
63
+ <!-- Separate Obsidian local graph panel; intentionally outside bdSidebar. -->
64
+ {% include navigation/local-graph.html %}
65
+ {% endunless %}
66
+
67
+ <!-- ================================ -->
68
+ <!-- MAIN CONTENT AREA -->
69
+ <!-- ================================ -->
70
+ <!-- Primary content section with scroll spy for table of contents navigation -->
71
+ <main class="bd-main order-1" data-bs-spy="scroll" data-bs-target="#TableOfContents" data-bs-offset="100" data-bs-smooth-scroll="true">
72
+
73
+ <!-- Page introduction: title, breadcrumbs, metadata -->
74
+ {% include content/intro.html %}
75
+
76
+ <!-- =============================== -->
77
+ <!-- RIGHT SIDEBAR - Table of Contents -->
78
+ <!-- =============================== -->
79
+ <!-- Page outline, shortcuts, and related links (hidden on mobile) -->
80
+ <!-- Right sidebar visibility also controlled by page.sidebar -->
81
+ {% unless page.sidebar == false %}
82
+ <div class="bd-toc text-body-secondary">
83
+ {% include navigation/sidebar-right.html %}
84
+ </div>
85
+ {% endunless %}
86
+
87
+ <!-- =============================== -->
88
+ <!-- MAIN CONTENT BODY -->
89
+ <!-- =============================== -->
90
+ <!-- Where the actual page content is rendered -->
91
+ <div class="bd-content ps-lg-2">
92
+ {{ content }}
93
+
94
+ {%- comment -%}
95
+ Optional Obsidian-style backlinks panel. Opt-in for the generic
96
+ default layout via `backlinks: true` in front matter (the `note`
97
+ layout enables it by default).
98
+ {%- endcomment -%}
99
+ {% if page.backlinks == true %}
100
+ {% include content/backlinks.html %}
101
+ {% endif %}
102
+ </div>
103
+ </main>
104
+ </div>
@@ -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['collections'].each do |collection_name|
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 do |doc|
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
- # Also check posts (which are special in Jekyll)
135
- site.posts.docs.each do |doc|
136
- unless has_preview?(doc)
137
- missing << {
138
- 'path' => doc.relative_path,
139
- 'title' => doc.data['title'] || File.basename(doc.relative_path),
140
- 'collection' => 'posts'
141
- }
142
- end
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
- missing.uniq { |m| m['path'] }
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
- site.data['preview_images_missing'] = PreviewImageGenerator.missing_previews(site)
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
@@ -152,18 +152,91 @@
152
152
  }
153
153
 
154
154
  // ----------------------------------------------------------------------------
155
- // Local graph widget (left sidebar) small Obsidian-style local graph view.
155
+ // Local graph side panel — Obsidian-style local graph view.
156
156
  // Rendered by _includes/navigation/local-graph.html via assets/js/obsidian-local-graph.js.
157
157
  // ----------------------------------------------------------------------------
158
+ .obsidian-local-graph-fab {
159
+ position: fixed;
160
+ left: 1rem;
161
+ bottom: 1rem;
162
+ z-index: 1032;
163
+
164
+ &[hidden] {
165
+ display: none !important;
166
+ }
167
+ }
168
+
169
+ .obsidian-local-graph-toggle {
170
+ width: 3.25rem;
171
+ height: 3.25rem;
172
+ display: inline-flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
176
+
177
+ &:hover,
178
+ &:focus-visible {
179
+ transform: translateY(-1px);
180
+ }
181
+
182
+ &:active {
183
+ transform: translateY(0);
184
+ }
185
+ }
186
+
187
+ .obsidian-local-graph-panel {
188
+ --bs-offcanvas-width: min(28rem, calc(100vw - 1.5rem));
189
+
190
+ &[hidden] {
191
+ display: none !important;
192
+ }
193
+
194
+ .offcanvas-body {
195
+ display: flex;
196
+ flex-direction: column;
197
+ min-height: 0;
198
+ }
199
+ }
200
+
158
201
  .obsidian-local-graph-widget {
202
+ display: flex;
203
+ flex: 1 1 auto;
204
+ flex-direction: column;
205
+ min-height: 0;
206
+
207
+ .obsidian-local-graph-meta {
208
+ flex: 0 0 auto;
209
+ }
210
+
159
211
  #obsidian-local-graph {
212
+ flex: 1 1 auto;
160
213
  width: 100%;
161
- height: 220px;
162
- min-height: 180px;
214
+ height: min(62vh, 34rem);
215
+ min-height: 22rem;
163
216
  border: 1px solid var(--bs-border-color, #dee2e6);
164
- border-radius: var(--bs-border-radius, 0.375rem);
217
+ border-radius: var(--bs-border-radius-lg, 0.5rem);
165
218
  background: var(--bs-tertiary-bg, #f8f9fa);
166
219
  overflow: hidden;
167
220
  position: relative;
168
221
  }
222
+
223
+ .obsidian-local-graph-status {
224
+ min-height: 1.25rem;
225
+ }
226
+ }
227
+
228
+ @media (max-width: 575.98px) {
229
+ .obsidian-local-graph-fab {
230
+ left: 0.875rem;
231
+ bottom: 0.875rem;
232
+ }
233
+
234
+ .obsidian-local-graph-panel {
235
+ --bs-offcanvas-width: min(100vw, 24rem);
236
+ }
237
+
238
+ .obsidian-local-graph-widget #obsidian-local-graph {
239
+ height: 58vh;
240
+ min-height: 18rem;
241
+ }
169
242
  }
@@ -4,13 +4,15 @@
4
4
  // =============================================================================
5
5
 
6
6
  #bdSidebar .offcanvas-header,
7
- #tocContents .offcanvas-header {
7
+ #tocContents .offcanvas-header,
8
+ #obsidianLocalGraphPanel .offcanvas-header {
8
9
  border-bottom: 2px solid var(--bs-border-color);
9
10
  padding: 1.25rem 1rem;
10
11
  }
11
12
 
12
13
  #bdSidebar .offcanvas-title,
13
- #tocContents .offcanvas-title {
14
+ #tocContents .offcanvas-title,
15
+ #obsidianLocalGraphPanel .offcanvas-title {
14
16
  font-size: 1.125rem;
15
17
  font-weight: 600;
16
18
  color: var(--bs-emphasis-color);
@@ -19,7 +21,8 @@
19
21
 
20
22
  @media (max-width: 991.98px) {
21
23
  #bdSidebar .offcanvas-header .btn-close,
22
- #tocContents .offcanvas-header .btn-close {
24
+ #tocContents .offcanvas-header .btn-close,
25
+ #obsidianLocalGraphPanel .offcanvas-header .btn-close {
23
26
  width: 48px;
24
27
  height: 48px;
25
28
  padding: 0;
@@ -36,7 +39,8 @@
36
39
  }
37
40
  }
38
41
 
39
- #tocContents .offcanvas-body {
42
+ #tocContents .offcanvas-body,
43
+ #obsidianLocalGraphPanel .offcanvas-body {
40
44
  padding: 1rem;
41
45
  }
42
46
 
@@ -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 body = doc.content | default: "" | strip_html -%}
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. We mask `![[…]]` embeds first so they don't
45
- also get split as `[[]]`, then split on `[[`, take everything
46
- before `]]`, and strip the optional `|alias` and `#anchor` parts.
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
- {%- if target != "" and target.size < 200 -%}
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 "&amp;&amp;" or target contains "&lt;" or target contains "&gt;" -%}
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": {{ doc.aliases | default: empty | jsonify }},
106
+ "aliases": {{ aliases | jsonify }},
69
107
  "outgoing": {{ outgoing | jsonify }},
70
108
  "excerpt": {{ body_excerpt | jsonify }}
71
109
  }{% unless forloop.last %},{% endunless %}
@@ -184,7 +184,6 @@
184
184
  var cy = window.cytoscape({
185
185
  container: container,
186
186
  elements: elements,
187
- wheelSensitivity: 0.2,
188
187
  minZoom: 0.1,
189
188
  maxZoom: 4,
190
189
  style: [