ligarb 0.6.0 → 0.8.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: 4aa971885dcefac0f440b02705cc3cb0adb88fcbf21c38cc23bed207887f30dc
4
- data.tar.gz: 155c53e158b727d4e7c5ef5b012b980e32106e54e737c8c55b4f3953c08caeb2
3
+ metadata.gz: 9a98936274fe3188801b1787fba5e58076a04ff6d5d4cb94183d3bad03643c25
4
+ data.tar.gz: 017da362d7d557808ac62478fc0ac5e95ac50ae68dda6143a76137da27ca5264
5
5
  SHA512:
6
- metadata.gz: a04d10091801975a7029c762a81fed663b685710634a29320b8f6cd0a149297627e1c6fc2d28efd0c69d6fd44576ec5c535764b88fdcd62411ad0ec4f06dae35
7
- data.tar.gz: b60a0faef98596347c5c4e6b25b2737a00a47b9998124a562d2d1b918648e1ad46560008e84426cc216d286d6e432d528e14d9f3f1ac3be595cab1463c5fbc54
6
+ metadata.gz: 4ca765bc7bab404fe43800686716eabb03e7f1d6b4b3c5210c7dd8ae610539a3fb71d929e621a176fdbae98b2054f1ca3b394b15fe92bdc73dc29a79f7abf7ae
7
+ data.tar.gz: 8d3e2954073174b3a6779aea90d6167a378c5c0fbfb7e05635f016d44061f1266db60868f8505f896759e4462729698c6895fec4956d69531089993eded8531c
@@ -0,0 +1,123 @@
1
+ /* ligarb public feedback UI — "Report as issue"
2
+ Static, backend-free: builds a prefilled GitHub issue URL and opens it.
3
+ Colors reuse the book's CSS variables so dark mode works automatically. */
4
+
5
+ #ligarb-fb-btn {
6
+ position: absolute;
7
+ z-index: 9999;
8
+ display: none;
9
+ padding: 6px 12px;
10
+ font-family: var(--font-sans);
11
+ font-size: 13px;
12
+ font-weight: 600;
13
+ line-height: 1;
14
+ color: #fff;
15
+ background: var(--color-accent);
16
+ border: none;
17
+ border-radius: 6px;
18
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
19
+ cursor: pointer;
20
+ }
21
+
22
+ #ligarb-fb-btn:hover { filter: brightness(1.08); }
23
+
24
+ #ligarb-fb-panel {
25
+ position: fixed;
26
+ z-index: 10000;
27
+ display: none;
28
+ flex-direction: column;
29
+ gap: 10px;
30
+ width: 320px;
31
+ max-width: calc(100vw - 24px);
32
+ padding: 16px;
33
+ font-family: var(--font-sans);
34
+ color: var(--color-text);
35
+ background: var(--color-bg);
36
+ border: 1px solid var(--color-border);
37
+ border-radius: 10px;
38
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.25);
39
+ }
40
+
41
+ #ligarb-fb-panel.open { display: flex; }
42
+
43
+ .ligarb-fb-title {
44
+ font-size: 14px;
45
+ font-weight: 700;
46
+ }
47
+
48
+ .ligarb-fb-quote {
49
+ max-height: 96px;
50
+ overflow-y: auto;
51
+ padding: 8px 10px;
52
+ font-size: 12px;
53
+ line-height: 1.5;
54
+ color: var(--color-text-muted);
55
+ background: var(--color-code-bg);
56
+ border: 1px solid var(--color-code-border);
57
+ border-radius: 6px;
58
+ white-space: pre-wrap;
59
+ word-break: break-word;
60
+ }
61
+
62
+ .ligarb-fb-label {
63
+ font-size: 12px;
64
+ font-weight: 600;
65
+ color: var(--color-text-muted);
66
+ }
67
+
68
+ #ligarb-fb-type,
69
+ #ligarb-fb-details {
70
+ width: 100%;
71
+ padding: 7px 9px;
72
+ font-family: inherit;
73
+ font-size: 13px;
74
+ color: var(--color-text);
75
+ background: var(--color-bg);
76
+ border: 1px solid var(--color-border);
77
+ border-radius: 6px;
78
+ }
79
+
80
+ #ligarb-fb-details { resize: vertical; min-height: 60px; }
81
+
82
+ #ligarb-fb-type:focus,
83
+ #ligarb-fb-details:focus {
84
+ outline: none;
85
+ border-color: var(--color-accent);
86
+ box-shadow: 0 0 0 2px var(--color-accent-light);
87
+ }
88
+
89
+ .ligarb-fb-actions {
90
+ display: flex;
91
+ justify-content: flex-end;
92
+ gap: 8px;
93
+ }
94
+
95
+ .ligarb-fb-submit,
96
+ .ligarb-fb-cancel {
97
+ padding: 7px 14px;
98
+ font-family: inherit;
99
+ font-size: 13px;
100
+ font-weight: 600;
101
+ border-radius: 6px;
102
+ cursor: pointer;
103
+ }
104
+
105
+ .ligarb-fb-submit {
106
+ color: #fff;
107
+ background: var(--color-accent);
108
+ border: none;
109
+ }
110
+
111
+ .ligarb-fb-submit:hover { filter: brightness(1.08); }
112
+
113
+ .ligarb-fb-cancel {
114
+ color: var(--color-text-muted);
115
+ background: transparent;
116
+ border: 1px solid var(--color-border);
117
+ }
118
+
119
+ .ligarb-fb-cancel:hover { background: var(--color-sidebar-hover); }
120
+
121
+ @media print {
122
+ #ligarb-fb-btn, #ligarb-fb-panel { display: none !important; }
123
+ }
@@ -0,0 +1,194 @@
1
+ // ligarb public feedback UI — "Report as issue"
2
+ // Static and backend-free: on text selection it offers a button that builds a
3
+ // prefilled GitHub Issue form URL (from data-src-* + window.location + the
4
+ // configured repository) and opens it in a new tab. No API calls, no tokens.
5
+ (function() {
6
+ 'use strict';
7
+
8
+ var cfg = window._ligarbReview;
9
+ if (!cfg || !cfg.base) return;
10
+
11
+ var ISSUE_BASE = cfg.base.replace(/\/+$/, '') + '/issues/new';
12
+ var TEMPLATE = cfg.issueTemplate || 'book-feedback.yml';
13
+ var LABELS = Array.isArray(cfg.labels) ? cfg.labels : [];
14
+
15
+ // Keep the whole URL comfortably under common limits (~8KB).
16
+ var MAX_QUOTE = 1200;
17
+ var MAX_URL = 7000;
18
+
19
+ // Short label -> value stored in the issue (the form keeps its own dropdown;
20
+ // we fold the reader's choice into `details` since dropdown prefill is flaky).
21
+ var TYPES = [
22
+ { value: '', label: '種類を選択 / Type…' },
23
+ { value: '誤り (error)', label: '誤り / Error' },
24
+ { value: 'わかりにくい (unclear)', label: 'わかりにくい / Unclear' },
25
+ { value: '疑問 (question)', label: '疑問 / Question' }
26
+ ];
27
+
28
+ function enc(s) { return encodeURIComponent(s == null ? '' : s); }
29
+
30
+ function escapeHTML(str) {
31
+ var div = document.createElement('div');
32
+ div.textContent = str == null ? '' : str;
33
+ return div.innerHTML;
34
+ }
35
+
36
+ // ── Selection capture (mirrors the serve review UI) ──
37
+
38
+ var btn = document.createElement('button');
39
+ btn.id = 'ligarb-fb-btn';
40
+ btn.type = 'button';
41
+ btn.textContent = 'Report as issue';
42
+ document.body.appendChild(btn);
43
+
44
+ var current = null; // { quote, chapterTitle, srcFile, headingText }
45
+
46
+ document.addEventListener('mouseup', function(e) {
47
+ if (e.target.closest('#ligarb-fb-btn, #ligarb-fb-panel')) return;
48
+
49
+ var sel = window.getSelection();
50
+ if (!sel || sel.isCollapsed || !sel.toString().trim()) {
51
+ hideButton();
52
+ return;
53
+ }
54
+
55
+ var anchor = sel.anchorNode;
56
+ var chapter = anchor && anchor.parentElement ? anchor.parentElement.closest('.chapter') : null;
57
+ if (!chapter || !chapter.dataset.srcFile) {
58
+ hideButton();
59
+ return;
60
+ }
61
+
62
+ current = {
63
+ quote: sel.toString().trim(),
64
+ chapterTitle: chapter.dataset.srcTitle || '',
65
+ srcFile: chapter.dataset.srcFile || '',
66
+ headingText: nearestHeadingText(chapter, sel)
67
+ };
68
+
69
+ var rect = sel.getRangeAt(0).getBoundingClientRect();
70
+ btn.style.display = 'block';
71
+ btn.style.top = (window.scrollY + rect.bottom + 6) + 'px';
72
+ btn.style.left = (window.scrollX + rect.left) + 'px';
73
+ });
74
+
75
+ function hideButton() {
76
+ btn.style.display = 'none';
77
+ current = null;
78
+ }
79
+
80
+ function nearestHeadingText(chapter, sel) {
81
+ var headings = chapter.querySelectorAll('h1[id], h2[id], h3[id]');
82
+ if (!headings.length) return '';
83
+ var range = sel.getRangeAt(0);
84
+ for (var i = headings.length - 1; i >= 0; i--) {
85
+ var hr = document.createRange();
86
+ hr.selectNode(headings[i]);
87
+ if (range.compareBoundaryPoints(Range.START_TO_START, hr) >= 0) {
88
+ return headings[i].textContent.trim();
89
+ }
90
+ }
91
+ return headings[0].textContent.trim();
92
+ }
93
+
94
+ btn.addEventListener('click', function(e) {
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ if (!current) return;
98
+ openPanel(current);
99
+ hideButton();
100
+ });
101
+
102
+ // ── Report panel ──
103
+
104
+ var panel = null;
105
+
106
+ function buildPanel() {
107
+ if (panel) return;
108
+ panel = document.createElement('div');
109
+ panel.id = 'ligarb-fb-panel';
110
+ var options = TYPES.map(function(t) {
111
+ return '<option value="' + escapeHTML(t.value) + '">' + escapeHTML(t.label) + '</option>';
112
+ }).join('');
113
+ panel.innerHTML =
114
+ '<div class="ligarb-fb-title">Report as issue</div>' +
115
+ '<div class="ligarb-fb-quote"></div>' +
116
+ '<label class="ligarb-fb-label" for="ligarb-fb-type">種類 / Type</label>' +
117
+ '<select id="ligarb-fb-type">' + options + '</select>' +
118
+ '<label class="ligarb-fb-label" for="ligarb-fb-details">コメント / Comment</label>' +
119
+ '<textarea id="ligarb-fb-details" placeholder="何が問題か、どう直すとよいか…"></textarea>' +
120
+ '<div class="ligarb-fb-actions">' +
121
+ '<button type="button" class="ligarb-fb-cancel">Cancel</button>' +
122
+ '<button type="button" class="ligarb-fb-submit">Report as issue</button>' +
123
+ '</div>';
124
+ document.body.appendChild(panel);
125
+
126
+ panel.querySelector('.ligarb-fb-cancel').addEventListener('click', closePanel);
127
+ panel.querySelector('.ligarb-fb-submit').addEventListener('click', submit);
128
+ }
129
+
130
+ function openPanel(ctx) {
131
+ buildPanel();
132
+ panel._ctx = ctx;
133
+ panel.querySelector('.ligarb-fb-quote').textContent = ctx.quote;
134
+ panel.querySelector('#ligarb-fb-type').value = '';
135
+ panel.querySelector('#ligarb-fb-details').value = '';
136
+
137
+ // Center-ish, then clamp into the viewport.
138
+ panel.classList.add('open');
139
+ var w = panel.offsetWidth, h = panel.offsetHeight;
140
+ var top = Math.max(12, (window.innerHeight - h) / 2);
141
+ var left = Math.max(12, (window.innerWidth - w) / 2);
142
+ panel.style.top = top + 'px';
143
+ panel.style.left = left + 'px';
144
+ panel.querySelector('#ligarb-fb-details').focus();
145
+ }
146
+
147
+ function closePanel() {
148
+ if (panel) panel.classList.remove('open');
149
+ }
150
+
151
+ function submit() {
152
+ var ctx = panel._ctx;
153
+ if (!ctx) return;
154
+ var type = panel.querySelector('#ligarb-fb-type').value;
155
+ var comment = panel.querySelector('#ligarb-fb-details').value.trim();
156
+
157
+ var locationLines = [];
158
+ var section = [ctx.chapterTitle, ctx.headingText].filter(Boolean).join(' › ');
159
+ if (section) locationLines.push('章/節: ' + section);
160
+ if (ctx.srcFile) locationLines.push('ソース: ' + ctx.srcFile);
161
+ locationLines.push('URL: ' + window.location.href);
162
+
163
+ var detailsLines = [];
164
+ if (type) detailsLines.push('種類: ' + type);
165
+ if (comment) detailsLines.push(comment);
166
+
167
+ var quote = ctx.quote;
168
+ if (quote.length > MAX_QUOTE) quote = quote.slice(0, MAX_QUOTE) + ' …(truncated)';
169
+
170
+ var url = buildUrl(locationLines.join('\n'), quote, detailsLines.join('\n\n'));
171
+
172
+ // If still too long, progressively shorten the quote.
173
+ while (url.length > MAX_URL && quote.length > 80) {
174
+ quote = quote.slice(0, Math.floor(quote.length / 2)) + ' …(truncated)';
175
+ url = buildUrl(locationLines.join('\n'), quote, detailsLines.join('\n\n'));
176
+ }
177
+
178
+ window.open(url, '_blank', 'noopener');
179
+ closePanel();
180
+ }
181
+
182
+ function buildUrl(location, quote, details) {
183
+ var params = 'template=' + enc(TEMPLATE);
184
+ if (LABELS.length) params += '&labels=' + enc(LABELS.join(','));
185
+ params += '&location=' + enc(location);
186
+ params += '&quote=' + enc(quote);
187
+ params += '&details=' + enc(details);
188
+ return ISSUE_BASE + '?' + params;
189
+ }
190
+
191
+ document.addEventListener('keydown', function(e) {
192
+ if (e.key === 'Escape') closePanel();
193
+ });
194
+ })();
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) mermaid.run({nodes: unrendered});
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
+ }
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,
@@ -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)
@@ -35,7 +41,8 @@ module Ligarb
35
41
  html = Template.new.render(config: @config, chapters: all_chapters,
36
42
  structure: structure, assets: assets,
37
43
  index_entries: index_entries,
38
- bibliography: bibliography)
44
+ bibliography: bibliography,
45
+ github_review: github_review_data(@config))
39
46
 
40
47
  FileUtils.mkdir_p(@config.output_path)
41
48
  output_file = File.join(@config.output_path, "index.html")
@@ -49,30 +56,98 @@ module Ligarb
49
56
 
50
57
  private
51
58
 
59
+ def build_multilang
60
+ hub_data = @config.instance_variable_get(:@translations_data)
61
+ hub_base = File.dirname(@config_path)
62
+ output_dir = hub_data.fetch("output_dir", "build")
63
+ output_path = File.join(hub_base, output_dir)
64
+
65
+ langs = []
66
+ all_lang_chapters = []
67
+
68
+ @config.translations.each do |trans|
69
+ child_config = Config.new(trans.config_path, parent_data: hub_data)
70
+ prefix = "#{trans.lang}--"
71
+ lang_data = build_language_data(trans.lang, child_config, prefix)
72
+ langs << lang_data
73
+ all_lang_chapters.concat(lang_data[:chapters])
74
+ end
75
+
76
+ assets = AssetManager.new(output_path)
77
+ assets.detect(all_lang_chapters)
78
+ assets.provision!
79
+
80
+ gr_config = langs.first && langs.first[:config]
81
+ html = Template.new.render_multilang(langs: langs, assets: assets,
82
+ hub_data: hub_data,
83
+ github_review: gr_config && github_review_data(gr_config))
84
+
85
+ FileUtils.mkdir_p(output_path)
86
+ output_file = File.join(output_path, "index.html")
87
+ File.write(output_file, html)
88
+
89
+ # Copy images from hub base dir
90
+ images_dir = File.join(hub_base, "images")
91
+ if Dir.exist?(images_dir)
92
+ dest = File.join(output_path, "images")
93
+ FileUtils.mkdir_p(dest)
94
+ Dir.glob(File.join(images_dir, "*")).each { |img| FileUtils.cp(img, dest) }
95
+ end
96
+
97
+ puts "Built #{output_file}"
98
+ langs.each { |ld| puts " #{ld[:lang]}: #{ld[:chapters].size} chapter(s)" }
99
+ end
100
+
101
+ def build_language_data(lang, config, slug_prefix)
102
+ @config = config
103
+ structure = load_structure(slug_prefix: slug_prefix)
104
+ all_chapters = collect_all_chapters(structure)
105
+ resolve_cross_references(all_chapters)
106
+ assign_relative_paths(all_chapters) if config.repository
107
+
108
+ index_entries = all_chapters.flat_map { |ch|
109
+ ch.index_entries.map { |e|
110
+ e.class.new(term: e.term, display_text: e.display_text,
111
+ chapter_slug: e.chapter_slug, anchor_id: e.anchor_id)
112
+ }
113
+ }
114
+
115
+ bibliography = resolve_citations!(all_chapters)
116
+
117
+ {
118
+ lang: lang,
119
+ config: config,
120
+ chapters: all_chapters,
121
+ structure: structure,
122
+ index_entries: index_entries,
123
+ bibliography: bibliography,
124
+ }
125
+ end
126
+
52
127
  # StructNode mirrors Config::StructEntry but holds loaded Chapter objects
53
128
  StructNode = Struct.new(:type, :chapter, :children, keyword_init: true)
54
129
 
55
- def load_structure
130
+ def load_structure(slug_prefix: nil)
56
131
  chapter_num = 0
57
132
  appendix_num = 0
58
133
 
59
134
  @config.structure.map do |entry|
60
135
  case entry.type
61
136
  when :cover
62
- ch = load_chapter(entry.path)
137
+ ch = load_chapter(entry.path, slug_prefix: slug_prefix)
63
138
  ch.cover = true
64
139
  StructNode.new(type: :cover, chapter: ch)
65
140
  when :chapter
66
141
  chapter_num += 1
67
- ch = load_chapter(entry.path)
142
+ ch = load_chapter(entry.path, slug_prefix: slug_prefix)
68
143
  ch.number = chapter_num if @config.chapter_numbers
69
144
  StructNode.new(type: :chapter, chapter: ch)
70
145
  when :part
71
- part_ch = load_chapter(entry.path)
146
+ part_ch = load_chapter(entry.path, slug_prefix: slug_prefix)
72
147
  part_ch.part_title = true
73
148
  children = (entry.children || []).map do |child|
74
149
  chapter_num += 1
75
- ch = load_chapter(child.path)
150
+ ch = load_chapter(child.path, slug_prefix: slug_prefix)
76
151
  ch.number = chapter_num if @config.chapter_numbers
77
152
  StructNode.new(type: :chapter, chapter: ch)
78
153
  end
@@ -80,7 +155,7 @@ module Ligarb
80
155
  when :appendix_group
81
156
  children = (entry.children || []).map do |child|
82
157
  appendix_num += 1
83
- ch = load_chapter(child.path)
158
+ ch = load_chapter(child.path, slug_prefix: slug_prefix)
84
159
  letter = ("A".ord + appendix_num - 1).chr
85
160
  ch.appendix_letter = letter if @config.chapter_numbers
86
161
  StructNode.new(type: :chapter, chapter: ch)
@@ -90,11 +165,11 @@ module Ligarb
90
165
  end
91
166
  end
92
167
 
93
- def load_chapter(path)
168
+ def load_chapter(path, slug_prefix: nil)
94
169
  unless File.exist?(path)
95
170
  abort "Error: chapter not found: #{path}"
96
171
  end
97
- Chapter.new(path, @config.base_dir)
172
+ Chapter.new(path, @config.base_dir, slug_prefix: slug_prefix)
98
173
  end
99
174
 
100
175
  def collect_all_chapters(structure)
@@ -126,6 +201,24 @@ module Ligarb
126
201
  end
127
202
  end
128
203
 
204
+ # Builds the data passed to the template for the reader feedback UI, or nil
205
+ # when it should not be injected. Requires `repository` (the issues/new base);
206
+ # warns and skips if the UI was requested but no repository is set.
207
+ def github_review_data(config)
208
+ return nil unless config.github_review_enabled?
209
+
210
+ unless config.repository
211
+ warn "Warning: github_review.enabled is true but 'repository' is not set; skipping the reader feedback UI"
212
+ return nil
213
+ end
214
+
215
+ {
216
+ base: config.repository.chomp("/"),
217
+ issue_template: config.github_review_issue_template,
218
+ labels: config.github_review_labels,
219
+ }
220
+ end
221
+
129
222
  def assign_relative_paths(chapters)
130
223
  git_root = find_git_root(@config.base_dir)
131
224
  chapters.each do |ch|
@@ -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
 
@@ -191,7 +192,8 @@ module Ligarb
191
192
  end
192
193
 
193
194
  # Convert $...$ to inline math (exclude $$, and $ followed/preceded by space)
194
- 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
195
197
  raw = decode_entities($1)
196
198
  %(<span class="math-inline" data-math="#{encode_attr(raw)}"></span>)
197
199
  end
@@ -88,8 +88,9 @@ module Ligarb
88
88
  - You may include multiple <patch> blocks for one or more files
89
89
  - If the comment applies to multiple chapters, read all relevant chapters and provide patches for each
90
90
  - When adding citations ([@key]), also add the corresponding entry to the bibliography file
91
- - Use ligarb Markdown features (admonitions, cross-references, index, etc.) where appropriate
91
+ - Use ligarb Markdown features from the spec where appropriate
92
92
  - If no code change is needed (e.g. answering a question), omit the <patch> blocks
93
+ - Refuse changes that would introduce security issues (e.g. injecting scripts, untrusted URLs, or arbitrary HTML)
93
94
  PROMPT
94
95
  end
95
96