ligarb 0.5.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/review.css +17 -0
- data/assets/review.js +5 -2
- data/assets/serve.js +33 -0
- data/assets/style.css +132 -0
- data/lib/ligarb/asset_manager.rb +17 -2
- data/lib/ligarb/builder.rb +82 -10
- data/lib/ligarb/chapter.rb +11 -5
- data/lib/ligarb/claude_runner.rb +140 -12
- data/lib/ligarb/cli.rb +161 -6
- data/lib/ligarb/config.rb +105 -2
- data/lib/ligarb/initializer.rb +20 -0
- data/lib/ligarb/review_store.rb +72 -50
- data/lib/ligarb/server.rb +145 -18
- data/lib/ligarb/template.rb +60 -0
- data/lib/ligarb/version.rb +1 -1
- data/lib/ligarb/writer.rb +26 -1
- data/templates/book.html.erb +598 -29
- 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/review.css
CHANGED
|
@@ -203,6 +203,23 @@
|
|
|
203
203
|
margin-top: 4px;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
.ligarb-file-path {
|
|
207
|
+
font-size: 10px;
|
|
208
|
+
color: var(--color-text-muted, #999);
|
|
209
|
+
margin-top: 2px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.ligarb-file-path summary {
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
opacity: 0.5;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.ligarb-file-path code {
|
|
218
|
+
font-size: 10px;
|
|
219
|
+
word-break: break-all;
|
|
220
|
+
user-select: all;
|
|
221
|
+
}
|
|
222
|
+
|
|
206
223
|
/* ── Messages ── */
|
|
207
224
|
|
|
208
225
|
.ligarb-messages {
|
data/assets/review.js
CHANGED
|
@@ -269,9 +269,11 @@
|
|
|
269
269
|
|
|
270
270
|
function renderReview(review) {
|
|
271
271
|
var ctx = review.context || {};
|
|
272
|
+
var filePath = review.file_path ? '<details class="ligarb-file-path"><summary>debug</summary><code>' + escapeHTML(review.file_path) + '</code></details>' : '';
|
|
272
273
|
panel.querySelector('.ligarb-context').innerHTML =
|
|
273
274
|
'<div class="ligarb-selected-text">"' + escapeHTML(ctx.selected_text || '') + '"</div>' +
|
|
274
|
-
'<div class="ligarb-meta">Chapter: ' + escapeHTML(ctx.chapter_slug || '') + '</div>'
|
|
275
|
+
'<div class="ligarb-meta">Chapter: ' + escapeHTML(ctx.chapter_slug || '') + '</div>' +
|
|
276
|
+
filePath;
|
|
275
277
|
|
|
276
278
|
var msgsEl = panel.querySelector('.ligarb-messages');
|
|
277
279
|
msgsEl.innerHTML = '';
|
|
@@ -331,7 +333,7 @@
|
|
|
331
333
|
var patches = '';
|
|
332
334
|
|
|
333
335
|
parts.forEach(function(part) {
|
|
334
|
-
var m = part.match(/<patch(?:\s+file="([^"]*)")
|
|
336
|
+
var m = part.match(/<patch(?:\s+file="([^"]*)")?>\s*<<<[ \t]*\r?\n([\s\S]*?)\r?\n===[ \t]*\r?\n([\s\S]*?)\r?\n>>>[ \t]*\s*<\/patch>/);
|
|
335
337
|
if (m) {
|
|
336
338
|
hasPatch = true;
|
|
337
339
|
var fileLabel = m[1] ? '<div class="ligarb-patch-file">' + escapeHTML(m[1]) + '</div>' : '';
|
|
@@ -505,6 +507,7 @@
|
|
|
505
507
|
}
|
|
506
508
|
|
|
507
509
|
body.innerHTML = '';
|
|
510
|
+
reviews.reverse();
|
|
508
511
|
reviews.forEach(function(r) {
|
|
509
512
|
var item = document.createElement('div');
|
|
510
513
|
item.className = 'ligarb-list-item ligarb-list-' + r.status;
|
data/assets/serve.js
CHANGED
|
@@ -53,6 +53,39 @@
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
window.scrollTo(0, scrollY);
|
|
56
|
+
|
|
57
|
+
// Re-initialize syntax highlighting and special blocks
|
|
58
|
+
if (typeof hljs !== 'undefined') hljs.highlightAll();
|
|
59
|
+
if (typeof mermaid !== 'undefined') {
|
|
60
|
+
var unrendered = oldMain.querySelectorAll('.mermaid:not([data-processed])');
|
|
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
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (typeof katex !== 'undefined') {
|
|
76
|
+
oldMain.querySelectorAll('.math-block[data-math]').forEach(function(el) {
|
|
77
|
+
if (el.childNodes.length === 0) {
|
|
78
|
+
try { katex.render(el.getAttribute('data-math'), el, {displayMode: true, throwOnError: false}); }
|
|
79
|
+
catch(e) { el.textContent = el.getAttribute('data-math'); }
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
oldMain.querySelectorAll('.math-inline[data-math]').forEach(function(el) {
|
|
83
|
+
if (el.childNodes.length === 0) {
|
|
84
|
+
try { katex.render(el.getAttribute('data-math'), el, {displayMode: false, throwOnError: false}); }
|
|
85
|
+
catch(e) { el.textContent = el.getAttribute('data-math'); }
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
56
89
|
}
|
|
57
90
|
refreshing = false;
|
|
58
91
|
hideReloadButton();
|
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);
|
|
@@ -447,6 +452,7 @@ mark.search-highlight {
|
|
|
447
452
|
--color-accent-light: #1c3a5c;
|
|
448
453
|
--color-code-bg: #0f172a;
|
|
449
454
|
--color-code-border: #2d3748;
|
|
455
|
+
--color-highlight: rgba(255, 243, 205, 0.15);
|
|
450
456
|
}
|
|
451
457
|
|
|
452
458
|
[data-theme="dark"] .admonition-note {
|
|
@@ -658,6 +664,17 @@ mark.search-highlight {
|
|
|
658
664
|
text-decoration: underline;
|
|
659
665
|
}
|
|
660
666
|
|
|
667
|
+
.ligarb-highlight {
|
|
668
|
+
border-radius: 4px;
|
|
669
|
+
padding: 4px 6px;
|
|
670
|
+
animation: ligarb-highlight-fade 3s ease-out forwards;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@keyframes ligarb-highlight-fade {
|
|
674
|
+
0% { background: var(--color-highlight, #fff3cd); }
|
|
675
|
+
100% { background: transparent; }
|
|
676
|
+
}
|
|
677
|
+
|
|
661
678
|
.cite-ref {
|
|
662
679
|
font-size: 0.8em;
|
|
663
680
|
}
|
|
@@ -757,6 +774,38 @@ mark.search-highlight {
|
|
|
757
774
|
border-color: #854d0e;
|
|
758
775
|
}
|
|
759
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
|
+
|
|
760
809
|
.chapter-footer {
|
|
761
810
|
margin-top: 2rem;
|
|
762
811
|
padding: 0.5rem 0.75rem;
|
|
@@ -765,6 +814,65 @@ mark.search-highlight {
|
|
|
765
814
|
border-top: 1px solid var(--color-border);
|
|
766
815
|
}
|
|
767
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
|
+
|
|
768
876
|
/* === Print === */
|
|
769
877
|
@media print {
|
|
770
878
|
.sidebar,
|
|
@@ -815,3 +923,27 @@ mark.search-highlight {
|
|
|
815
923
|
}
|
|
816
924
|
}
|
|
817
925
|
}
|
|
926
|
+
|
|
927
|
+
/* === Mermaid source toggle === */
|
|
928
|
+
details.mermaid-source {
|
|
929
|
+
margin-top: 0.3em;
|
|
930
|
+
font-size: 0.8em;
|
|
931
|
+
}
|
|
932
|
+
details.mermaid-source summary {
|
|
933
|
+
cursor: pointer;
|
|
934
|
+
color: #888;
|
|
935
|
+
user-select: none;
|
|
936
|
+
}
|
|
937
|
+
details.mermaid-source pre {
|
|
938
|
+
margin-top: 0.3em;
|
|
939
|
+
padding: 0.5em;
|
|
940
|
+
background: #f5f5f5;
|
|
941
|
+
border: 1px solid #ddd;
|
|
942
|
+
border-radius: 4px;
|
|
943
|
+
overflow-x: auto;
|
|
944
|
+
white-space: pre-wrap;
|
|
945
|
+
}
|
|
946
|
+
.dark-mode details.mermaid-source pre {
|
|
947
|
+
background: #2a2a2a;
|
|
948
|
+
border-color: #444;
|
|
949
|
+
}
|
data/lib/ligarb/asset_manager.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Ligarb
|
|
|
9
9
|
class AssetManager
|
|
10
10
|
ASSETS = {
|
|
11
11
|
highlight: {
|
|
12
|
-
fence_pattern: /language-(?!mermaid|math)(\w+)/,
|
|
12
|
+
fence_pattern: /language-(?!mermaid|math|functionplot)(\w+)/,
|
|
13
13
|
files: {
|
|
14
14
|
"js/highlight.min.js" => "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js",
|
|
15
15
|
"css/highlight.css" => "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css",
|
|
@@ -28,6 +28,13 @@ module Ligarb
|
|
|
28
28
|
"css/katex.min.css" => "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css",
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
|
+
functionplot: {
|
|
32
|
+
fence_pattern: /class="functionplot"/,
|
|
33
|
+
files: {
|
|
34
|
+
"js/d3.min.js" => "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js",
|
|
35
|
+
"js/function-plot.min.js" => "https://cdn.jsdelivr.net/npm/function-plot@1/dist/function-plot.js",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
31
38
|
}.freeze
|
|
32
39
|
|
|
33
40
|
def initialize(output_path)
|
|
@@ -60,6 +67,8 @@ module Ligarb
|
|
|
60
67
|
|
|
61
68
|
private
|
|
62
69
|
|
|
70
|
+
MAX_ASSET_SIZE = 5 * 1024 * 1024 # 5MB
|
|
71
|
+
|
|
63
72
|
def download(url, dest)
|
|
64
73
|
FileUtils.mkdir_p(File.dirname(dest))
|
|
65
74
|
$stderr.print "Downloading #{File.basename(dest)}... "
|
|
@@ -68,6 +77,9 @@ module Ligarb
|
|
|
68
77
|
response = fetch_with_redirects(uri)
|
|
69
78
|
|
|
70
79
|
if response.is_a?(Net::HTTPSuccess)
|
|
80
|
+
if response.body.bytesize > MAX_ASSET_SIZE
|
|
81
|
+
abort "Error: downloaded file too large: #{File.basename(dest)} (#{response.body.bytesize} bytes, limit #{MAX_ASSET_SIZE})"
|
|
82
|
+
end
|
|
71
83
|
File.write(dest, response.body)
|
|
72
84
|
$stderr.puts "done"
|
|
73
85
|
else
|
|
@@ -77,11 +89,14 @@ module Ligarb
|
|
|
77
89
|
|
|
78
90
|
def fetch_with_redirects(uri, limit = 5)
|
|
79
91
|
raise "Too many redirects" if limit == 0
|
|
92
|
+
raise "Only HTTPS URLs are supported for asset downloads" unless uri.scheme == "https"
|
|
80
93
|
|
|
81
94
|
response = Net::HTTP.get_response(uri)
|
|
82
95
|
case response
|
|
83
96
|
when Net::HTTPRedirection
|
|
84
|
-
|
|
97
|
+
new_uri = URI(response["location"])
|
|
98
|
+
raise "Redirect to non-HTTPS URL: #{new_uri}" unless new_uri.scheme == "https"
|
|
99
|
+
fetch_with_redirects(new_uri, limit - 1)
|
|
85
100
|
else
|
|
86
101
|
response
|
|
87
102
|
end
|
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
|
|
|
@@ -166,14 +167,18 @@ module Ligarb
|
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
def convert_special_code_blocks(html)
|
|
169
|
-
html.gsub(%r{<pre><code class="language-(mermaid|math)">(.*?)</code></pre>}m) do
|
|
170
|
+
html.gsub(%r{<pre><code class="language-(mermaid|math|functionplot)">(.*?)</code></pre>}m) do
|
|
170
171
|
lang = $1
|
|
171
172
|
raw = decode_entities($2)
|
|
172
173
|
case lang
|
|
173
174
|
when "mermaid"
|
|
174
|
-
|
|
175
|
+
escaped = raw.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
176
|
+
%(<div class="mermaid">\n#{raw}</div>) +
|
|
177
|
+
%(<details class="mermaid-source"><summary>mermaid source</summary><pre>#{escaped}</pre></details>)
|
|
175
178
|
when "math"
|
|
176
179
|
%(<div class="math-block" data-math="#{encode_attr(raw)}"></div>)
|
|
180
|
+
when "functionplot"
|
|
181
|
+
%(<div class="functionplot" data-plot="#{encode_attr(raw)}"></div>)
|
|
177
182
|
end
|
|
178
183
|
end
|
|
179
184
|
end
|
|
@@ -187,7 +192,8 @@ module Ligarb
|
|
|
187
192
|
end
|
|
188
193
|
|
|
189
194
|
# Convert $...$ to inline math (exclude $$, and $ followed/preceded by space)
|
|
190
|
-
|
|
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
|
|
191
197
|
raw = decode_entities($1)
|
|
192
198
|
%(<span class="math-inline" data-math="#{encode_attr(raw)}"></span>)
|
|
193
199
|
end
|