ligarb 0.1.0 → 0.2.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/assets/style.css +130 -0
- data/lib/ligarb/builder.rb +9 -1
- data/lib/ligarb/chapter.rb +58 -1
- data/lib/ligarb/cli.rb +44 -0
- data/lib/ligarb/template.rb +65 -1
- data/lib/ligarb/version.rb +1 -1
- data/templates/book.html.erb +92 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c88870d66d05c780d5dd46b93ebad2f0daffa3ce201d8a51b6cec8c61eedcb73
|
|
4
|
+
data.tar.gz: 3044c7b204c390fe1ed67f187598d50164e27d917e82dae9897878c0d418f334
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f1f24fa71f51560116db5ee1390701988e2640ed17dd4680124aad7c1f59ae82d9f513379ed38de3f8b187874399c41322f6649ea588b205e50ae760087bc62
|
|
7
|
+
data.tar.gz: 927eb04a33dcc3b24525a5899a412f852d2a0f872c6422b0034d881ed67ba27c0151b602e01d42ebf7900324ad3f4d9392d6b7843abc9ac472d9e707466b618c
|
data/assets/style.css
CHANGED
|
@@ -376,6 +376,53 @@ mark.search-highlight {
|
|
|
376
376
|
border-radius: 2px;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
/* === Admonitions === */
|
|
380
|
+
.admonition {
|
|
381
|
+
border-left: 4px solid;
|
|
382
|
+
border-radius: 0 4px 4px 0;
|
|
383
|
+
padding: 0.75rem 1rem;
|
|
384
|
+
margin-bottom: 1rem;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.admonition p:last-child {
|
|
388
|
+
margin-bottom: 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.admonition-title {
|
|
392
|
+
font-weight: 600;
|
|
393
|
+
margin-bottom: 0.4rem !important;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.admonition-note {
|
|
397
|
+
border-left-color: #2563eb;
|
|
398
|
+
background: #eff6ff;
|
|
399
|
+
}
|
|
400
|
+
.admonition-note .admonition-title::before { content: "\2139\FE0F "; }
|
|
401
|
+
|
|
402
|
+
.admonition-tip {
|
|
403
|
+
border-left-color: #16a34a;
|
|
404
|
+
background: #f0fdf4;
|
|
405
|
+
}
|
|
406
|
+
.admonition-tip .admonition-title::before { content: "\1F4A1 "; }
|
|
407
|
+
|
|
408
|
+
.admonition-warning {
|
|
409
|
+
border-left-color: #ca8a04;
|
|
410
|
+
background: #fefce8;
|
|
411
|
+
}
|
|
412
|
+
.admonition-warning .admonition-title::before { content: "\26A0\FE0F "; }
|
|
413
|
+
|
|
414
|
+
.admonition-caution {
|
|
415
|
+
border-left-color: #dc2626;
|
|
416
|
+
background: #fef2f2;
|
|
417
|
+
}
|
|
418
|
+
.admonition-caution .admonition-title::before { content: "\1F6D1 "; }
|
|
419
|
+
|
|
420
|
+
.admonition-important {
|
|
421
|
+
border-left-color: #9333ea;
|
|
422
|
+
background: #faf5ff;
|
|
423
|
+
}
|
|
424
|
+
.admonition-important .admonition-title::before { content: "\2757 "; }
|
|
425
|
+
|
|
379
426
|
/* === Dark Mode === */
|
|
380
427
|
[data-theme="dark"] {
|
|
381
428
|
--color-bg: #1a1a2e;
|
|
@@ -390,6 +437,22 @@ mark.search-highlight {
|
|
|
390
437
|
--color-code-border: #2d3748;
|
|
391
438
|
}
|
|
392
439
|
|
|
440
|
+
[data-theme="dark"] .admonition-note {
|
|
441
|
+
background: #1e2a3a;
|
|
442
|
+
}
|
|
443
|
+
[data-theme="dark"] .admonition-tip {
|
|
444
|
+
background: #1a2e1a;
|
|
445
|
+
}
|
|
446
|
+
[data-theme="dark"] .admonition-warning {
|
|
447
|
+
background: #2e2a1a;
|
|
448
|
+
}
|
|
449
|
+
[data-theme="dark"] .admonition-caution {
|
|
450
|
+
background: #2e1a1a;
|
|
451
|
+
}
|
|
452
|
+
[data-theme="dark"] .admonition-important {
|
|
453
|
+
background: #2a1a2e;
|
|
454
|
+
}
|
|
455
|
+
|
|
393
456
|
[data-theme="dark"] .search-box input {
|
|
394
457
|
background: var(--color-code-bg);
|
|
395
458
|
color: var(--color-text);
|
|
@@ -561,6 +624,52 @@ mark.search-highlight {
|
|
|
561
624
|
}
|
|
562
625
|
}
|
|
563
626
|
|
|
627
|
+
/* === Index === */
|
|
628
|
+
.index-group {
|
|
629
|
+
margin-bottom: 1.5rem;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.index-letter {
|
|
633
|
+
font-size: 1.3rem;
|
|
634
|
+
font-weight: 700;
|
|
635
|
+
color: var(--color-accent);
|
|
636
|
+
border-bottom: 2px solid var(--color-border);
|
|
637
|
+
padding-bottom: 0.25rem;
|
|
638
|
+
margin-bottom: 0.5rem;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.index-entries {
|
|
642
|
+
margin-left: 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.index-entries dt {
|
|
646
|
+
font-weight: 600;
|
|
647
|
+
margin-top: 0.4rem;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.index-entries dt.index-sub {
|
|
651
|
+
font-weight: 400;
|
|
652
|
+
margin-left: 1.5rem;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.index-entries dd {
|
|
656
|
+
margin-left: 1.5rem;
|
|
657
|
+
font-size: 0.92rem;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.index-entries dd.index-sub {
|
|
661
|
+
margin-left: 3rem;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.index-entries dd a {
|
|
665
|
+
color: var(--color-accent);
|
|
666
|
+
text-decoration: none;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.index-entries dd a:hover {
|
|
670
|
+
text-decoration: underline;
|
|
671
|
+
}
|
|
672
|
+
|
|
564
673
|
/* === Print === */
|
|
565
674
|
@media print {
|
|
566
675
|
.sidebar,
|
|
@@ -570,6 +679,14 @@ mark.search-highlight {
|
|
|
570
679
|
display: none !important;
|
|
571
680
|
}
|
|
572
681
|
|
|
682
|
+
html {
|
|
683
|
+
font-size: 11pt;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
body {
|
|
687
|
+
line-height: 1.5;
|
|
688
|
+
}
|
|
689
|
+
|
|
573
690
|
.content {
|
|
574
691
|
margin-left: 0;
|
|
575
692
|
max-width: 100%;
|
|
@@ -589,4 +706,17 @@ mark.search-highlight {
|
|
|
589
706
|
white-space: pre-wrap;
|
|
590
707
|
word-wrap: break-word;
|
|
591
708
|
}
|
|
709
|
+
|
|
710
|
+
.chapter pre code {
|
|
711
|
+
font-size: 0.8rem;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
@page {
|
|
715
|
+
margin: 20mm 15mm;
|
|
716
|
+
@bottom-center {
|
|
717
|
+
content: counter(page);
|
|
718
|
+
font-size: 9pt;
|
|
719
|
+
color: #666;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
592
722
|
}
|
data/lib/ligarb/builder.rb
CHANGED
|
@@ -22,8 +22,16 @@ module Ligarb
|
|
|
22
22
|
assets.detect(all_chapters)
|
|
23
23
|
assets.provision!
|
|
24
24
|
|
|
25
|
+
index_entries = all_chapters.flat_map { |ch|
|
|
26
|
+
ch.index_entries.map { |e|
|
|
27
|
+
e.class.new(term: e.term, display_text: e.display_text,
|
|
28
|
+
chapter_slug: e.chapter_slug, anchor_id: e.anchor_id)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
html = Template.new.render(config: @config, chapters: all_chapters,
|
|
26
|
-
structure: structure, assets: assets
|
|
33
|
+
structure: structure, assets: assets,
|
|
34
|
+
index_entries: index_entries)
|
|
27
35
|
|
|
28
36
|
FileUtils.mkdir_p(@config.output_path)
|
|
29
37
|
output_file = File.join(@config.output_path, "index.html")
|
data/lib/ligarb/chapter.rb
CHANGED
|
@@ -5,10 +5,11 @@ require "kramdown-parser-gfm"
|
|
|
5
5
|
|
|
6
6
|
module Ligarb
|
|
7
7
|
class Chapter
|
|
8
|
-
attr_reader :title, :slug, :html, :headings, :number, :appendix_letter
|
|
8
|
+
attr_reader :title, :slug, :html, :headings, :number, :appendix_letter, :index_entries
|
|
9
9
|
attr_accessor :part_title, :cover, :relative_path
|
|
10
10
|
|
|
11
11
|
Heading = Struct.new(:level, :text, :id, :display_text, keyword_init: true)
|
|
12
|
+
IndexEntry = Struct.new(:term, :display_text, :chapter_slug, :anchor_id, keyword_init: true)
|
|
12
13
|
|
|
13
14
|
def initialize(path, base_dir)
|
|
14
15
|
@path = path
|
|
@@ -60,7 +61,10 @@ module Ligarb
|
|
|
60
61
|
@html = rewrite_image_paths(doc.to_html)
|
|
61
62
|
@html = apply_heading_ids(@html)
|
|
62
63
|
@html = convert_special_code_blocks(@html)
|
|
64
|
+
@html = convert_admonitions(@html)
|
|
63
65
|
@html = scope_footnote_ids(@html)
|
|
66
|
+
@index_entries = []
|
|
67
|
+
@html = extract_index_markers(@html)
|
|
64
68
|
@title = @headings.first&.text || @slug
|
|
65
69
|
end
|
|
66
70
|
|
|
@@ -139,12 +143,65 @@ module Ligarb
|
|
|
139
143
|
text.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
140
144
|
end
|
|
141
145
|
|
|
146
|
+
ADMONITION_TITLES = {
|
|
147
|
+
"NOTE" => "Note",
|
|
148
|
+
"TIP" => "Tip",
|
|
149
|
+
"WARNING" => "Warning",
|
|
150
|
+
"CAUTION" => "Caution",
|
|
151
|
+
"IMPORTANT" => "Important",
|
|
152
|
+
}.freeze
|
|
153
|
+
|
|
154
|
+
def convert_admonitions(html)
|
|
155
|
+
html.gsub(%r{<blockquote>\s*<p>\[!(NOTE|TIP|WARNING|CAUTION|IMPORTANT)\]\s*\n?(.*?)</p>(.*?)</blockquote>}m) do
|
|
156
|
+
type = $1
|
|
157
|
+
first_content = $2.strip
|
|
158
|
+
rest = $3
|
|
159
|
+
css_class = type.downcase
|
|
160
|
+
title = ADMONITION_TITLES[type]
|
|
161
|
+
|
|
162
|
+
inner = if first_content.empty?
|
|
163
|
+
rest
|
|
164
|
+
else
|
|
165
|
+
"<p>#{first_content}</p>#{rest}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
%(<div class="admonition admonition-#{css_class}">\n<p class="admonition-title">#{title}</p>\n#{inner}</div>)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
142
172
|
def scope_footnote_ids(html)
|
|
143
173
|
html.gsub(/(id="|href="#)(fn:|fnref:)(\w+)/) do
|
|
144
174
|
"#{$1}#{$2}#{@slug}--#{$3}"
|
|
145
175
|
end
|
|
146
176
|
end
|
|
147
177
|
|
|
178
|
+
def extract_index_markers(html)
|
|
179
|
+
idx_count = 0
|
|
180
|
+
html.gsub(%r{<a\s+href="#index(?::([^"]*))?">(.*?)</a>}m) do
|
|
181
|
+
terms_str = $1
|
|
182
|
+
display_text = $2
|
|
183
|
+
anchor_id = "#{@slug}--idx-#{idx_count}"
|
|
184
|
+
idx_count += 1
|
|
185
|
+
|
|
186
|
+
terms = if terms_str && !terms_str.empty?
|
|
187
|
+
terms_str.split(",").map(&:strip)
|
|
188
|
+
else
|
|
189
|
+
[display_text.gsub(/<[^>]+>/, "")] # strip any HTML tags for the term
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
terms.each do |term|
|
|
193
|
+
@index_entries << IndexEntry.new(
|
|
194
|
+
term: term,
|
|
195
|
+
display_text: display_text,
|
|
196
|
+
chapter_slug: @slug,
|
|
197
|
+
anchor_id: anchor_id
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
%(<span id="#{anchor_id}">#{display_text}</span>)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
148
205
|
def rewrite_image_paths(html)
|
|
149
206
|
html.gsub(/(<img\s[^>]*src=")([^"]+)(")/) do
|
|
150
207
|
prefix = $1
|
data/lib/ligarb/cli.rb
CHANGED
|
@@ -296,6 +296,21 @@ module Ligarb
|
|
|
296
296
|
Footnote IDs are scoped per chapter to avoid collisions in the single-page
|
|
297
297
|
output.
|
|
298
298
|
|
|
299
|
+
== Index ==
|
|
300
|
+
|
|
301
|
+
Mark terms for the book index using Markdown link syntax with #index:
|
|
302
|
+
|
|
303
|
+
[Ruby](#index) Index the link text as-is
|
|
304
|
+
[dynamic typing](#index:動的型付け) Index under a specific term
|
|
305
|
+
[Ruby](#index:Ruby,Languages/Ruby) Multiple index entries (comma-separated)
|
|
306
|
+
[Ruby](#index:Languages/Ruby) Hierarchical: Languages > Ruby
|
|
307
|
+
|
|
308
|
+
The link is rendered as plain text in the output (no link styling).
|
|
309
|
+
An "Index" section is automatically appended at the end of the book,
|
|
310
|
+
with terms sorted alphabetically and grouped by first character.
|
|
311
|
+
|
|
312
|
+
Clicking an index entry navigates to the exact location in the chapter.
|
|
313
|
+
|
|
299
314
|
== Custom CSS ==
|
|
300
315
|
|
|
301
316
|
Add a 'style' field to book.yml to inject custom CSS:
|
|
@@ -330,6 +345,35 @@ module Ligarb
|
|
|
330
345
|
Each chapter will show a "View on GitHub" link pointing to:
|
|
331
346
|
{repository}/blob/HEAD/{path-from-git-root}
|
|
332
347
|
|
|
348
|
+
== Admonitions ==
|
|
349
|
+
|
|
350
|
+
GFM-style blockquote alerts are converted to styled admonition boxes.
|
|
351
|
+
Five types are supported: NOTE, TIP, WARNING, CAUTION, IMPORTANT.
|
|
352
|
+
|
|
353
|
+
Syntax:
|
|
354
|
+
|
|
355
|
+
> [!NOTE]
|
|
356
|
+
> This is a note.
|
|
357
|
+
|
|
358
|
+
> [!TIP]
|
|
359
|
+
> Helpful advice here.
|
|
360
|
+
|
|
361
|
+
> [!WARNING]
|
|
362
|
+
> Be careful about this.
|
|
363
|
+
|
|
364
|
+
> [!CAUTION]
|
|
365
|
+
> Dangerous operation.
|
|
366
|
+
|
|
367
|
+
> [!IMPORTANT]
|
|
368
|
+
> Critical information.
|
|
369
|
+
|
|
370
|
+
Each type renders with a distinct color and icon:
|
|
371
|
+
- NOTE: blue (info)
|
|
372
|
+
- TIP: green (lightbulb)
|
|
373
|
+
- WARNING: yellow (warning)
|
|
374
|
+
- CAUTION: red (stop)
|
|
375
|
+
- IMPORTANT: purple (exclamation)
|
|
376
|
+
|
|
333
377
|
== Previous/Next Navigation ==
|
|
334
378
|
|
|
335
379
|
Each chapter displays Previous and Next navigation links at the bottom.
|
data/lib/ligarb/template.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Ligarb
|
|
|
12
12
|
@css_path = File.join(ASSETS_DIR, "style.css")
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def render(config:, chapters:, structure:, assets:)
|
|
15
|
+
def render(config:, chapters:, structure:, assets:, index_entries: [])
|
|
16
16
|
css = File.read(@css_path)
|
|
17
17
|
template = File.read(@template_path)
|
|
18
18
|
|
|
@@ -31,8 +31,72 @@ module Ligarb
|
|
|
31
31
|
b.local_variable_set(:assets, assets)
|
|
32
32
|
b.local_variable_set(:repository, config.repository)
|
|
33
33
|
b.local_variable_set(:appendix_label, config.appendix_label)
|
|
34
|
+
b.local_variable_set(:index_tree, build_index_tree(index_entries, chapters))
|
|
34
35
|
|
|
35
36
|
ERB.new(template, trim_mode: "-").result(b)
|
|
36
37
|
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Build a sorted tree structure for the index.
|
|
42
|
+
# Returns: { "A" => [ { term: "Algorithm", refs: [...] },
|
|
43
|
+
# { term: "Array", refs: [...], children: [ { term: "sort", refs: [...] } ] } ],
|
|
44
|
+
# ... }
|
|
45
|
+
def build_index_tree(entries, chapters)
|
|
46
|
+
return {} if entries.empty?
|
|
47
|
+
|
|
48
|
+
chapter_titles = chapters.each_with_object({}) { |ch, h| h[ch.slug] = ch.display_title }
|
|
49
|
+
|
|
50
|
+
# Group by full term, collecting refs
|
|
51
|
+
term_refs = {}
|
|
52
|
+
entries.each do |e|
|
|
53
|
+
parts = e.term.split("/", 2)
|
|
54
|
+
top = parts[0]
|
|
55
|
+
sub = parts[1]
|
|
56
|
+
|
|
57
|
+
key = sub ? [top, sub] : [top]
|
|
58
|
+
term_refs[key] ||= []
|
|
59
|
+
term_refs[key] << { chapter_slug: e.chapter_slug,
|
|
60
|
+
chapter_title: chapter_titles[e.chapter_slug] || e.chapter_slug,
|
|
61
|
+
anchor_id: e.anchor_id }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build nested structure grouped by first character
|
|
65
|
+
nested = {}
|
|
66
|
+
term_refs.each do |key, refs|
|
|
67
|
+
top = key[0]
|
|
68
|
+
sub = key[1]
|
|
69
|
+
|
|
70
|
+
nested[top] ||= { refs: [], children: {} }
|
|
71
|
+
if sub
|
|
72
|
+
nested[top][:children][sub] ||= []
|
|
73
|
+
nested[top][:children][sub].concat(refs)
|
|
74
|
+
else
|
|
75
|
+
nested[top][:refs].concat(refs)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Group by first character and sort
|
|
80
|
+
grouped = {}
|
|
81
|
+
nested.sort_by { |k, _| k }.each do |term, data|
|
|
82
|
+
letter = first_letter(term)
|
|
83
|
+
grouped[letter] ||= []
|
|
84
|
+
children = data[:children].sort_by { |k, _| k }.map { |k, v| { term: k, refs: v } }
|
|
85
|
+
grouped[letter] << { term: term, refs: data[:refs], children: children }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
grouped
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def first_letter(term)
|
|
92
|
+
ch = term[0]
|
|
93
|
+
if ch&.match?(/[a-zA-Z]/)
|
|
94
|
+
ch.upcase
|
|
95
|
+
elsif ch&.match?(/\p{Hiragana}|\p{Katakana}/)
|
|
96
|
+
ch
|
|
97
|
+
else
|
|
98
|
+
ch || "#"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
37
101
|
end
|
|
38
102
|
end
|
data/lib/ligarb/version.rb
CHANGED
data/templates/book.html.erb
CHANGED
|
@@ -103,6 +103,11 @@
|
|
|
103
103
|
</li>
|
|
104
104
|
<%- end -%>
|
|
105
105
|
<%- end -%>
|
|
106
|
+
<%- unless index_tree.empty? -%>
|
|
107
|
+
<li class="toc-chapter" data-chapter="__index__">
|
|
108
|
+
<a href="#__index__" class="toc-h1" onclick="showChapter('__index__')"><%= language == 'ja' ? '索引' : 'Index' %></a>
|
|
109
|
+
</li>
|
|
110
|
+
<%- end -%>
|
|
106
111
|
</ul>
|
|
107
112
|
</nav>
|
|
108
113
|
</div>
|
|
@@ -132,11 +137,35 @@
|
|
|
132
137
|
<%- end -%>
|
|
133
138
|
</section>
|
|
134
139
|
<%- end -%>
|
|
140
|
+
<%- unless index_tree.empty? -%>
|
|
141
|
+
<section class="chapter index-chapter" id="chapter-__index__" style="display: none;">
|
|
142
|
+
<h1><%= language == 'ja' ? '索引' : 'Index' %></h1>
|
|
143
|
+
<%- index_tree.keys.sort.each do |letter| -%>
|
|
144
|
+
<div class="index-group">
|
|
145
|
+
<h2 class="index-letter"><%= letter %></h2>
|
|
146
|
+
<dl class="index-entries">
|
|
147
|
+
<%- index_tree[letter].each do |item| -%>
|
|
148
|
+
<dt><%= item[:term] %></dt>
|
|
149
|
+
<%- item[:refs].each do |ref| -%>
|
|
150
|
+
<dd><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
|
|
151
|
+
<%- end -%>
|
|
152
|
+
<%- item[:children].each do |child| -%>
|
|
153
|
+
<dt class="index-sub"><%= child[:term] %></dt>
|
|
154
|
+
<%- child[:refs].each do |ref| -%>
|
|
155
|
+
<dd class="index-sub"><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
|
|
156
|
+
<%- end -%>
|
|
157
|
+
<%- end -%>
|
|
158
|
+
<%- end -%>
|
|
159
|
+
</dl>
|
|
160
|
+
</div>
|
|
161
|
+
<%- end -%>
|
|
162
|
+
</section>
|
|
163
|
+
<%- end -%>
|
|
135
164
|
</main>
|
|
136
165
|
|
|
137
166
|
<script>
|
|
138
167
|
(function() {
|
|
139
|
-
var chapters = [<%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %>];
|
|
168
|
+
var chapters = [<%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__index__"' unless index_tree.empty? %>];
|
|
140
169
|
var currentChapter = null;
|
|
141
170
|
|
|
142
171
|
function showChapter(slug) {
|
|
@@ -163,6 +192,26 @@
|
|
|
163
192
|
if (searchInput && searchInput.value) {
|
|
164
193
|
highlightContent(searchInput.value);
|
|
165
194
|
}
|
|
195
|
+
|
|
196
|
+
// Render mermaid/KaTeX in newly visible chapter if not yet rendered
|
|
197
|
+
renderSpecialBlocks(slug);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderSpecialBlocks(slug) {
|
|
201
|
+
var section = document.getElementById('chapter-' + slug);
|
|
202
|
+
if (!section) return;
|
|
203
|
+
if (typeof mermaid !== 'undefined') {
|
|
204
|
+
var unrendered = section.querySelectorAll('.mermaid:not([data-processed])');
|
|
205
|
+
if (unrendered.length > 0) mermaid.run({nodes: unrendered});
|
|
206
|
+
}
|
|
207
|
+
if (typeof katex !== 'undefined') {
|
|
208
|
+
section.querySelectorAll('.math-block[data-math]').forEach(function(el) {
|
|
209
|
+
if (el.childNodes.length === 0) {
|
|
210
|
+
try { katex.render(el.getAttribute('data-math'), el, {displayMode: true, throwOnError: false}); }
|
|
211
|
+
catch(e) { el.textContent = el.getAttribute('data-math'); }
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
166
215
|
}
|
|
167
216
|
|
|
168
217
|
function showChapterAndScroll(slug, headingId) {
|
|
@@ -188,6 +237,18 @@
|
|
|
188
237
|
return;
|
|
189
238
|
}
|
|
190
239
|
|
|
240
|
+
// Check for footnote links (fn: or fnref:)
|
|
241
|
+
var fnMatch = hash.match(/^(?:fn|fnref):(.+?)--/);
|
|
242
|
+
if (fnMatch) {
|
|
243
|
+
var fnChSlug = fnMatch[1];
|
|
244
|
+
if (chapters.indexOf(fnChSlug) !== -1) {
|
|
245
|
+
if (currentChapter !== fnChSlug) showChapter(fnChSlug);
|
|
246
|
+
var fnTarget = document.getElementById(hash);
|
|
247
|
+
if (fnTarget) fnTarget.scrollIntoView({ behavior: 'smooth' });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
191
252
|
// Check for deep link (chapter--heading)
|
|
192
253
|
var parts = hash.split('--');
|
|
193
254
|
if (parts.length >= 2) {
|
|
@@ -302,9 +363,36 @@
|
|
|
302
363
|
localStorage.setItem('ligarb-theme', next);
|
|
303
364
|
});
|
|
304
365
|
|
|
305
|
-
//
|
|
366
|
+
// Footnote link click handler — scroll within current chapter without triggering chapter switch
|
|
367
|
+
document.addEventListener('click', function(e) {
|
|
368
|
+
var link = e.target.closest('a[href^="#fn:"], a[href^="#fnref:"]');
|
|
369
|
+
if (!link) return;
|
|
370
|
+
var targetId = link.getAttribute('href').substring(1);
|
|
371
|
+
var target = document.getElementById(targetId);
|
|
372
|
+
if (target) {
|
|
373
|
+
e.preventDefault();
|
|
374
|
+
history.replaceState(null, '', '#' + targetId);
|
|
375
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Add title attribute to footnote references for hover preview
|
|
380
|
+
document.querySelectorAll('sup a[href^="#fn:"]').forEach(function(link) {
|
|
381
|
+
var targetId = link.getAttribute('href').substring(1);
|
|
382
|
+
var footnoteLi = document.getElementById(targetId);
|
|
383
|
+
if (footnoteLi) {
|
|
384
|
+
// Clone to strip the back-reference link, then get text
|
|
385
|
+
var clone = footnoteLi.cloneNode(true);
|
|
386
|
+
var backlinks = clone.querySelectorAll('a[href^="#fnref:"]');
|
|
387
|
+
backlinks.forEach(function(bl) { bl.remove(); });
|
|
388
|
+
link.title = clone.textContent.trim();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Expose to onclick handlers and external scripts
|
|
306
393
|
window.showChapter = showChapter;
|
|
307
394
|
window.showChapterAndScroll = showChapterAndScroll;
|
|
395
|
+
window.renderSpecialBlocks = function() { if (currentChapter) renderSpecialBlocks(currentChapter); };
|
|
308
396
|
|
|
309
397
|
// Initialize
|
|
310
398
|
handleHash();
|
|
@@ -319,17 +407,12 @@
|
|
|
319
407
|
<script src="js/mermaid.min.js"></script>
|
|
320
408
|
<script>
|
|
321
409
|
mermaid.initialize({startOnLoad: false});
|
|
322
|
-
|
|
410
|
+
renderSpecialBlocks();
|
|
323
411
|
</script>
|
|
324
412
|
<%- end -%>
|
|
325
413
|
<%- if assets.need?(:katex) -%>
|
|
326
414
|
<script src="js/katex.min.js"></script>
|
|
327
|
-
<script>
|
|
328
|
-
document.querySelectorAll('.math-block').forEach(function(el) {
|
|
329
|
-
try { katex.render(el.getAttribute('data-math'), el, {displayMode: true, throwOnError: false}); }
|
|
330
|
-
catch(e) { el.textContent = el.getAttribute('data-math'); }
|
|
331
|
-
});
|
|
332
|
-
</script>
|
|
415
|
+
<script>renderSpecialBlocks();</script>
|
|
333
416
|
<%- end -%>
|
|
334
417
|
</body>
|
|
335
418
|
</html>
|