ligarb 0.4.0 → 0.6.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 +682 -0
- data/assets/review.js +684 -0
- data/assets/serve.js +97 -0
- data/assets/style.css +103 -0
- data/lib/ligarb/asset_manager.rb +17 -2
- data/lib/ligarb/builder.rb +176 -1
- data/lib/ligarb/chapter.rb +32 -4
- data/lib/ligarb/claude_runner.rb +313 -0
- data/lib/ligarb/cli.rb +207 -9
- data/lib/ligarb/config.rb +25 -1
- data/lib/ligarb/initializer.rb +20 -0
- data/lib/ligarb/inotify.rb +75 -0
- data/lib/ligarb/review_store.rb +133 -0
- data/lib/ligarb/server.rb +1218 -0
- data/lib/ligarb/template.rb +7 -1
- data/lib/ligarb/version.rb +1 -1
- data/lib/ligarb/writer.rb +96 -18
- data/templates/book.html.erb +226 -32
- metadata +36 -1
data/templates/book.html.erb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="<%= language %>">
|
|
2
|
+
<html lang="<%= h(language) %>">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title><%= title %></title>
|
|
6
|
+
<title><%= h(title) %></title>
|
|
7
7
|
<%- if ai_generated -%>
|
|
8
8
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
9
9
|
<meta name="robots" content="noai, noimageai">
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
<div class="sidebar-header-top">
|
|
31
31
|
<%- cover_chapter = chapters.find(&:cover?) -%>
|
|
32
32
|
<%- if cover_chapter -%>
|
|
33
|
-
<h1 class="book-title"><a href="#<%= cover_chapter.slug %>" onclick="showChapter('<%= cover_chapter.slug %>'); return false;"><%= title %></a></h1>
|
|
33
|
+
<h1 class="book-title"><a href="#<%= cover_chapter.slug %>" onclick="showChapter('<%= cover_chapter.slug %>'); return false;"><%= h(title) %></a></h1>
|
|
34
34
|
<%- else -%>
|
|
35
|
-
<h1 class="book-title"><%= title %></h1>
|
|
35
|
+
<h1 class="book-title"><%= h(title) %></h1>
|
|
36
36
|
<%- end -%>
|
|
37
37
|
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">☾</button>
|
|
38
38
|
</div>
|
|
39
39
|
<%- unless author.empty? -%>
|
|
40
|
-
<p class="book-author"><%= author %></p>
|
|
40
|
+
<p class="book-author"><%= h(author) %></p>
|
|
41
41
|
<%- end -%>
|
|
42
42
|
<%- if ai_generated -%>
|
|
43
43
|
<span class="ai-badge"><%= language == 'ja' ? 'AI 生成' : 'AI Generated' %></span>
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
<%- when :cover -%>
|
|
55
55
|
<%- when :chapter -%>
|
|
56
56
|
<li class="toc-chapter" data-chapter="<%= node.chapter.slug %>">
|
|
57
|
-
<a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= node.chapter.display_title %></a>
|
|
57
|
+
<a href="#<%= node.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.display_title) %></a>
|
|
58
58
|
<%- sub_headings = node.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
59
59
|
<%- unless sub_headings.empty? -%>
|
|
60
60
|
<ul>
|
|
61
61
|
<%- sub_headings.each do |heading| -%>
|
|
62
62
|
<li>
|
|
63
|
-
<a href="#<%= node.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= node.chapter.slug %>', '<%= node.chapter.slug %>--<%= heading.id %>')"><%= heading.display_text %></a>
|
|
63
|
+
<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>
|
|
64
64
|
</li>
|
|
65
65
|
<%- end -%>
|
|
66
66
|
</ul>
|
|
@@ -68,17 +68,17 @@
|
|
|
68
68
|
</li>
|
|
69
69
|
<%- when :part -%>
|
|
70
70
|
<li class="toc-part">
|
|
71
|
-
<a href="#<%= node.chapter.slug %>" class="toc-part-title" onclick="showChapter('<%= node.chapter.slug %>')"><%= node.chapter.title %></a>
|
|
71
|
+
<a href="#<%= node.chapter.slug %>" class="toc-part-title" onclick="showChapter('<%= node.chapter.slug %>')"><%= h(node.chapter.title) %></a>
|
|
72
72
|
<ul>
|
|
73
73
|
<%- (node.children || []).each do |child| -%>
|
|
74
74
|
<li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
|
|
75
|
-
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= child.chapter.display_title %></a>
|
|
75
|
+
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
|
|
76
76
|
<%- sub_headings = child.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
77
77
|
<%- unless sub_headings.empty? -%>
|
|
78
78
|
<ul>
|
|
79
79
|
<%- sub_headings.each do |heading| -%>
|
|
80
80
|
<li>
|
|
81
|
-
<a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= heading.display_text %></a>
|
|
81
|
+
<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>
|
|
82
82
|
</li>
|
|
83
83
|
<%- end -%>
|
|
84
84
|
</ul>
|
|
@@ -89,17 +89,17 @@
|
|
|
89
89
|
</li>
|
|
90
90
|
<%- when :appendix_group -%>
|
|
91
91
|
<li class="toc-appendix">
|
|
92
|
-
<span class="toc-appendix-title"><%= appendix_label %></span>
|
|
92
|
+
<span class="toc-appendix-title"><%= h(appendix_label) %></span>
|
|
93
93
|
<ul>
|
|
94
94
|
<%- (node.children || []).each do |child| -%>
|
|
95
95
|
<li class="toc-chapter" data-chapter="<%= child.chapter.slug %>">
|
|
96
|
-
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= child.chapter.display_title %></a>
|
|
96
|
+
<a href="#<%= child.chapter.slug %>" class="toc-h1" onclick="showChapter('<%= child.chapter.slug %>')"><%= h(child.chapter.display_title) %></a>
|
|
97
97
|
<%- sub_headings = child.chapter.headings.select { |h| h.level >= 2 } -%>
|
|
98
98
|
<%- unless sub_headings.empty? -%>
|
|
99
99
|
<ul>
|
|
100
100
|
<%- sub_headings.each do |heading| -%>
|
|
101
101
|
<li>
|
|
102
|
-
<a href="#<%= child.chapter.slug %>--<%= heading.id %>" class="toc-h<%= heading.level %>" onclick="showChapterAndScroll('<%= child.chapter.slug %>', '<%= child.chapter.slug %>--<%= heading.id %>')"><%= heading.display_text %></a>
|
|
102
|
+
<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>
|
|
103
103
|
</li>
|
|
104
104
|
<%- end -%>
|
|
105
105
|
</ul>
|
|
@@ -110,6 +110,11 @@
|
|
|
110
110
|
</li>
|
|
111
111
|
<%- end -%>
|
|
112
112
|
<%- end -%>
|
|
113
|
+
<%- unless bibliography.empty? -%>
|
|
114
|
+
<li class="toc-chapter" data-chapter="__bibliography__">
|
|
115
|
+
<a href="#__bibliography__" class="toc-h1" onclick="showChapter('__bibliography__')"><%= language == 'ja' ? '参考文献' : 'Bibliography' %></a>
|
|
116
|
+
</li>
|
|
117
|
+
<%- end -%>
|
|
113
118
|
<%- unless index_tree.empty? -%>
|
|
114
119
|
<li class="toc-chapter" data-chapter="__index__">
|
|
115
120
|
<a href="#__index__" class="toc-h1" onclick="showChapter('__index__')"><%= language == 'ja' ? '索引' : 'Index' %></a>
|
|
@@ -127,26 +132,36 @@
|
|
|
127
132
|
<%= chapter.html %>
|
|
128
133
|
<%- if repository && !chapter.part_title? && !chapter.cover? -%>
|
|
129
134
|
<div class="edit-link">
|
|
130
|
-
<a href="<%= repository.chomp('/') %>/blob/HEAD/<%= chapter.relative_path %>" target="_blank" rel="noopener">View on GitHub</a>
|
|
135
|
+
<a href="<%= h(repository.chomp('/')) %>/blob/HEAD/<%= h(chapter.relative_path) %>" target="_blank" rel="noopener">View on GitHub</a>
|
|
131
136
|
</div>
|
|
132
137
|
<%- end -%>
|
|
133
138
|
<%- if footer && !chapter.part_title? && !chapter.cover? -%>
|
|
134
|
-
<div class="chapter-footer"><%= footer %></div>
|
|
139
|
+
<div class="chapter-footer"><%= h(footer) %></div>
|
|
135
140
|
<%- end -%>
|
|
136
141
|
<%- unless chapter.cover? -%>
|
|
137
142
|
<nav class="chapter-nav">
|
|
138
143
|
<%- if idx > 0 -%>
|
|
139
|
-
<a href="#" class="nav-prev" onclick="showChapter('<%= chapters[idx-1].slug %>'); return false;">← <%= chapters[idx-1].display_title %></a>
|
|
144
|
+
<a href="#" class="nav-prev" onclick="showChapter('<%= chapters[idx-1].slug %>'); return false;">← <%= h(chapters[idx-1].display_title) %></a>
|
|
140
145
|
<%- else -%>
|
|
141
146
|
<span></span>
|
|
142
147
|
<%- end -%>
|
|
143
148
|
<%- if idx < chapters.size - 1 -%>
|
|
144
|
-
<a href="#" class="nav-next" onclick="showChapter('<%= chapters[idx+1].slug %>'); return false;"><%= chapters[idx+1].display_title %> →</a>
|
|
149
|
+
<a href="#" class="nav-next" onclick="showChapter('<%= chapters[idx+1].slug %>'); return false;"><%= h(chapters[idx+1].display_title) %> →</a>
|
|
145
150
|
<%- end -%>
|
|
146
151
|
</nav>
|
|
147
152
|
<%- end -%>
|
|
148
153
|
</section>
|
|
149
154
|
<%- end -%>
|
|
155
|
+
<%- unless bibliography.empty? -%>
|
|
156
|
+
<section class="chapter bibliography-chapter" id="chapter-__bibliography__" style="display: none;">
|
|
157
|
+
<h1><%= language == 'ja' ? '参考文献' : 'Bibliography' %></h1>
|
|
158
|
+
<ul class="bibliography-list">
|
|
159
|
+
<%- bibliography.each do |entry| -%>
|
|
160
|
+
<li id="bib-<%= h(entry[:key]) %>"><span class="bib-label">[<%= h(entry[:label]) %>]</span> <%= entry[:formatted_html] %></li>
|
|
161
|
+
<%- end -%>
|
|
162
|
+
</ul>
|
|
163
|
+
</section>
|
|
164
|
+
<%- end -%>
|
|
150
165
|
<%- unless index_tree.empty? -%>
|
|
151
166
|
<section class="chapter index-chapter" id="chapter-__index__" style="display: none;">
|
|
152
167
|
<h1><%= language == 'ja' ? '索引' : 'Index' %></h1>
|
|
@@ -155,12 +170,12 @@
|
|
|
155
170
|
<h2 class="index-letter"><%= letter %></h2>
|
|
156
171
|
<dl class="index-entries">
|
|
157
172
|
<%- index_tree[letter].each do |item| -%>
|
|
158
|
-
<dt><%= item[:term] %></dt>
|
|
173
|
+
<dt><%= h(item[:term]) %></dt>
|
|
159
174
|
<%- item[:refs].each do |ref| -%>
|
|
160
175
|
<dd><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
|
|
161
176
|
<%- end -%>
|
|
162
177
|
<%- item[:children].each do |child| -%>
|
|
163
|
-
<dt class="index-sub"><%= child[:term] %></dt>
|
|
178
|
+
<dt class="index-sub"><%= h(child[:term]) %></dt>
|
|
164
179
|
<%- child[:refs].each do |ref| -%>
|
|
165
180
|
<dd class="index-sub"><a href="#<%= ref[:anchor_id] %>" onclick="showChapterAndScroll('<%= ref[:chapter_slug] %>', '<%= ref[:anchor_id] %>'); return false;"><%= ref[:chapter_title] %></a></dd>
|
|
166
181
|
<%- end -%>
|
|
@@ -175,10 +190,12 @@
|
|
|
175
190
|
|
|
176
191
|
<script>
|
|
177
192
|
(function() {
|
|
178
|
-
var chapters = [<%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__index__"' unless index_tree.empty? %>];
|
|
193
|
+
var chapters = [<%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__bibliography__"' unless bibliography.empty? %><%= ', "__index__"' unless index_tree.empty? %>];
|
|
179
194
|
var currentChapter = null;
|
|
180
195
|
|
|
181
|
-
|
|
196
|
+
var navigating = false; // flag to suppress pushState during popstate/initial load
|
|
197
|
+
|
|
198
|
+
function showChapter(slug, hash) {
|
|
182
199
|
chapters.forEach(function(ch) {
|
|
183
200
|
var el = document.getElementById('chapter-' + ch);
|
|
184
201
|
if (el) el.style.display = (ch === slug) ? 'block' : 'none';
|
|
@@ -195,7 +212,10 @@
|
|
|
195
212
|
});
|
|
196
213
|
|
|
197
214
|
currentChapter = slug;
|
|
198
|
-
|
|
215
|
+
var newHash = '#' + (hash || slug);
|
|
216
|
+
if (!navigating) {
|
|
217
|
+
history.pushState(null, '', newHash);
|
|
218
|
+
}
|
|
199
219
|
window.scrollTo(0, 0);
|
|
200
220
|
|
|
201
221
|
// Re-apply highlight if search is active
|
|
@@ -228,39 +248,133 @@
|
|
|
228
248
|
}
|
|
229
249
|
});
|
|
230
250
|
}
|
|
251
|
+
if (typeof functionPlot !== 'undefined') {
|
|
252
|
+
section.querySelectorAll('.functionplot[data-plot]').forEach(function(el) {
|
|
253
|
+
if (el.childNodes.length > 0) return;
|
|
254
|
+
try {
|
|
255
|
+
var spec = el.getAttribute('data-plot');
|
|
256
|
+
var opts = parseFunctionPlotSpec(spec);
|
|
257
|
+
opts.target = el;
|
|
258
|
+
functionPlot(opts);
|
|
259
|
+
} catch(e) {
|
|
260
|
+
el.textContent = 'Error rendering plot: ' + e.message;
|
|
261
|
+
el.style.color = 'red';
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Parse functionplot spec: lines of "y = expr" and "key: value" options
|
|
268
|
+
function parseFunctionPlotSpec(spec) {
|
|
269
|
+
var lines = spec.split('\n');
|
|
270
|
+
var fns = [];
|
|
271
|
+
var opts = {width: 600, height: 400};
|
|
272
|
+
lines.forEach(function(line) {
|
|
273
|
+
line = line.trim();
|
|
274
|
+
if (!line) return;
|
|
275
|
+
// Option lines: key: value
|
|
276
|
+
var optMatch = line.match(/^(xrange|yrange|range|width|height|title|grid)\s*:\s*(.+)$/i);
|
|
277
|
+
if (optMatch) {
|
|
278
|
+
var key = optMatch[1].toLowerCase();
|
|
279
|
+
var val = optMatch[2].trim();
|
|
280
|
+
if (key === 'range' || key === 'xrange') {
|
|
281
|
+
opts.xAxis = {domain: parseRange(val)};
|
|
282
|
+
} else if (key === 'yrange') {
|
|
283
|
+
opts.yAxis = {domain: parseRange(val)};
|
|
284
|
+
} else if (key === 'width') {
|
|
285
|
+
opts.width = parseInt(val, 10);
|
|
286
|
+
} else if (key === 'height') {
|
|
287
|
+
opts.height = parseInt(val, 10);
|
|
288
|
+
} else if (key === 'title') {
|
|
289
|
+
opts.title = val;
|
|
290
|
+
} else if (key === 'grid') {
|
|
291
|
+
opts.grid = val === 'true';
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Function lines: y = expr, r = expr (polar), x = expr (implicit), or bare expr
|
|
296
|
+
var fnMatch = line.match(/^y\s*=\s*(.+)$/);
|
|
297
|
+
if (fnMatch) { fns.push({fn: fnMatch[1].trim()}); return; }
|
|
298
|
+
var polarMatch = line.match(/^r\s*=\s*(.+)$/);
|
|
299
|
+
if (polarMatch) { fns.push({r: polarMatch[1].trim(), fnType: 'polar', graphType: 'polyline'}); return; }
|
|
300
|
+
var paramMatch = line.match(/^parametric\s*:\s*(.+?)\s*,\s*(.+)$/);
|
|
301
|
+
if (paramMatch) { fns.push({x: paramMatch[1].trim(), y: paramMatch[2].trim(), fnType: 'parametric', graphType: 'polyline'}); return; }
|
|
302
|
+
// Bare expression treated as y = expr
|
|
303
|
+
fns.push({fn: line});
|
|
304
|
+
});
|
|
305
|
+
opts.data = fns;
|
|
306
|
+
return opts;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function parseRange(str) {
|
|
310
|
+
var m = str.match(/\[?\s*(-?[\d.eE+piPI*\/]+)\s*,\s*(-?[\d.eE+piPI*\/]+)\s*\]?/);
|
|
311
|
+
if (!m) return [-10, 10];
|
|
312
|
+
return [evalRangeNum(m[1]), evalRangeNum(m[2])];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function evalRangeNum(s) {
|
|
316
|
+
s = s.replace(/pi/gi, String(Math.PI));
|
|
317
|
+
try { return Function('"use strict"; return (' + s + ')')(); }
|
|
318
|
+
catch(e) { return parseFloat(s) || 0; }
|
|
231
319
|
}
|
|
232
320
|
|
|
233
321
|
function showChapterAndScroll(slug, headingId) {
|
|
234
|
-
showChapter(slug);
|
|
235
|
-
history.replaceState(null, '', '#' + headingId);
|
|
322
|
+
showChapter(slug, headingId);
|
|
236
323
|
setTimeout(function() {
|
|
237
324
|
var target = document.getElementById(headingId);
|
|
238
|
-
if (target)
|
|
325
|
+
if (target) {
|
|
326
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
327
|
+
highlightElement(target);
|
|
328
|
+
}
|
|
239
329
|
}, 50);
|
|
240
330
|
}
|
|
241
331
|
|
|
242
|
-
|
|
332
|
+
function highlightElement(el) {
|
|
333
|
+
var prev = document.querySelector('.ligarb-highlight');
|
|
334
|
+
if (prev) prev.classList.remove('ligarb-highlight');
|
|
335
|
+
el.classList.add('ligarb-highlight');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Navigate to chapter/heading based on current URL hash
|
|
243
339
|
function handleHash() {
|
|
340
|
+
navigating = true;
|
|
244
341
|
var hash = location.hash.replace('#', '');
|
|
245
342
|
if (!hash) {
|
|
246
343
|
if (chapters.length > 0) showChapter(chapters[0]);
|
|
344
|
+
navigating = false;
|
|
247
345
|
return;
|
|
248
346
|
}
|
|
249
347
|
|
|
250
348
|
// Check if hash matches a chapter slug directly
|
|
251
349
|
if (chapters.indexOf(hash) !== -1) {
|
|
252
350
|
showChapter(hash);
|
|
351
|
+
navigating = false;
|
|
253
352
|
return;
|
|
254
353
|
}
|
|
255
354
|
|
|
355
|
+
<%- unless bibliography.empty? -%>
|
|
356
|
+
// Check for bibliography entry (bib-KEY)
|
|
357
|
+
var bibMatch = hash.match(/^bib-(.+)/);
|
|
358
|
+
if (bibMatch) {
|
|
359
|
+
showChapter('__bibliography__', hash);
|
|
360
|
+
setTimeout(function() {
|
|
361
|
+
var target = document.getElementById(hash);
|
|
362
|
+
if (target) target.scrollIntoView({ behavior: 'smooth' });
|
|
363
|
+
}, 50);
|
|
364
|
+
navigating = false;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
<%- end -%>
|
|
368
|
+
|
|
256
369
|
// Check for footnote links (fn: or fnref:)
|
|
257
370
|
var fnMatch = hash.match(/^(?:fn|fnref):(.+?)--/);
|
|
258
371
|
if (fnMatch) {
|
|
259
372
|
var fnChSlug = fnMatch[1];
|
|
260
373
|
if (chapters.indexOf(fnChSlug) !== -1) {
|
|
261
|
-
if (currentChapter !== fnChSlug) showChapter(fnChSlug);
|
|
374
|
+
if (currentChapter !== fnChSlug) showChapter(fnChSlug, hash);
|
|
262
375
|
var fnTarget = document.getElementById(hash);
|
|
263
376
|
if (fnTarget) fnTarget.scrollIntoView({ behavior: 'smooth' });
|
|
377
|
+
navigating = false;
|
|
264
378
|
return;
|
|
265
379
|
}
|
|
266
380
|
}
|
|
@@ -271,26 +385,98 @@
|
|
|
271
385
|
var chSlug = parts[0];
|
|
272
386
|
if (chapters.indexOf(chSlug) !== -1) {
|
|
273
387
|
showChapterAndScroll(chSlug, hash);
|
|
388
|
+
navigating = false;
|
|
274
389
|
return;
|
|
275
390
|
}
|
|
276
391
|
}
|
|
277
392
|
|
|
278
393
|
// Fallback
|
|
279
394
|
if (chapters.length > 0) showChapter(chapters[0]);
|
|
395
|
+
navigating = false;
|
|
280
396
|
}
|
|
281
397
|
|
|
282
398
|
// TOC search + content highlight
|
|
283
399
|
var searchInput = document.getElementById('toc-search');
|
|
284
400
|
var clearBtn = document.getElementById('search-clear');
|
|
285
401
|
|
|
402
|
+
// Build text cache for full-text search (lazy, built on first search)
|
|
403
|
+
var chapterTextCache = null;
|
|
404
|
+
function buildTextCache() {
|
|
405
|
+
if (chapterTextCache) return;
|
|
406
|
+
chapterTextCache = {};
|
|
407
|
+
chapters.forEach(function(slug) {
|
|
408
|
+
var section = document.getElementById('chapter-' + slug);
|
|
409
|
+
if (section) {
|
|
410
|
+
chapterTextCache[slug] = section.textContent.toLowerCase();
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function countMatches(text, query) {
|
|
416
|
+
var count = 0;
|
|
417
|
+
var idx = text.indexOf(query);
|
|
418
|
+
while (idx !== -1) {
|
|
419
|
+
count++;
|
|
420
|
+
idx = text.indexOf(query, idx + query.length);
|
|
421
|
+
}
|
|
422
|
+
return count;
|
|
423
|
+
}
|
|
424
|
+
|
|
286
425
|
function updateSearch() {
|
|
287
426
|
var query = searchInput.value.toLowerCase();
|
|
427
|
+
clearBtn.style.display = query ? 'block' : 'none';
|
|
428
|
+
|
|
429
|
+
// Remove existing match count badges
|
|
430
|
+
document.querySelectorAll('.search-match-count').forEach(function(el) { el.remove(); });
|
|
431
|
+
|
|
288
432
|
var items = document.querySelectorAll('.toc-chapter');
|
|
433
|
+
var partItems = document.querySelectorAll('.toc-part, .toc-appendix');
|
|
434
|
+
|
|
435
|
+
if (!query || query.length < 2) {
|
|
436
|
+
// Show all items when query is too short
|
|
437
|
+
items.forEach(function(item) { item.style.display = ''; });
|
|
438
|
+
partItems.forEach(function(item) { item.style.display = ''; });
|
|
439
|
+
highlightContent('');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
buildTextCache();
|
|
444
|
+
|
|
445
|
+
// Track which chapters match (content or TOC text)
|
|
446
|
+
var matchingSlugs = {};
|
|
289
447
|
items.forEach(function(item) {
|
|
290
|
-
var
|
|
291
|
-
|
|
448
|
+
var slug = item.dataset.chapter;
|
|
449
|
+
var tocText = item.textContent.toLowerCase();
|
|
450
|
+
var contentText = chapterTextCache[slug] || '';
|
|
451
|
+
var tocMatch = tocText.indexOf(query) !== -1;
|
|
452
|
+
var contentMatch = contentText.indexOf(query) !== -1;
|
|
453
|
+
|
|
454
|
+
if (tocMatch || contentMatch) {
|
|
455
|
+
item.style.display = '';
|
|
456
|
+
matchingSlugs[slug] = true;
|
|
457
|
+
// Show match count badge for content matches
|
|
458
|
+
if (contentMatch) {
|
|
459
|
+
var count = countMatches(contentText, query);
|
|
460
|
+
var badge = document.createElement('span');
|
|
461
|
+
badge.className = 'search-match-count';
|
|
462
|
+
badge.textContent = count;
|
|
463
|
+
var link = item.querySelector('a');
|
|
464
|
+
if (link) link.appendChild(badge);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
item.style.display = 'none';
|
|
468
|
+
}
|
|
292
469
|
});
|
|
293
|
-
|
|
470
|
+
|
|
471
|
+
// Show/hide part and appendix headers based on whether they have visible children
|
|
472
|
+
partItems.forEach(function(item) {
|
|
473
|
+
var children = item.querySelectorAll('.toc-chapter');
|
|
474
|
+
var hasVisible = Array.prototype.some.call(children, function(ch) {
|
|
475
|
+
return ch.style.display !== 'none';
|
|
476
|
+
});
|
|
477
|
+
item.style.display = hasVisible ? '' : 'none';
|
|
478
|
+
});
|
|
479
|
+
|
|
294
480
|
highlightContent(searchInput.value);
|
|
295
481
|
}
|
|
296
482
|
|
|
@@ -410,9 +596,12 @@
|
|
|
410
596
|
window.showChapterAndScroll = showChapterAndScroll;
|
|
411
597
|
window.renderSpecialBlocks = function() { if (currentChapter) renderSpecialBlocks(currentChapter); };
|
|
412
598
|
|
|
413
|
-
// Initialize
|
|
599
|
+
// Initialize (replace initial entry so first back goes to previous page, not same page)
|
|
600
|
+
navigating = true;
|
|
414
601
|
handleHash();
|
|
415
|
-
|
|
602
|
+
history.replaceState(null, '', location.hash || '#' + (chapters[0] || ''));
|
|
603
|
+
navigating = false;
|
|
604
|
+
window.addEventListener('popstate', handleHash);
|
|
416
605
|
})();
|
|
417
606
|
</script>
|
|
418
607
|
<%- if assets.need?(:highlight) -%>
|
|
@@ -430,5 +619,10 @@ renderSpecialBlocks();
|
|
|
430
619
|
<script src="js/katex.min.js"></script>
|
|
431
620
|
<script>renderSpecialBlocks();</script>
|
|
432
621
|
<%- end -%>
|
|
622
|
+
<%- if assets.need?(:functionplot) -%>
|
|
623
|
+
<script src="js/d3.min.js"></script>
|
|
624
|
+
<script src="js/function-plot.min.js"></script>
|
|
625
|
+
<script>renderSpecialBlocks();</script>
|
|
626
|
+
<%- end -%>
|
|
433
627
|
</body>
|
|
434
628
|
</html>
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ligarb contributors
|
|
@@ -37,6 +37,34 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '1.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webrick
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.7'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.7'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: fiddle
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.1'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.1'
|
|
40
68
|
- !ruby/object:Gem::Dependency
|
|
41
69
|
name: rake
|
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -72,14 +100,21 @@ executables:
|
|
|
72
100
|
extensions: []
|
|
73
101
|
extra_rdoc_files: []
|
|
74
102
|
files:
|
|
103
|
+
- assets/review.css
|
|
104
|
+
- assets/review.js
|
|
105
|
+
- assets/serve.js
|
|
75
106
|
- assets/style.css
|
|
76
107
|
- exe/ligarb
|
|
77
108
|
- lib/ligarb/asset_manager.rb
|
|
78
109
|
- lib/ligarb/builder.rb
|
|
79
110
|
- lib/ligarb/chapter.rb
|
|
111
|
+
- lib/ligarb/claude_runner.rb
|
|
80
112
|
- lib/ligarb/cli.rb
|
|
81
113
|
- lib/ligarb/config.rb
|
|
82
114
|
- lib/ligarb/initializer.rb
|
|
115
|
+
- lib/ligarb/inotify.rb
|
|
116
|
+
- lib/ligarb/review_store.rb
|
|
117
|
+
- lib/ligarb/server.rb
|
|
83
118
|
- lib/ligarb/template.rb
|
|
84
119
|
- lib/ligarb/version.rb
|
|
85
120
|
- lib/ligarb/writer.rb
|