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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e697032ff94ff3c4a910343f29d6a977bd386afaa92dbcf4dbb0ca0108e70b2
4
- data.tar.gz: d142ecba5fb0cb7287f2206e31309c40dff8016b216f79053669f5fa8bc59c47
3
+ metadata.gz: 9771ee497c353bffb143092dae4b8631c72587e264ca3e6081e614e582de9d1b
4
+ data.tar.gz: 1def484d79741c7e88d657c485799762e7bf6909cd3bf9a3118aac00ac3cf32f
5
5
  SHA512:
6
- metadata.gz: 6a24c126ae03426cd2361885244882a08b5e666cd112fa77b437225a48d954adb0ec2ea59d3c620b317a9549ea78efa17bfcde367c8aed9bd69be79f6b7f45b9
7
- data.tar.gz: b77a95db5ec9f3541349d999b9c936ddad7b65df844a3ec15f0059698e05f8973cabde8ac2b3fe60598acfa43ab866c70162bee0c1ce46bc7350be41ec195253
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="([^"]*)")?>[\s\S]*?<<<\n([\s\S]*?)\n===\n([\s\S]*?)\n>>>\s*<\/patch>/);
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, '&lt;') + '</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
+ }
@@ -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
- fetch_with_redirects(URI(response["location"]), limit - 1)
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
@@ -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)
@@ -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
- @slug = File.basename(path, ".md").gsub(/[^a-zA-Z0-9_-]/, "-")
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
- %(<div class="mermaid">\n#{raw}</div>)
175
+ escaped = raw.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
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
- result = protected.gsub(/(?<!\$)\$(?!\$)(?!\s)(.+?)(?<!\s)(?<!\$)\$(?!\$)/m) do
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