ligarb 0.6.0 → 0.7.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/serve.js +13 -1
- data/assets/style.css +96 -0
- data/lib/ligarb/builder.rb +82 -10
- data/lib/ligarb/chapter.rb +5 -3
- data/lib/ligarb/cli.rb +57 -2
- data/lib/ligarb/config.rb +105 -2
- data/lib/ligarb/review_store.rb +1 -0
- data/lib/ligarb/template.rb +55 -0
- data/lib/ligarb/version.rb +1 -1
- data/templates/book.html.erb +495 -8
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9771ee497c353bffb143092dae4b8631c72587e264ca3e6081e614e582de9d1b
|
|
4
|
+
data.tar.gz: 1def484d79741c7e88d657c485799762e7bf6909cd3bf9a3118aac00ac3cf32f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f7902c30dbc242df9605bfc95995a01c6a23729e1c7079811b0ff9e7a7545d534225c3216cce46c6619a073d215a728dd24a1ab6fb2011206887180ed786c03
|
|
7
|
+
data.tar.gz: 720e0fc0a1433537e956987edc1e2656931b5fb01ceb22dd57ee6b537451ab69c30c34d671ad16c3985f60d5f00ee8ddab4028c8fb4c17527b57b5d8724d70a7
|
data/assets/serve.js
CHANGED
|
@@ -58,7 +58,19 @@
|
|
|
58
58
|
if (typeof hljs !== 'undefined') hljs.highlightAll();
|
|
59
59
|
if (typeof mermaid !== 'undefined') {
|
|
60
60
|
var unrendered = oldMain.querySelectorAll('.mermaid:not([data-processed])');
|
|
61
|
-
if (unrendered.length > 0)
|
|
61
|
+
if (unrendered.length > 0) {
|
|
62
|
+
var sources = {};
|
|
63
|
+
unrendered.forEach(function(el) { sources[el.id || el.textContent.slice(0, 50)] = el.textContent; });
|
|
64
|
+
mermaid.run({nodes: unrendered, suppressErrors: true}).catch(function() {}).finally(function() {
|
|
65
|
+
unrendered.forEach(function(el) {
|
|
66
|
+
if (!el.querySelector('svg')) {
|
|
67
|
+
var src = sources[el.id || el.textContent.slice(0, 50)] || el.textContent;
|
|
68
|
+
el.innerHTML = '<pre style="color:#c00;border:1px solid #c00;padding:0.5em;white-space:pre-wrap">mermaid エラー:\n' +
|
|
69
|
+
src.replace(/</g, '<') + '</pre>';
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
62
74
|
}
|
|
63
75
|
if (typeof katex !== 'undefined') {
|
|
64
76
|
oldMain.querySelectorAll('.math-block[data-math]').forEach(function(el) {
|
data/assets/style.css
CHANGED
|
@@ -161,6 +161,11 @@ body {
|
|
|
161
161
|
color: var(--color-text-muted);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
.toc a.active-section {
|
|
165
|
+
color: var(--color-accent);
|
|
166
|
+
font-weight: 600;
|
|
167
|
+
}
|
|
168
|
+
|
|
164
169
|
.toc-chapter.active > a.toc-h1 {
|
|
165
170
|
border-left-color: var(--color-accent);
|
|
166
171
|
color: var(--color-accent);
|
|
@@ -769,6 +774,38 @@ mark.search-highlight {
|
|
|
769
774
|
border-color: #854d0e;
|
|
770
775
|
}
|
|
771
776
|
|
|
777
|
+
.lang-switcher {
|
|
778
|
+
display: flex;
|
|
779
|
+
gap: 0.3rem;
|
|
780
|
+
margin-top: 0.4rem;
|
|
781
|
+
flex-wrap: wrap;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.lang-link {
|
|
785
|
+
display: inline-block;
|
|
786
|
+
font-size: 0.7rem;
|
|
787
|
+
font-weight: 600;
|
|
788
|
+
padding: 0.15rem 0.4rem;
|
|
789
|
+
border-radius: 3px;
|
|
790
|
+
text-decoration: none;
|
|
791
|
+
border: 1px solid var(--color-border);
|
|
792
|
+
color: var(--color-accent);
|
|
793
|
+
background: transparent;
|
|
794
|
+
transition: background 0.15s, color 0.15s;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.lang-link:hover {
|
|
798
|
+
background: var(--color-accent);
|
|
799
|
+
color: #fff;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.lang-link.lang-current {
|
|
803
|
+
background: var(--color-accent);
|
|
804
|
+
color: #fff;
|
|
805
|
+
border-color: var(--color-accent);
|
|
806
|
+
cursor: default;
|
|
807
|
+
}
|
|
808
|
+
|
|
772
809
|
.chapter-footer {
|
|
773
810
|
margin-top: 2rem;
|
|
774
811
|
padding: 0.5rem 0.75rem;
|
|
@@ -777,6 +814,65 @@ mark.search-highlight {
|
|
|
777
814
|
border-top: 1px solid var(--color-border);
|
|
778
815
|
}
|
|
779
816
|
|
|
817
|
+
/* === Full TOC Page === */
|
|
818
|
+
.toc-full {
|
|
819
|
+
line-height: 1.8;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.toc-full a {
|
|
823
|
+
color: var(--color-text);
|
|
824
|
+
text-decoration: none;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.toc-full a:hover {
|
|
828
|
+
color: var(--color-accent);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.toc-full-part {
|
|
832
|
+
margin-top: 1.5rem;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.toc-full-part-title {
|
|
836
|
+
font-weight: 700;
|
|
837
|
+
font-size: 1.05rem;
|
|
838
|
+
text-transform: uppercase;
|
|
839
|
+
letter-spacing: 0.03em;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
a.toc-full-part-title {
|
|
843
|
+
color: var(--color-text);
|
|
844
|
+
text-decoration: none;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
a.toc-full-part-title:hover {
|
|
848
|
+
color: var(--color-accent);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.toc-full-chapter {
|
|
852
|
+
margin-top: 0.5rem;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.toc-full-chapter > a {
|
|
856
|
+
font-weight: 600;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.toc-full ul {
|
|
860
|
+
list-style: none;
|
|
861
|
+
margin: 0;
|
|
862
|
+
padding: 0;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.toc-full-h2 {
|
|
866
|
+
padding-left: 1.5rem;
|
|
867
|
+
font-size: 0.92rem;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.toc-full-h3 {
|
|
871
|
+
padding-left: 3rem;
|
|
872
|
+
font-size: 0.85rem;
|
|
873
|
+
color: var(--color-text-muted);
|
|
874
|
+
}
|
|
875
|
+
|
|
780
876
|
/* === Print === */
|
|
781
877
|
@media print {
|
|
782
878
|
.sidebar,
|
data/lib/ligarb/builder.rb
CHANGED
|
@@ -8,11 +8,17 @@ require_relative "asset_manager"
|
|
|
8
8
|
|
|
9
9
|
module Ligarb
|
|
10
10
|
class Builder
|
|
11
|
-
def initialize(config_path)
|
|
12
|
-
@config = Config.new(config_path)
|
|
11
|
+
def initialize(config_path, parent_data: nil)
|
|
12
|
+
@config = Config.new(config_path, parent_data: parent_data)
|
|
13
|
+
@config_path = File.expand_path(config_path)
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def build
|
|
17
|
+
if @config.translations_hub?
|
|
18
|
+
build_multilang
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
structure = load_structure
|
|
17
23
|
|
|
18
24
|
all_chapters = collect_all_chapters(structure)
|
|
@@ -49,30 +55,96 @@ module Ligarb
|
|
|
49
55
|
|
|
50
56
|
private
|
|
51
57
|
|
|
58
|
+
def build_multilang
|
|
59
|
+
hub_data = @config.instance_variable_get(:@translations_data)
|
|
60
|
+
hub_base = File.dirname(@config_path)
|
|
61
|
+
output_dir = hub_data.fetch("output_dir", "build")
|
|
62
|
+
output_path = File.join(hub_base, output_dir)
|
|
63
|
+
|
|
64
|
+
langs = []
|
|
65
|
+
all_lang_chapters = []
|
|
66
|
+
|
|
67
|
+
@config.translations.each do |trans|
|
|
68
|
+
child_config = Config.new(trans.config_path, parent_data: hub_data)
|
|
69
|
+
prefix = "#{trans.lang}--"
|
|
70
|
+
lang_data = build_language_data(trans.lang, child_config, prefix)
|
|
71
|
+
langs << lang_data
|
|
72
|
+
all_lang_chapters.concat(lang_data[:chapters])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
assets = AssetManager.new(output_path)
|
|
76
|
+
assets.detect(all_lang_chapters)
|
|
77
|
+
assets.provision!
|
|
78
|
+
|
|
79
|
+
html = Template.new.render_multilang(langs: langs, assets: assets,
|
|
80
|
+
hub_data: hub_data)
|
|
81
|
+
|
|
82
|
+
FileUtils.mkdir_p(output_path)
|
|
83
|
+
output_file = File.join(output_path, "index.html")
|
|
84
|
+
File.write(output_file, html)
|
|
85
|
+
|
|
86
|
+
# Copy images from hub base dir
|
|
87
|
+
images_dir = File.join(hub_base, "images")
|
|
88
|
+
if Dir.exist?(images_dir)
|
|
89
|
+
dest = File.join(output_path, "images")
|
|
90
|
+
FileUtils.mkdir_p(dest)
|
|
91
|
+
Dir.glob(File.join(images_dir, "*")).each { |img| FileUtils.cp(img, dest) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
puts "Built #{output_file}"
|
|
95
|
+
langs.each { |ld| puts " #{ld[:lang]}: #{ld[:chapters].size} chapter(s)" }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_language_data(lang, config, slug_prefix)
|
|
99
|
+
@config = config
|
|
100
|
+
structure = load_structure(slug_prefix: slug_prefix)
|
|
101
|
+
all_chapters = collect_all_chapters(structure)
|
|
102
|
+
resolve_cross_references(all_chapters)
|
|
103
|
+
assign_relative_paths(all_chapters) if config.repository
|
|
104
|
+
|
|
105
|
+
index_entries = all_chapters.flat_map { |ch|
|
|
106
|
+
ch.index_entries.map { |e|
|
|
107
|
+
e.class.new(term: e.term, display_text: e.display_text,
|
|
108
|
+
chapter_slug: e.chapter_slug, anchor_id: e.anchor_id)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
bibliography = resolve_citations!(all_chapters)
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
lang: lang,
|
|
116
|
+
config: config,
|
|
117
|
+
chapters: all_chapters,
|
|
118
|
+
structure: structure,
|
|
119
|
+
index_entries: index_entries,
|
|
120
|
+
bibliography: bibliography,
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
52
124
|
# StructNode mirrors Config::StructEntry but holds loaded Chapter objects
|
|
53
125
|
StructNode = Struct.new(:type, :chapter, :children, keyword_init: true)
|
|
54
126
|
|
|
55
|
-
def load_structure
|
|
127
|
+
def load_structure(slug_prefix: nil)
|
|
56
128
|
chapter_num = 0
|
|
57
129
|
appendix_num = 0
|
|
58
130
|
|
|
59
131
|
@config.structure.map do |entry|
|
|
60
132
|
case entry.type
|
|
61
133
|
when :cover
|
|
62
|
-
ch = load_chapter(entry.path)
|
|
134
|
+
ch = load_chapter(entry.path, slug_prefix: slug_prefix)
|
|
63
135
|
ch.cover = true
|
|
64
136
|
StructNode.new(type: :cover, chapter: ch)
|
|
65
137
|
when :chapter
|
|
66
138
|
chapter_num += 1
|
|
67
|
-
ch = load_chapter(entry.path)
|
|
139
|
+
ch = load_chapter(entry.path, slug_prefix: slug_prefix)
|
|
68
140
|
ch.number = chapter_num if @config.chapter_numbers
|
|
69
141
|
StructNode.new(type: :chapter, chapter: ch)
|
|
70
142
|
when :part
|
|
71
|
-
part_ch = load_chapter(entry.path)
|
|
143
|
+
part_ch = load_chapter(entry.path, slug_prefix: slug_prefix)
|
|
72
144
|
part_ch.part_title = true
|
|
73
145
|
children = (entry.children || []).map do |child|
|
|
74
146
|
chapter_num += 1
|
|
75
|
-
ch = load_chapter(child.path)
|
|
147
|
+
ch = load_chapter(child.path, slug_prefix: slug_prefix)
|
|
76
148
|
ch.number = chapter_num if @config.chapter_numbers
|
|
77
149
|
StructNode.new(type: :chapter, chapter: ch)
|
|
78
150
|
end
|
|
@@ -80,7 +152,7 @@ module Ligarb
|
|
|
80
152
|
when :appendix_group
|
|
81
153
|
children = (entry.children || []).map do |child|
|
|
82
154
|
appendix_num += 1
|
|
83
|
-
ch = load_chapter(child.path)
|
|
155
|
+
ch = load_chapter(child.path, slug_prefix: slug_prefix)
|
|
84
156
|
letter = ("A".ord + appendix_num - 1).chr
|
|
85
157
|
ch.appendix_letter = letter if @config.chapter_numbers
|
|
86
158
|
StructNode.new(type: :chapter, chapter: ch)
|
|
@@ -90,11 +162,11 @@ module Ligarb
|
|
|
90
162
|
end
|
|
91
163
|
end
|
|
92
164
|
|
|
93
|
-
def load_chapter(path)
|
|
165
|
+
def load_chapter(path, slug_prefix: nil)
|
|
94
166
|
unless File.exist?(path)
|
|
95
167
|
abort "Error: chapter not found: #{path}"
|
|
96
168
|
end
|
|
97
|
-
Chapter.new(path, @config.base_dir)
|
|
169
|
+
Chapter.new(path, @config.base_dir, slug_prefix: slug_prefix)
|
|
98
170
|
end
|
|
99
171
|
|
|
100
172
|
def collect_all_chapters(structure)
|
data/lib/ligarb/chapter.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Ligarb
|
|
|
14
14
|
IndexEntry = Struct.new(:term, :display_text, :chapter_slug, :anchor_id, keyword_init: true)
|
|
15
15
|
CiteEntry = Struct.new(:key, :display_text, :chapter_slug, :anchor_id, keyword_init: true)
|
|
16
16
|
|
|
17
|
-
def initialize(path, base_dir)
|
|
17
|
+
def initialize(path, base_dir, slug_prefix: nil)
|
|
18
18
|
@path = path
|
|
19
19
|
@base_dir = base_dir
|
|
20
20
|
@source = File.read(path)
|
|
@@ -24,7 +24,8 @@ module Ligarb
|
|
|
24
24
|
@cover = false
|
|
25
25
|
|
|
26
26
|
@relative_path = nil
|
|
27
|
-
|
|
27
|
+
base_slug = File.basename(path, ".md").gsub(/[^a-zA-Z0-9_-]/, "-")
|
|
28
|
+
@slug = slug_prefix ? "#{slug_prefix}#{base_slug}" : base_slug
|
|
28
29
|
parse!
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -191,7 +192,8 @@ module Ligarb
|
|
|
191
192
|
end
|
|
192
193
|
|
|
193
194
|
# Convert $...$ to inline math (exclude $$, and $ followed/preceded by space)
|
|
194
|
-
|
|
195
|
+
# Inline math must be on a single line (no /m flag) to avoid $200 etc. matching across lines
|
|
196
|
+
result = protected.gsub(/(?<!\$)\$(?!\$)(?!\s)(.+?)(?<!\s)(?<!\$)\$(?!\$)/) do
|
|
195
197
|
raw = decode_entities($1)
|
|
196
198
|
%(<span class="math-inline" data-math="#{encode_attr(raw)}"></span>)
|
|
197
199
|
end
|
data/lib/ligarb/cli.rb
CHANGED
|
@@ -90,6 +90,7 @@ module Ligarb
|
|
|
90
90
|
repository (optional) GitHub repository URL for "Edit on GitHub" links
|
|
91
91
|
ai_generated (optional) Mark as AI-generated (badge + meta tags, default: false)
|
|
92
92
|
footer (optional) Custom text at bottom of each chapter
|
|
93
|
+
translations (optional) Map of lang => config path for multi-language builds
|
|
93
94
|
|
|
94
95
|
Example:
|
|
95
96
|
ligarb build
|
|
@@ -194,8 +195,10 @@ module Ligarb
|
|
|
194
195
|
footer: (optional) Custom text displayed at the bottom of each chapter.
|
|
195
196
|
Overrides the default ai_generated disclaimer if both are set.
|
|
196
197
|
Useful for copyright notices, disclaimers, or other per-chapter text.
|
|
198
|
+
translations: (optional) A mapping of language codes to config file paths.
|
|
199
|
+
Enables multi-language support. See "Translations" section below.
|
|
197
200
|
chapters: (required) Book structure. An array that can contain:
|
|
198
|
-
- A cover:
|
|
201
|
+
- A cover: the landing page shown when the book is opened
|
|
199
202
|
- A string: a chapter Markdown file path (relative to book.yml)
|
|
200
203
|
- A part: groups chapters under a titled section
|
|
201
204
|
- An appendix: groups chapters with alphabetic numbering (A, B, C, ...)
|
|
@@ -204,7 +207,7 @@ module Ligarb
|
|
|
204
207
|
|
|
205
208
|
1. Cover (object with 'cover' key):
|
|
206
209
|
chapters:
|
|
207
|
-
- cover: cover.md # Markdown file:
|
|
210
|
+
- cover: cover.md # Markdown file: landing page shown when the book is opened
|
|
208
211
|
# Not shown in the TOC sidebar.
|
|
209
212
|
|
|
210
213
|
2. Plain chapter (string):
|
|
@@ -658,6 +661,8 @@ module Ligarb
|
|
|
658
661
|
language: (optional) Language. Default: "ja".
|
|
659
662
|
audience: (optional) Target audience (used in the prompt).
|
|
660
663
|
notes: (optional) Additional instructions for Claude (free text).
|
|
664
|
+
sources: (optional) Reference files for AI context. Array of strings
|
|
665
|
+
or {path:, label:} objects. Paths relative to brief.yml.
|
|
661
666
|
author: (optional) Passed through to book.yml.
|
|
662
667
|
output_dir: (optional) Passed through to book.yml.
|
|
663
668
|
chapter_numbers: (optional) Passed through to book.yml.
|
|
@@ -668,6 +673,56 @@ module Ligarb
|
|
|
668
673
|
Example: 'ligarb write ruby_book/brief.yml' creates files in ruby_book/.
|
|
669
674
|
|
|
670
675
|
Requires the 'claude' CLI to be installed.
|
|
676
|
+
|
|
677
|
+
== Translations (Multi-Language) ==
|
|
678
|
+
|
|
679
|
+
ligarb supports building the same book in multiple languages. A parent
|
|
680
|
+
config file (hub) defines shared settings and points to per-language
|
|
681
|
+
config files.
|
|
682
|
+
|
|
683
|
+
Hub config (book.yml):
|
|
684
|
+
|
|
685
|
+
repository: "https://github.com/user/repo"
|
|
686
|
+
ai_generated: true
|
|
687
|
+
translations:
|
|
688
|
+
ja: book.ja.yml
|
|
689
|
+
en: book.en.yml
|
|
690
|
+
|
|
691
|
+
Per-language config (book.ja.yml):
|
|
692
|
+
|
|
693
|
+
title: "マニュアル"
|
|
694
|
+
language: "ja"
|
|
695
|
+
chapters:
|
|
696
|
+
- chapters/ja/01-intro.md
|
|
697
|
+
|
|
698
|
+
Per-language config (book.en.yml):
|
|
699
|
+
|
|
700
|
+
title: "Manual"
|
|
701
|
+
language: "en"
|
|
702
|
+
chapters:
|
|
703
|
+
- chapters/en/01-intro.md
|
|
704
|
+
|
|
705
|
+
Building the hub builds all translations:
|
|
706
|
+
|
|
707
|
+
ligarb build book.yml # Builds all languages
|
|
708
|
+
ligarb build book.ja.yml # Builds only Japanese (standalone)
|
|
709
|
+
|
|
710
|
+
Inheritance rules:
|
|
711
|
+
- The hub's settings (repository, style, ai_generated, etc.) are
|
|
712
|
+
inherited by each per-language config as defaults.
|
|
713
|
+
- Per-language configs can override any inherited setting.
|
|
714
|
+
- title, language, and chapters are always per-language (required in
|
|
715
|
+
each per-language config).
|
|
716
|
+
|
|
717
|
+
The hub config does not need 'title' or 'chapters' fields — it only
|
|
718
|
+
needs 'translations'. If the hub has no 'chapters', it is treated
|
|
719
|
+
purely as a translations hub.
|
|
720
|
+
|
|
721
|
+
Language switcher:
|
|
722
|
+
- When built via the hub, each output HTML includes a language switcher
|
|
723
|
+
in the sidebar header (e.g. [JA | EN]).
|
|
724
|
+
- Links use relative paths between output directories.
|
|
725
|
+
- The current language is highlighted; others are clickable links.
|
|
671
726
|
SPEC
|
|
672
727
|
end
|
|
673
728
|
|
data/lib/ligarb/config.rb
CHANGED
|
@@ -6,20 +6,56 @@ module Ligarb
|
|
|
6
6
|
class Config
|
|
7
7
|
REQUIRED_KEYS = %w[title chapters].freeze
|
|
8
8
|
|
|
9
|
+
# Keys that can be inherited from a parent (translations hub) config.
|
|
10
|
+
# output_dir is intentionally excluded — it always comes from the
|
|
11
|
+
# config file that was directly passed to `ligarb build`.
|
|
12
|
+
INHERITABLE_KEYS = %w[author language chapter_numbers style
|
|
13
|
+
repository ai_generated footer bibliography].freeze
|
|
14
|
+
|
|
9
15
|
# Represents a structural entry in the book
|
|
10
16
|
StructEntry = Struct.new(:type, :path, :children, keyword_init: true)
|
|
11
17
|
# type: :chapter, :part, or :appendix_group
|
|
12
18
|
# path: markdown file path (for :chapter and :part), nil for :appendix_group
|
|
13
19
|
# children: array of StructEntry (for :part and :appendix_group)
|
|
14
20
|
|
|
21
|
+
# Represents a translation entry
|
|
22
|
+
# output_path: absolute path to the translation's build directory (for link generation)
|
|
23
|
+
TranslationEntry = Struct.new(:lang, :config_path, :title, :language, :output_path, keyword_init: true)
|
|
24
|
+
|
|
15
25
|
attr_reader :title, :author, :language, :output_dir, :base_dir,
|
|
16
26
|
:chapter_numbers, :structure, :style, :repository,
|
|
17
|
-
:ai_generated, :footer, :bibliography, :sources
|
|
27
|
+
:ai_generated, :footer, :bibliography, :sources,
|
|
28
|
+
:translations
|
|
18
29
|
|
|
19
|
-
def initialize(path)
|
|
30
|
+
def initialize(path, parent_data: nil)
|
|
20
31
|
@base_dir = File.dirname(File.expand_path(path))
|
|
21
32
|
data = YAML.safe_load_file(path)
|
|
22
33
|
|
|
34
|
+
# If this is a translations hub (has translations but no chapters), skip normal validation
|
|
35
|
+
if data.is_a?(Hash) && data.key?("translations") && !data.key?("chapters")
|
|
36
|
+
@translations_hub = true
|
|
37
|
+
@translations_data = data
|
|
38
|
+
load_translations_hub(data)
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Load inherit file if specified (for standalone builds of child configs)
|
|
43
|
+
if !parent_data && data.is_a?(Hash) && data.key?("inherit")
|
|
44
|
+
inherit_path = File.expand_path(data["inherit"], @base_dir)
|
|
45
|
+
if File.exist?(inherit_path)
|
|
46
|
+
parent_data = YAML.safe_load_file(inherit_path)
|
|
47
|
+
else
|
|
48
|
+
abort "Error: inherit config not found: #{inherit_path}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Merge parent (inheritable) keys as defaults
|
|
53
|
+
if parent_data
|
|
54
|
+
INHERITABLE_KEYS.each do |key|
|
|
55
|
+
data[key] = parent_data[key] if parent_data.key?(key) && !data.key?(key)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
23
59
|
validate!(data)
|
|
24
60
|
|
|
25
61
|
@title = data["title"]
|
|
@@ -34,6 +70,13 @@ module Ligarb
|
|
|
34
70
|
@bibliography = data.fetch("bibliography", nil)
|
|
35
71
|
@sources = parse_sources(data.fetch("sources", []))
|
|
36
72
|
@structure = parse_structure(data["chapters"])
|
|
73
|
+
@translations = []
|
|
74
|
+
|
|
75
|
+
load_translations(data, path) if data.key?("translations")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def translations_hub?
|
|
79
|
+
@translations_hub == true
|
|
37
80
|
end
|
|
38
81
|
|
|
39
82
|
def output_path
|
|
@@ -137,6 +180,66 @@ module Ligarb
|
|
|
137
180
|
collect_chapter_paths(entries)
|
|
138
181
|
end
|
|
139
182
|
|
|
183
|
+
def load_translations(data, config_path)
|
|
184
|
+
trans = data["translations"]
|
|
185
|
+
return unless trans.is_a?(Hash)
|
|
186
|
+
|
|
187
|
+
trans.each do |lang, rel_path|
|
|
188
|
+
abs_path = File.expand_path(rel_path, @base_dir)
|
|
189
|
+
unless File.exist?(abs_path)
|
|
190
|
+
abort "Error: translation config not found: #{abs_path}"
|
|
191
|
+
end
|
|
192
|
+
child_data = YAML.safe_load_file(abs_path)
|
|
193
|
+
# Merge parent keys for output_dir resolution
|
|
194
|
+
INHERITABLE_KEYS.each do |key|
|
|
195
|
+
child_data[key] = data[key] if data.key?(key) && !child_data.key?(key)
|
|
196
|
+
end
|
|
197
|
+
child_title = child_data["title"] || @title
|
|
198
|
+
child_lang = child_data.fetch("language", lang)
|
|
199
|
+
child_base = File.dirname(abs_path)
|
|
200
|
+
child_output = File.join(child_base, child_data.fetch("output_dir", "build"))
|
|
201
|
+
@translations << TranslationEntry.new(
|
|
202
|
+
lang: lang,
|
|
203
|
+
config_path: abs_path,
|
|
204
|
+
title: child_title,
|
|
205
|
+
language: child_lang,
|
|
206
|
+
output_path: child_output
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def load_translations_hub(data)
|
|
212
|
+
@title = data.fetch("title", "")
|
|
213
|
+
@translations = []
|
|
214
|
+
|
|
215
|
+
trans = data["translations"]
|
|
216
|
+
return unless trans.is_a?(Hash)
|
|
217
|
+
|
|
218
|
+
trans.each do |lang, rel_path|
|
|
219
|
+
abs_path = File.expand_path(rel_path, @base_dir)
|
|
220
|
+
unless File.exist?(abs_path)
|
|
221
|
+
abort "Error: translation config not found: #{abs_path}"
|
|
222
|
+
end
|
|
223
|
+
child_data = YAML.safe_load_file(abs_path)
|
|
224
|
+
# Merge inheritable keys from hub as defaults
|
|
225
|
+
merged = child_data.dup
|
|
226
|
+
INHERITABLE_KEYS.each do |key|
|
|
227
|
+
merged[key] = data[key] if data.key?(key) && !merged.key?(key)
|
|
228
|
+
end
|
|
229
|
+
child_title = merged["title"] || @title
|
|
230
|
+
child_lang = merged.fetch("language", lang)
|
|
231
|
+
child_base = File.dirname(abs_path)
|
|
232
|
+
child_output = File.join(child_base, merged.fetch("output_dir", "build"))
|
|
233
|
+
@translations << TranslationEntry.new(
|
|
234
|
+
lang: lang,
|
|
235
|
+
config_path: abs_path,
|
|
236
|
+
title: child_title,
|
|
237
|
+
language: child_lang,
|
|
238
|
+
output_path: child_output
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
140
243
|
def validate!(data)
|
|
141
244
|
unless data.is_a?(Hash)
|
|
142
245
|
abort "Error: book.yml must be a YAML mapping"
|
data/lib/ligarb/review_store.rb
CHANGED
data/lib/ligarb/template.rb
CHANGED
|
@@ -35,6 +35,61 @@ module Ligarb
|
|
|
35
35
|
b.local_variable_set(:footer, config.effective_footer)
|
|
36
36
|
b.local_variable_set(:index_tree, build_index_tree(index_entries, chapters))
|
|
37
37
|
b.local_variable_set(:bibliography, bibliography)
|
|
38
|
+
b.local_variable_set(:multilang, false)
|
|
39
|
+
b.local_variable_set(:langs, [])
|
|
40
|
+
|
|
41
|
+
ERB.new(template, trim_mode: "-").result(b)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Render a single HTML with all languages, switchable via JS
|
|
45
|
+
def render_multilang(langs:, assets:, hub_data:)
|
|
46
|
+
css = File.read(@css_path)
|
|
47
|
+
template = File.read(@template_path)
|
|
48
|
+
|
|
49
|
+
first = langs.first
|
|
50
|
+
first_config = first[:config]
|
|
51
|
+
|
|
52
|
+
custom_css = if first_config.style_path && File.exist?(first_config.style_path)
|
|
53
|
+
File.read(first_config.style_path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Build per-language template data
|
|
57
|
+
lang_data = langs.map do |ld|
|
|
58
|
+
cfg = ld[:config]
|
|
59
|
+
{
|
|
60
|
+
lang: ld[:lang],
|
|
61
|
+
title: cfg.title,
|
|
62
|
+
author: cfg.author,
|
|
63
|
+
language: cfg.language,
|
|
64
|
+
chapters: ld[:chapters],
|
|
65
|
+
structure: ld[:structure],
|
|
66
|
+
repository: cfg.repository,
|
|
67
|
+
appendix_label: cfg.appendix_label,
|
|
68
|
+
ai_generated: cfg.ai_generated,
|
|
69
|
+
footer: cfg.effective_footer,
|
|
70
|
+
index_tree: build_index_tree(ld[:index_entries], ld[:chapters]),
|
|
71
|
+
bibliography: ld[:bibliography],
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
b = binding
|
|
76
|
+
# Use first language's values as defaults for shared template vars
|
|
77
|
+
b.local_variable_set(:title, first_config.title)
|
|
78
|
+
b.local_variable_set(:author, first_config.author)
|
|
79
|
+
b.local_variable_set(:language, first_config.language)
|
|
80
|
+
b.local_variable_set(:chapters, first[:chapters])
|
|
81
|
+
b.local_variable_set(:structure, first[:structure])
|
|
82
|
+
b.local_variable_set(:css, css)
|
|
83
|
+
b.local_variable_set(:custom_css, custom_css)
|
|
84
|
+
b.local_variable_set(:assets, assets)
|
|
85
|
+
b.local_variable_set(:repository, first_config.repository)
|
|
86
|
+
b.local_variable_set(:appendix_label, first_config.appendix_label)
|
|
87
|
+
b.local_variable_set(:ai_generated, first_config.ai_generated)
|
|
88
|
+
b.local_variable_set(:footer, first_config.effective_footer)
|
|
89
|
+
b.local_variable_set(:index_tree, build_index_tree(first[:index_entries], first[:chapters]))
|
|
90
|
+
b.local_variable_set(:bibliography, first[:bibliography])
|
|
91
|
+
b.local_variable_set(:multilang, true)
|
|
92
|
+
b.local_variable_set(:langs, lang_data)
|
|
38
93
|
|
|
39
94
|
ERB.new(template, trim_mode: "-").result(b)
|
|
40
95
|
end
|
data/lib/ligarb/version.rb
CHANGED
data/templates/book.html.erb
CHANGED
|
@@ -28,34 +28,151 @@
|
|
|
28
28
|
<div class="sidebar" id="sidebar">
|
|
29
29
|
<div class="sidebar-header">
|
|
30
30
|
<div class="sidebar-header-top">
|
|
31
|
+
<%- if multilang -%>
|
|
32
|
+
<%- langs.each do |ld| -%>
|
|
33
|
+
<%- ld_cover = ld[:chapters].find(&:cover?) -%>
|
|
34
|
+
<%- if ld_cover -%>
|
|
35
|
+
<h1 class="book-title lang-content" data-lang="<%= ld[:lang] %>"><a href="#<%= ld_cover.slug %>" onclick="showChapter('<%= ld_cover.slug %>'); return false;"><%= h(ld[:title]) %></a></h1>
|
|
36
|
+
<%- else -%>
|
|
37
|
+
<h1 class="book-title lang-content" data-lang="<%= ld[:lang] %>"><%= h(ld[:title]) %></h1>
|
|
38
|
+
<%- end -%>
|
|
39
|
+
<%- end -%>
|
|
40
|
+
<%- else -%>
|
|
31
41
|
<%- cover_chapter = chapters.find(&:cover?) -%>
|
|
32
42
|
<%- if cover_chapter -%>
|
|
33
43
|
<h1 class="book-title"><a href="#<%= cover_chapter.slug %>" onclick="showChapter('<%= cover_chapter.slug %>'); return false;"><%= h(title) %></a></h1>
|
|
34
44
|
<%- else -%>
|
|
35
45
|
<h1 class="book-title"><%= h(title) %></h1>
|
|
36
46
|
<%- end -%>
|
|
47
|
+
<%- end -%>
|
|
37
48
|
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">☾</button>
|
|
38
49
|
</div>
|
|
50
|
+
<%- if multilang -%>
|
|
51
|
+
<%- langs.each do |ld| -%>
|
|
52
|
+
<%- unless ld[:author].empty? -%>
|
|
53
|
+
<p class="book-author lang-content" data-lang="<%= ld[:lang] %>"><%= h(ld[:author]) %></p>
|
|
54
|
+
<%- end -%>
|
|
55
|
+
<%- end -%>
|
|
56
|
+
<%- else -%>
|
|
39
57
|
<%- unless author.empty? -%>
|
|
40
58
|
<p class="book-author"><%= h(author) %></p>
|
|
41
59
|
<%- end -%>
|
|
60
|
+
<%- end -%>
|
|
61
|
+
<%- if multilang -%>
|
|
62
|
+
<%- if ai_generated -%>
|
|
63
|
+
<%- langs.each do |ld| -%>
|
|
64
|
+
<span class="ai-badge lang-content" data-lang="<%= ld[:lang] %>"><%= ld[:language] == 'ja' ? 'AI 生成' : 'AI Generated' %></span>
|
|
65
|
+
<%- end -%>
|
|
66
|
+
<%- end -%>
|
|
67
|
+
<%- else -%>
|
|
42
68
|
<%- if ai_generated -%>
|
|
43
69
|
<span class="ai-badge"><%= language == 'ja' ? 'AI 生成' : 'AI Generated' %></span>
|
|
44
70
|
<%- end -%>
|
|
71
|
+
<%- end -%>
|
|
72
|
+
<%- if multilang -%>
|
|
73
|
+
<div class="lang-switcher">
|
|
74
|
+
<%- langs.each do |ld| -%>
|
|
75
|
+
<button class="lang-link" data-lang="<%= ld[:lang] %>" onclick="switchLang('<%= ld[:lang] %>')"><%= h(ld[:lang].upcase) %></button>
|
|
76
|
+
<%- end -%>
|
|
77
|
+
</div>
|
|
78
|
+
<%- end -%>
|
|
45
79
|
</div>
|
|
46
80
|
<div class="search-box">
|
|
47
81
|
<input type="text" id="toc-search" placeholder="Search..." autocomplete="off">
|
|
48
82
|
<button class="search-clear" id="search-clear" type="button" aria-label="Clear search">×</button>
|
|
49
83
|
</div>
|
|
50
84
|
<nav class="toc" id="toc">
|
|
85
|
+
<%- if multilang -%>
|
|
86
|
+
<%- langs.each do |ld| -%>
|
|
87
|
+
<ul class="lang-content" data-lang="<%= ld[:lang] %>">
|
|
88
|
+
<li class="toc-chapter" data-chapter="<%= ld[:lang] %>--__toc__">
|
|
89
|
+
<a href="#<%= ld[:lang] %>--__toc__" class="toc-h1" onclick="showChapter('<%= ld[:lang] %>--__toc__')"><%= ld[:language] == 'ja' ? '目次' : 'Contents' %></a>
|
|
90
|
+
</li>
|
|
91
|
+
<%- ld[:structure].each do |node| -%>
|
|
92
|
+
<%- case node.type -%>
|
|
93
|
+
<%- when :cover -%>
|
|
94
|
+
<%- when :chapter -%>
|
|
95
|
+
<li class="toc-chapter" data-chapter="<%= node.chapter.slug %>">
|
|
96
|
+
<a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.display_title) %></a>
|
|
97
|
+
<%- sub_headings = node.chapter.headings.select { |h| h.level == 2 } -%>
|
|
98
|
+
<%- unless sub_headings.empty? -%>
|
|
99
|
+
<ul>
|
|
100
|
+
<%- sub_headings.each do |heading| -%>
|
|
101
|
+
<li>
|
|
102
|
+
<a href="#<%= node.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
|
|
103
|
+
</li>
|
|
104
|
+
<%- end -%>
|
|
105
|
+
</ul>
|
|
106
|
+
<%- end -%>
|
|
107
|
+
</li>
|
|
108
|
+
<%- when :part -%>
|
|
109
|
+
<li class="toc-part">
|
|
110
|
+
<a href="#<%= node.chapter.slug %>" class="toc-part-title" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.title) %></a>
|
|
111
|
+
<ul>
|
|
112
|
+
<%- (node.children || []).each do |child| -%>
|
|
113
|
+
<li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
|
|
114
|
+
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
|
|
115
|
+
<%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
|
|
116
|
+
<%- unless sub_headings.empty? -%>
|
|
117
|
+
<ul>
|
|
118
|
+
<%- sub_headings.each do |heading| -%>
|
|
119
|
+
<li>
|
|
120
|
+
<a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
|
|
121
|
+
</li>
|
|
122
|
+
<%- end -%>
|
|
123
|
+
</ul>
|
|
124
|
+
<%- end -%>
|
|
125
|
+
</li>
|
|
126
|
+
<%- end -%>
|
|
127
|
+
</ul>
|
|
128
|
+
</li>
|
|
129
|
+
<%- when :appendix_group -%>
|
|
130
|
+
<li class="toc-appendix">
|
|
131
|
+
<span class="toc-appendix-title"><%= h(ld[:appendix_label]) %></span>
|
|
132
|
+
<ul>
|
|
133
|
+
<%- (node.children || []).each do |child| -%>
|
|
134
|
+
<li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
|
|
135
|
+
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
|
|
136
|
+
<%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
|
|
137
|
+
<%- unless sub_headings.empty? -%>
|
|
138
|
+
<ul>
|
|
139
|
+
<%- sub_headings.each do |heading| -%>
|
|
140
|
+
<li>
|
|
141
|
+
<a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= h(heading.display_text) %></a>
|
|
142
|
+
</li>
|
|
143
|
+
<%- end -%>
|
|
144
|
+
</ul>
|
|
145
|
+
<%- end -%>
|
|
146
|
+
</li>
|
|
147
|
+
<%- end -%>
|
|
148
|
+
</ul>
|
|
149
|
+
</li>
|
|
150
|
+
<%- end -%>
|
|
151
|
+
<%- end -%>
|
|
152
|
+
<%- unless ld[:bibliography].empty? -%>
|
|
153
|
+
<li class="toc-chapter" data-chapter="<%= ld[:lang] %>--__bibliography__">
|
|
154
|
+
<a href="#<%= ld[:lang] %>--__bibliography__" class="toc-h1" onclick="showChapter('<%= ld[:lang] %>--__bibliography__')"><%= ld[:language] == 'ja' ? '参考文献' : 'Bibliography' %></a>
|
|
155
|
+
</li>
|
|
156
|
+
<%- end -%>
|
|
157
|
+
<%- unless ld[:index_tree].empty? -%>
|
|
158
|
+
<li class="toc-chapter" data-chapter="<%= ld[:lang] %>--__index__">
|
|
159
|
+
<a href="#<%= ld[:lang] %>--__index__" class="toc-h1" onclick="showChapter('<%= ld[:lang] %>--__index__')"><%= ld[:language] == 'ja' ? '索引' : 'Index' %></a>
|
|
160
|
+
</li>
|
|
161
|
+
<%- end -%>
|
|
162
|
+
</ul>
|
|
163
|
+
<%- end -%>
|
|
164
|
+
<%- else -%>
|
|
51
165
|
<ul>
|
|
166
|
+
<li class="toc-chapter" data-chapter="__toc__">
|
|
167
|
+
<a href="#__toc__" class="toc-h1" onclick="showChapter('__toc__')"><%= language == 'ja' ? '目次' : 'Contents' %></a>
|
|
168
|
+
</li>
|
|
52
169
|
<%- structure.each do |node| -%>
|
|
53
170
|
<%- case node.type -%>
|
|
54
171
|
<%- when :cover -%>
|
|
55
172
|
<%- when :chapter -%>
|
|
56
173
|
<li class="toc-chapter" data-chapter="<%= node.chapter.slug %>">
|
|
57
174
|
<a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.display_title) %></a>
|
|
58
|
-
<%- sub_headings = node.chapter.headings.select { |h| h.level
|
|
175
|
+
<%- sub_headings = node.chapter.headings.select { |h| h.level == 2 } -%>
|
|
59
176
|
<%- unless sub_headings.empty? -%>
|
|
60
177
|
<ul>
|
|
61
178
|
<%- sub_headings.each do |heading| -%>
|
|
@@ -73,7 +190,7 @@
|
|
|
73
190
|
<%- (node.children || []).each do |child| -%>
|
|
74
191
|
<li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
|
|
75
192
|
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
|
|
76
|
-
<%- sub_headings = child.chapter.headings.select { |h| h.level
|
|
193
|
+
<%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
|
|
77
194
|
<%- unless sub_headings.empty? -%>
|
|
78
195
|
<ul>
|
|
79
196
|
<%- sub_headings.each do |heading| -%>
|
|
@@ -94,7 +211,7 @@
|
|
|
94
211
|
<%- (node.children || []).each do |child| -%>
|
|
95
212
|
<li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
|
|
96
213
|
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
|
|
97
|
-
<%- sub_headings = child.chapter.headings.select { |h| h.level
|
|
214
|
+
<%- sub_headings = child.chapter.headings.select { |h| h.level == 2 } -%>
|
|
98
215
|
<%- unless sub_headings.empty? -%>
|
|
99
216
|
<ul>
|
|
100
217
|
<%- sub_headings.each do |heading| -%>
|
|
@@ -121,12 +238,176 @@
|
|
|
121
238
|
</li>
|
|
122
239
|
<%- end -%>
|
|
123
240
|
</ul>
|
|
241
|
+
<%- end -%>
|
|
124
242
|
</nav>
|
|
125
243
|
</div>
|
|
126
244
|
|
|
127
245
|
<button class="sidebar-toggle" id="sidebar-toggle" aria-label="Toggle sidebar">☰</button>
|
|
128
246
|
|
|
129
247
|
<main class="content" id="content">
|
|
248
|
+
<%- if multilang -%>
|
|
249
|
+
<%- langs.each do |ld| -%>
|
|
250
|
+
<section class="chapter toc-page" id="chapter-<%= ld[:lang] %>--__toc__" data-lang="<%= ld[:lang] %>" style="display: none;">
|
|
251
|
+
<h1><%= ld[:language] == 'ja' ? '目次' : 'Contents' %></h1>
|
|
252
|
+
<nav class="toc-full">
|
|
253
|
+
<%- ld[:structure].each do |node| -%>
|
|
254
|
+
<%- case node.type -%>
|
|
255
|
+
<%- when :cover -%>
|
|
256
|
+
<%- when :chapter -%>
|
|
257
|
+
<div class="toc-full-chapter">
|
|
258
|
+
<a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;"><%= h(node.chapter.display_title) %></a>
|
|
259
|
+
<%- sub = node.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
260
|
+
<%- unless sub.empty? -%>
|
|
261
|
+
<ul><%- sub.each do |heading| -%>
|
|
262
|
+
<li class="toc-full-h<%= heading.level %>"><a href="#<%= node.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
|
|
263
|
+
<%- end -%></ul>
|
|
264
|
+
<%- end -%>
|
|
265
|
+
</div>
|
|
266
|
+
<%- when :part -%>
|
|
267
|
+
<div class="toc-full-part">
|
|
268
|
+
<a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;" class="toc-full-part-title"><%= h(node.chapter.title) %></a>
|
|
269
|
+
<%- (node.children || []).each do |child| -%>
|
|
270
|
+
<div class="toc-full-chapter">
|
|
271
|
+
<a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
|
|
272
|
+
<%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
273
|
+
<%- unless sub.empty? -%>
|
|
274
|
+
<ul><%- sub.each do |heading| -%>
|
|
275
|
+
<li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
|
|
276
|
+
<%- end -%></ul>
|
|
277
|
+
<%- end -%>
|
|
278
|
+
</div>
|
|
279
|
+
<%- end -%>
|
|
280
|
+
</div>
|
|
281
|
+
<%- when :appendix_group -%>
|
|
282
|
+
<div class="toc-full-part">
|
|
283
|
+
<span class="toc-full-part-title"><%= h(ld[:appendix_label]) %></span>
|
|
284
|
+
<%- (node.children || []).each do |child| -%>
|
|
285
|
+
<div class="toc-full-chapter">
|
|
286
|
+
<a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
|
|
287
|
+
<%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
288
|
+
<%- unless sub.empty? -%>
|
|
289
|
+
<ul><%- sub.each do |heading| -%>
|
|
290
|
+
<li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
|
|
291
|
+
<%- end -%></ul>
|
|
292
|
+
<%- end -%>
|
|
293
|
+
</div>
|
|
294
|
+
<%- end -%>
|
|
295
|
+
</div>
|
|
296
|
+
<%- end -%>
|
|
297
|
+
<%- end -%>
|
|
298
|
+
</nav>
|
|
299
|
+
</section>
|
|
300
|
+
<%- ld[:chapters].each_with_index do |chapter, idx| -%>
|
|
301
|
+
<section class="<%= chapter.cover? ? 'chapter cover-page' : 'chapter' %>" id="chapter-<%= chapter.slug %>" data-lang="<%= ld[:lang] %>" style="display: none;">
|
|
302
|
+
<%= chapter.html %>
|
|
303
|
+
<%- if ld[:repository] && !chapter.part_title? && !chapter.cover? -%>
|
|
304
|
+
<div class="edit-link">
|
|
305
|
+
<a href="<%= h(ld[:repository].chomp('/')) %>/blob/HEAD/<%= h(chapter.relative_path) %>" target="_blank" rel="noopener">View on GitHub</a>
|
|
306
|
+
</div>
|
|
307
|
+
<%- end -%>
|
|
308
|
+
<%- if ld[:footer] && !chapter.part_title? && !chapter.cover? -%>
|
|
309
|
+
<div class="chapter-footer"><%= h(ld[:footer]) %></div>
|
|
310
|
+
<%- end -%>
|
|
311
|
+
<%- unless chapter.cover? -%>
|
|
312
|
+
<nav class="chapter-nav">
|
|
313
|
+
<%- if idx > 0 -%>
|
|
314
|
+
<a href="#" class="nav-prev" onclick="showChapter('<%= ld[:chapters][idx-1].slug %>'); return false;">← <%= h(ld[:chapters][idx-1].display_title) %></a>
|
|
315
|
+
<%- else -%>
|
|
316
|
+
<span></span>
|
|
317
|
+
<%- end -%>
|
|
318
|
+
<%- if idx < ld[:chapters].size - 1 -%>
|
|
319
|
+
<a href="#" class="nav-next" onclick="showChapter('<%= ld[:chapters][idx+1].slug %>'); return false;"><%= h(ld[:chapters][idx+1].display_title) %> →</a>
|
|
320
|
+
<%- end -%>
|
|
321
|
+
</nav>
|
|
322
|
+
<%- end -%>
|
|
323
|
+
</section>
|
|
324
|
+
<%- end -%>
|
|
325
|
+
<%- unless ld[:bibliography].empty? -%>
|
|
326
|
+
<section class="chapter bibliography-chapter" id="chapter-<%= ld[:lang] %>--__bibliography__" data-lang="<%= ld[:lang] %>" style="display: none;">
|
|
327
|
+
<h1><%= ld[:language] == 'ja' ? '参考文献' : 'Bibliography' %></h1>
|
|
328
|
+
<ul class="bibliography-list">
|
|
329
|
+
<%- ld[:bibliography].each do |entry| -%>
|
|
330
|
+
<li id="<%= ld[:lang] %>--bib-<%= h(entry[:key]) %>"><span class="bib-label">[<%= h(entry[:label]) %>]</span> <%= entry[:formatted_html] %></li>
|
|
331
|
+
<%- end -%>
|
|
332
|
+
</ul>
|
|
333
|
+
</section>
|
|
334
|
+
<%- end -%>
|
|
335
|
+
<%- unless ld[:index_tree].empty? -%>
|
|
336
|
+
<section class="chapter index-chapter" id="chapter-<%= ld[:lang] %>--__index__" data-lang="<%= ld[:lang] %>" style="display: none;">
|
|
337
|
+
<h1><%= ld[:language] == 'ja' ? '索引' : 'Index' %></h1>
|
|
338
|
+
<%- ld[:index_tree].keys.sort.each do |letter| -%>
|
|
339
|
+
<div class="index-group">
|
|
340
|
+
<h2 class="index-letter"><%= letter %></h2>
|
|
341
|
+
<dl class="index-entries">
|
|
342
|
+
<%- ld[:index_tree][letter].each do |item| -%>
|
|
343
|
+
<dt><%= h(item[:term]) %></dt>
|
|
344
|
+
<%- item[:refs].each do |ref| -%>
|
|
345
|
+
<dd><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
|
|
346
|
+
<%- end -%>
|
|
347
|
+
<%- item[:children].each do |child| -%>
|
|
348
|
+
<dt class="index-sub"><%= h(child[:term]) %></dt>
|
|
349
|
+
<%- child[:refs].each do |ref| -%>
|
|
350
|
+
<dd class="index-sub"><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
|
|
351
|
+
<%- end -%>
|
|
352
|
+
<%- end -%>
|
|
353
|
+
<%- end -%>
|
|
354
|
+
</dl>
|
|
355
|
+
</div>
|
|
356
|
+
<%- end -%>
|
|
357
|
+
</section>
|
|
358
|
+
<%- end -%>
|
|
359
|
+
<%- end -%>
|
|
360
|
+
<%- else -%>
|
|
361
|
+
<section class="chapter toc-page" id="chapter-__toc__" style="display: none;">
|
|
362
|
+
<h1><%= language == 'ja' ? '目次' : 'Contents' %></h1>
|
|
363
|
+
<nav class="toc-full">
|
|
364
|
+
<%- structure.each do |node| -%>
|
|
365
|
+
<%- case node.type -%>
|
|
366
|
+
<%- when :cover -%>
|
|
367
|
+
<%- when :chapter -%>
|
|
368
|
+
<div class="toc-full-chapter">
|
|
369
|
+
<a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;"><%= h(node.chapter.display_title) %></a>
|
|
370
|
+
<%- sub = node.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
371
|
+
<%- unless sub.empty? -%>
|
|
372
|
+
<ul><%- sub.each do |heading| -%>
|
|
373
|
+
<li class="toc-full-h<%= heading.level %>"><a href="#<%= node.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
|
|
374
|
+
<%- end -%></ul>
|
|
375
|
+
<%- end -%>
|
|
376
|
+
</div>
|
|
377
|
+
<%- when :part -%>
|
|
378
|
+
<div class="toc-full-part">
|
|
379
|
+
<a href="#<%= node.chapter.slug %>" onclick="showChapter('<%= node.chapter.slug %>'); return false;" class="toc-full-part-title"><%= h(node.chapter.title) %></a>
|
|
380
|
+
<%- (node.children || []).each do |child| -%>
|
|
381
|
+
<div class="toc-full-chapter">
|
|
382
|
+
<a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
|
|
383
|
+
<%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
384
|
+
<%- unless sub.empty? -%>
|
|
385
|
+
<ul><%- sub.each do |heading| -%>
|
|
386
|
+
<li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
|
|
387
|
+
<%- end -%></ul>
|
|
388
|
+
<%- end -%>
|
|
389
|
+
</div>
|
|
390
|
+
<%- end -%>
|
|
391
|
+
</div>
|
|
392
|
+
<%- when :appendix_group -%>
|
|
393
|
+
<div class="toc-full-part">
|
|
394
|
+
<span class="toc-full-part-title"><%= h(appendix_label) %></span>
|
|
395
|
+
<%- (node.children || []).each do |child| -%>
|
|
396
|
+
<div class="toc-full-chapter">
|
|
397
|
+
<a href="#<%= child.chapter.slug %>" onclick="showChapter('<%= child.chapter.slug %>'); return false;"><%= h(child.chapter.display_title) %></a>
|
|
398
|
+
<%- sub = child.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
399
|
+
<%- unless sub.empty? -%>
|
|
400
|
+
<ul><%- sub.each do |heading| -%>
|
|
401
|
+
<li class="toc-full-h<%= heading.level %>"><a href="#<%= child.chapter.slug %>--<%= heading.id %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>'); return false;"><%= h(heading.display_text) %></a></li>
|
|
402
|
+
<%- end -%></ul>
|
|
403
|
+
<%- end -%>
|
|
404
|
+
</div>
|
|
405
|
+
<%- end -%>
|
|
406
|
+
</div>
|
|
407
|
+
<%- end -%>
|
|
408
|
+
<%- end -%>
|
|
409
|
+
</nav>
|
|
410
|
+
</section>
|
|
130
411
|
<%- chapters.each_with_index do |chapter, idx| -%>
|
|
131
412
|
<section class="<%= chapter.cover? ? 'chapter cover-page' : 'chapter' %>" id="chapter-<%= chapter.slug %>" style="display: none;">
|
|
132
413
|
<%= chapter.html %>
|
|
@@ -186,38 +467,180 @@
|
|
|
186
467
|
<%- end -%>
|
|
187
468
|
</section>
|
|
188
469
|
<%- end -%>
|
|
470
|
+
<%- end -%>
|
|
189
471
|
</main>
|
|
190
472
|
|
|
191
473
|
<script>
|
|
192
474
|
(function() {
|
|
193
|
-
|
|
475
|
+
<%- if multilang -%>
|
|
476
|
+
var langChapters = {
|
|
477
|
+
<%- langs.each do |ld| -%>
|
|
478
|
+
"<%= ld[:lang] %>": ["<%= ld[:lang] %>--__toc__", <%= ld[:chapters].map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ", \"#{ld[:lang]}--__bibliography__\"" unless ld[:bibliography].empty? %><%= ", \"#{ld[:lang]}--__index__\"" unless ld[:index_tree].empty? %>],
|
|
479
|
+
<%- end -%>
|
|
480
|
+
};
|
|
481
|
+
var allLangs = [<%= langs.map { |ld| "\"#{ld[:lang]}\"" }.join(", ") %>];
|
|
482
|
+
var currentLang = localStorage.getItem('ligarb-lang') || allLangs[0];
|
|
483
|
+
if (allLangs.indexOf(currentLang) === -1) currentLang = allLangs[0];
|
|
484
|
+
var chapters = langChapters[currentLang];
|
|
485
|
+
<%- else -%>
|
|
486
|
+
var chapters = ["__toc__", <%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__bibliography__"' unless bibliography.empty? %><%= ', "__index__"' unless index_tree.empty? %>];
|
|
487
|
+
<%- end -%>
|
|
194
488
|
var currentChapter = null;
|
|
195
489
|
|
|
196
490
|
var navigating = false; // flag to suppress pushState during popstate/initial load
|
|
197
491
|
|
|
492
|
+
// Track the currently visible heading index and highlight TOC
|
|
493
|
+
var currentHeadingIndex = -1;
|
|
494
|
+
var headingObserver = null;
|
|
495
|
+
|
|
496
|
+
function setupHeadingObserver() {
|
|
497
|
+
if (headingObserver) headingObserver.disconnect();
|
|
498
|
+
if (!currentChapter) return;
|
|
499
|
+
var section = document.getElementById('chapter-' + currentChapter);
|
|
500
|
+
if (!section) return;
|
|
501
|
+
var headings = section.querySelectorAll('h1, h2, h3');
|
|
502
|
+
if (headings.length === 0) return;
|
|
503
|
+
|
|
504
|
+
headingObserver = new IntersectionObserver(function(entries) {
|
|
505
|
+
entries.forEach(function(entry) {
|
|
506
|
+
if (entry.isIntersecting) {
|
|
507
|
+
var idx = Array.prototype.indexOf.call(headings, entry.target);
|
|
508
|
+
if (idx !== -1) {
|
|
509
|
+
currentHeadingIndex = idx;
|
|
510
|
+
highlightTocHeading(entry.target);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}, { rootMargin: '0px 0px -70% 0px', threshold: 0 });
|
|
515
|
+
|
|
516
|
+
headings.forEach(function(h) { headingObserver.observe(h); });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function highlightTocHeading(heading) {
|
|
520
|
+
// Remove previous active-section highlights
|
|
521
|
+
document.querySelectorAll('.toc a.active-section').forEach(function(a) {
|
|
522
|
+
a.classList.remove('active-section');
|
|
523
|
+
});
|
|
524
|
+
// Find the TOC link matching this heading's ID
|
|
525
|
+
var hid = heading.id;
|
|
526
|
+
if (!hid) return;
|
|
527
|
+
var tocLink = document.querySelector('.toc a[href="#' + CSS.escape(hid) + '"]');
|
|
528
|
+
if (tocLink) tocLink.classList.add('active-section');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
<%- if multilang -%>
|
|
532
|
+
|
|
533
|
+
function switchLang(lang) {
|
|
534
|
+
// Remember current position: chapter base slug and heading index
|
|
535
|
+
var oldLang = currentLang;
|
|
536
|
+
var targetSlug = null;
|
|
537
|
+
var targetHeadingIdx = currentHeadingIndex;
|
|
538
|
+
|
|
539
|
+
if (currentChapter) {
|
|
540
|
+
// Strip lang prefix to get the base slug (e.g. "ja--03-book-yml" → "03-book-yml")
|
|
541
|
+
var baseSlug = currentChapter.replace(new RegExp('^' + oldLang + '--'), '');
|
|
542
|
+
targetSlug = lang + '--' + baseSlug;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
currentLang = lang;
|
|
546
|
+
chapters = langChapters[lang];
|
|
547
|
+
localStorage.setItem('ligarb-lang', lang);
|
|
548
|
+
|
|
549
|
+
// Toggle lang-content elements (TOC, header)
|
|
550
|
+
document.querySelectorAll('.lang-content').forEach(function(el) {
|
|
551
|
+
el.style.display = el.dataset.lang === lang ? '' : 'none';
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Toggle lang-switcher buttons
|
|
555
|
+
document.querySelectorAll('.lang-link').forEach(function(btn) {
|
|
556
|
+
if (btn.dataset.lang === lang) {
|
|
557
|
+
btn.classList.add('lang-current');
|
|
558
|
+
} else {
|
|
559
|
+
btn.classList.remove('lang-current');
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Hide all chapter sections, then show first of current lang
|
|
564
|
+
document.querySelectorAll('main .chapter').forEach(function(el) {
|
|
565
|
+
el.style.display = 'none';
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// Reset search cache
|
|
569
|
+
chapterTextCache = null;
|
|
570
|
+
|
|
571
|
+
// Navigate to the corresponding chapter in the new language
|
|
572
|
+
if (targetSlug && chapters.indexOf(targetSlug) !== -1) {
|
|
573
|
+
showChapter(targetSlug);
|
|
574
|
+
// Scroll to the corresponding heading by index
|
|
575
|
+
if (targetHeadingIdx >= 0) {
|
|
576
|
+
setTimeout(function() {
|
|
577
|
+
var section = document.getElementById('chapter-' + targetSlug);
|
|
578
|
+
if (!section) return;
|
|
579
|
+
var headings = section.querySelectorAll('h1, h2, h3');
|
|
580
|
+
if (targetHeadingIdx < headings.length) {
|
|
581
|
+
headings[targetHeadingIdx].scrollIntoView({ behavior: 'smooth' });
|
|
582
|
+
}
|
|
583
|
+
}, 50);
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
showChapter(chapters[0]);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Initialize lang display
|
|
591
|
+
function initLang() {
|
|
592
|
+
document.querySelectorAll('.lang-content').forEach(function(el) {
|
|
593
|
+
el.style.display = el.dataset.lang === currentLang ? '' : 'none';
|
|
594
|
+
});
|
|
595
|
+
document.querySelectorAll('.lang-link').forEach(function(btn) {
|
|
596
|
+
if (btn.dataset.lang === currentLang) btn.classList.add('lang-current');
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
window.switchLang = switchLang;
|
|
601
|
+
<%- end -%>
|
|
602
|
+
|
|
198
603
|
function showChapter(slug, hash) {
|
|
199
604
|
chapters.forEach(function(ch) {
|
|
200
605
|
var el = document.getElementById('chapter-' + ch);
|
|
201
606
|
if (el) el.style.display = (ch === slug) ? 'block' : 'none';
|
|
202
607
|
});
|
|
203
608
|
|
|
204
|
-
// Update active state in TOC
|
|
609
|
+
// Update active state in TOC and scroll sidebar to show active item
|
|
610
|
+
<%- if multilang -%>
|
|
611
|
+
var tocContainer = document.querySelector('.toc .lang-content[data-lang="' + currentLang + '"]');
|
|
612
|
+
var tocItems = tocContainer ? tocContainer.querySelectorAll('.toc-chapter') : [];
|
|
613
|
+
<%- else -%>
|
|
205
614
|
var tocItems = document.querySelectorAll('.toc-chapter');
|
|
615
|
+
<%- end -%>
|
|
616
|
+
var activeItem = null;
|
|
206
617
|
tocItems.forEach(function(item) {
|
|
207
618
|
if (item.dataset.chapter === slug) {
|
|
208
619
|
item.classList.add('active');
|
|
620
|
+
activeItem = item;
|
|
209
621
|
} else {
|
|
210
622
|
item.classList.remove('active');
|
|
211
623
|
}
|
|
212
624
|
});
|
|
625
|
+
if (activeItem) {
|
|
626
|
+
var tocNav = document.getElementById('toc');
|
|
627
|
+
var itemRect = activeItem.getBoundingClientRect();
|
|
628
|
+
var tocRect = tocNav.getBoundingClientRect();
|
|
629
|
+
if (itemRect.top < tocRect.top || itemRect.bottom > tocRect.bottom) {
|
|
630
|
+
activeItem.scrollIntoView({ block: 'center' });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
213
633
|
|
|
214
634
|
currentChapter = slug;
|
|
635
|
+
currentHeadingIndex = -1;
|
|
215
636
|
var newHash = '#' + (hash || slug);
|
|
216
637
|
if (!navigating) {
|
|
217
638
|
history.pushState(null, '', newHash);
|
|
218
639
|
}
|
|
219
640
|
window.scrollTo(0, 0);
|
|
220
641
|
|
|
642
|
+
setupHeadingObserver();
|
|
643
|
+
|
|
221
644
|
// Re-apply highlight if search is active
|
|
222
645
|
if (searchInput && searchInput.value) {
|
|
223
646
|
highlightContent(searchInput.value);
|
|
@@ -232,7 +655,21 @@
|
|
|
232
655
|
if (!section) return;
|
|
233
656
|
if (typeof mermaid !== 'undefined') {
|
|
234
657
|
var unrendered = section.querySelectorAll('.mermaid:not([data-processed])');
|
|
235
|
-
if (unrendered.length > 0)
|
|
658
|
+
if (unrendered.length > 0) {
|
|
659
|
+
// Save original source for error recovery
|
|
660
|
+
var sources = {};
|
|
661
|
+
unrendered.forEach(function(el) { sources[el.id || el.textContent.slice(0, 50)] = el.textContent; });
|
|
662
|
+
mermaid.run({nodes: unrendered, suppressErrors: true}).catch(function() {}).finally(function() {
|
|
663
|
+
unrendered.forEach(function(el) {
|
|
664
|
+
// mermaid sets data-processed; if it's still text-only (no SVG), it failed
|
|
665
|
+
if (!el.querySelector('svg')) {
|
|
666
|
+
var src = sources[el.id || el.textContent.slice(0, 50)] || el.textContent;
|
|
667
|
+
el.innerHTML = '<pre style="color:#c00;border:1px solid #c00;padding:0.5em;white-space:pre-wrap">mermaid error:\n' +
|
|
668
|
+
src.replace(/</g, '<') + '</pre>';
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
}
|
|
236
673
|
}
|
|
237
674
|
if (typeof katex !== 'undefined') {
|
|
238
675
|
section.querySelectorAll('.math-block[data-math]').forEach(function(el) {
|
|
@@ -325,6 +762,7 @@
|
|
|
325
762
|
if (target) {
|
|
326
763
|
target.scrollIntoView({ behavior: 'smooth' });
|
|
327
764
|
highlightElement(target);
|
|
765
|
+
highlightTocHeading(target);
|
|
328
766
|
}
|
|
329
767
|
}, 50);
|
|
330
768
|
}
|
|
@@ -345,6 +783,14 @@
|
|
|
345
783
|
return;
|
|
346
784
|
}
|
|
347
785
|
|
|
786
|
+
<%- if multilang -%>
|
|
787
|
+
// In multilang mode, detect language from hash prefix (e.g., "en--cover")
|
|
788
|
+
var langMatch = hash.match(/^([a-z]{2})--/);
|
|
789
|
+
if (langMatch && allLangs.indexOf(langMatch[1]) !== -1 && langMatch[1] !== currentLang) {
|
|
790
|
+
switchLang(langMatch[1]);
|
|
791
|
+
}
|
|
792
|
+
<%- end -%>
|
|
793
|
+
|
|
348
794
|
// Check if hash matches a chapter slug directly
|
|
349
795
|
if (chapters.indexOf(hash) !== -1) {
|
|
350
796
|
showChapter(hash);
|
|
@@ -352,7 +798,20 @@
|
|
|
352
798
|
return;
|
|
353
799
|
}
|
|
354
800
|
|
|
355
|
-
|
|
801
|
+
<%- if multilang -%>
|
|
802
|
+
// Check for bibliography entry (bib-KEY)
|
|
803
|
+
var bibMatch = hash.match(/^(?:([a-z]{2})--)?bib-(.+)/);
|
|
804
|
+
if (bibMatch) {
|
|
805
|
+
var bibLang = bibMatch[1] || currentLang;
|
|
806
|
+
showChapter(bibLang + '--__bibliography__', hash);
|
|
807
|
+
setTimeout(function() {
|
|
808
|
+
var target = document.getElementById(hash);
|
|
809
|
+
if (target) target.scrollIntoView({ behavior: 'smooth' });
|
|
810
|
+
}, 50);
|
|
811
|
+
navigating = false;
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
<%- elsif !bibliography.empty? -%>
|
|
356
815
|
// Check for bibliography entry (bib-KEY)
|
|
357
816
|
var bibMatch = hash.match(/^bib-(.+)/);
|
|
358
817
|
if (bibMatch) {
|
|
@@ -364,7 +823,7 @@
|
|
|
364
823
|
navigating = false;
|
|
365
824
|
return;
|
|
366
825
|
}
|
|
367
|
-
|
|
826
|
+
<%- end -%>
|
|
368
827
|
|
|
369
828
|
// Check for footnote links (fn: or fnref:)
|
|
370
829
|
var fnMatch = hash.match(/^(?:fn|fnref):(.+?)--/);
|
|
@@ -382,12 +841,31 @@
|
|
|
382
841
|
// Check for deep link (chapter--heading)
|
|
383
842
|
var parts = hash.split('--');
|
|
384
843
|
if (parts.length >= 2) {
|
|
844
|
+
<%- if multilang -%>
|
|
845
|
+
// In multilang, slug is "lang--chapter" so join first two parts
|
|
846
|
+
var chSlug = parts[0] + '--' + parts[1];
|
|
847
|
+
if (chapters.indexOf(chSlug) !== -1) {
|
|
848
|
+
showChapterAndScroll(chSlug, hash);
|
|
849
|
+
navigating = false;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
// Try 3 parts: "lang--chapter--heading" → chapter is "lang--chapter"
|
|
853
|
+
if (parts.length >= 3) {
|
|
854
|
+
chSlug = parts[0] + '--' + parts[1];
|
|
855
|
+
if (chapters.indexOf(chSlug) !== -1) {
|
|
856
|
+
showChapterAndScroll(chSlug, hash);
|
|
857
|
+
navigating = false;
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
<%- else -%>
|
|
385
862
|
var chSlug = parts[0];
|
|
386
863
|
if (chapters.indexOf(chSlug) !== -1) {
|
|
387
864
|
showChapterAndScroll(chSlug, hash);
|
|
388
865
|
navigating = false;
|
|
389
866
|
return;
|
|
390
867
|
}
|
|
868
|
+
<%- end -%>
|
|
391
869
|
}
|
|
392
870
|
|
|
393
871
|
// Fallback
|
|
@@ -429,8 +907,14 @@
|
|
|
429
907
|
// Remove existing match count badges
|
|
430
908
|
document.querySelectorAll('.search-match-count').forEach(function(el) { el.remove(); });
|
|
431
909
|
|
|
910
|
+
<%- if multilang -%>
|
|
911
|
+
var tocContainer = document.querySelector('.toc .lang-content[data-lang="' + currentLang + '"]');
|
|
912
|
+
var items = tocContainer ? tocContainer.querySelectorAll('.toc-chapter') : [];
|
|
913
|
+
var partItems = tocContainer ? tocContainer.querySelectorAll('.toc-part, .toc-appendix') : [];
|
|
914
|
+
<%- else -%>
|
|
432
915
|
var items = document.querySelectorAll('.toc-chapter');
|
|
433
916
|
var partItems = document.querySelectorAll('.toc-part, .toc-appendix');
|
|
917
|
+
<%- end -%>
|
|
434
918
|
|
|
435
919
|
if (!query || query.length < 2) {
|
|
436
920
|
// Show all items when query is too short
|
|
@@ -597,6 +1081,9 @@
|
|
|
597
1081
|
window.renderSpecialBlocks = function() { if (currentChapter) renderSpecialBlocks(currentChapter); };
|
|
598
1082
|
|
|
599
1083
|
// Initialize (replace initial entry so first back goes to previous page, not same page)
|
|
1084
|
+
<%- if multilang -%>
|
|
1085
|
+
initLang();
|
|
1086
|
+
<%- end -%>
|
|
600
1087
|
navigating = true;
|
|
601
1088
|
handleHash();
|
|
602
1089
|
history.replaceState(null, '', location.hash || '#' + (chapters[0] || ''));
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ligarb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ligarb contributors
|
|
@@ -137,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
138
|
version: '0'
|
|
139
139
|
requirements: []
|
|
140
|
-
rubygems_version: 4.0.
|
|
140
|
+
rubygems_version: 4.0.6
|
|
141
141
|
specification_version: 4
|
|
142
142
|
summary: Generate a single-page HTML book from Markdown files
|
|
143
143
|
test_files: []
|