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 +4 -4
- data/assets/feedback.css +123 -0
- data/assets/feedback.js +194 -0
- data/assets/serve.js +13 -1
- data/assets/style.css +96 -0
- data/lib/ligarb/builder.rb +104 -11
- data/lib/ligarb/chapter.rb +5 -3
- data/lib/ligarb/claude_runner.rb +2 -1
- data/lib/ligarb/cli.rb +11 -571
- data/lib/ligarb/config.rb +128 -2
- data/lib/ligarb/github_review.rb +245 -0
- data/lib/ligarb/review_store.rb +1 -0
- data/lib/ligarb/template.rb +87 -1
- data/lib/ligarb/version.rb +1 -1
- data/lib/ligarb/writer.rb +2 -2
- data/templates/book.html.erb +520 -10
- data/templates/github_review/.github/ISSUE_TEMPLATE/book-feedback.yml +43 -0
- data/templates/github_review/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/templates/github_review/.github/workflows/build-check.yml +25 -0
- data/templates/github_review/.github/workflows/claude-feedback.yml +128 -0
- data/templates/github_review/.github/workflows/claude-pr-mention.yml +121 -0
- data/templates/github_review/.github/workflows/deploy-book.yml +45 -0
- data/templates/github_review/SETUP.md +167 -0
- data/templates/github_review/SETUP.sh +49 -0
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a98936274fe3188801b1787fba5e58076a04ff6d5d4cb94183d3bad03643c25
|
|
4
|
+
data.tar.gz: 017da362d7d557808ac62478fc0ac5e95ac50ae68dda6143a76137da27ca5264
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ca765bc7bab404fe43800686716eabb03e7f1d6b4b3c5210c7dd8ae610539a3fb71d929e621a176fdbae98b2054f1ca3b394b15fe92bdc73dc29a79f7abf7ae
|
|
7
|
+
data.tar.gz: 8d3e2954073174b3a6779aea90d6167a378c5c0fbfb7e05635f016d44061f1266db60868f8505f896759e4462729698c6895fec4956d69531089993eded8531c
|
data/assets/feedback.css
ADDED
|
@@ -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
|
+
}
|
data/assets/feedback.js
ADDED
|
@@ -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 += '"e=' + 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)
|
|
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)
|
|
@@ -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|
|
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/claude_runner.rb
CHANGED
|
@@ -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
|
|
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
|
|