ligarb 0.7.0 → 0.8.1

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.
data/lib/ligarb/config.rb CHANGED
@@ -10,7 +10,8 @@ module Ligarb
10
10
  # output_dir is intentionally excluded — it always comes from the
11
11
  # config file that was directly passed to `ligarb build`.
12
12
  INHERITABLE_KEYS = %w[author language chapter_numbers style
13
- repository ai_generated footer bibliography].freeze
13
+ repository ai_generated footer bibliography
14
+ github_review].freeze
14
15
 
15
16
  # Represents a structural entry in the book
16
17
  StructEntry = Struct.new(:type, :path, :children, keyword_init: true)
@@ -25,7 +26,7 @@ module Ligarb
25
26
  attr_reader :title, :author, :language, :output_dir, :base_dir,
26
27
  :chapter_numbers, :structure, :style, :repository,
27
28
  :ai_generated, :footer, :bibliography, :sources,
28
- :translations
29
+ :translations, :github_review
29
30
 
30
31
  def initialize(path, parent_data: nil)
31
32
  @base_dir = File.dirname(File.expand_path(path))
@@ -68,6 +69,7 @@ module Ligarb
68
69
  @ai_generated = data.fetch("ai_generated", false)
69
70
  @footer = data.fetch("footer", nil)
70
71
  @bibliography = data.fetch("bibliography", nil)
72
+ @github_review = data.fetch("github_review", nil)
71
73
  @sources = parse_sources(data.fetch("sources", []))
72
74
  @structure = parse_structure(data["chapters"])
73
75
  @translations = []
@@ -95,6 +97,23 @@ module Ligarb
95
97
  @language == "ja" ? "付録" : "Appendix"
96
98
  end
97
99
 
100
+ # Whether the reader's "Report as issue" feedback UI is requested in book.yml.
101
+ # This reflects intent only; actual injection also requires `repository`
102
+ # (the issues/new base) — the builder warns and skips when it is missing.
103
+ def github_review_enabled?
104
+ @github_review.is_a?(Hash) && @github_review.fetch("enabled", false) == true
105
+ end
106
+
107
+ def github_review_issue_template
108
+ tmpl = @github_review.is_a?(Hash) ? @github_review["issue_template"] : nil
109
+ tmpl && !tmpl.to_s.empty? ? tmpl.to_s : "book-feedback.yml"
110
+ end
111
+
112
+ def github_review_labels
113
+ labels = @github_review.is_a?(Hash) ? @github_review["labels"] : nil
114
+ Array(labels || ["feedback"]).map(&:to_s)
115
+ end
116
+
98
117
  def effective_footer
99
118
  return @footer if @footer
100
119
  return nil unless @ai_generated
@@ -254,6 +273,10 @@ module Ligarb
254
273
  unless data["chapters"].is_a?(Array) && !data["chapters"].empty?
255
274
  abort "Error: 'chapters' must be a non-empty array"
256
275
  end
276
+
277
+ if data.key?("github_review") && !data["github_review"].is_a?(Hash)
278
+ abort "Error: 'github_review' must be a mapping (e.g. { enabled: true })"
279
+ end
257
280
  end
258
281
  end
259
282
  end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "yaml"
5
+
6
+ module Ligarb
7
+ # Sets up the GitHub-based review scaffolding (.github/ + SETUP.md) in a
8
+ # project — the `ligarb setup-github-review` command. This is pure file
9
+ # copying: ligarb never calls Claude or GitHub at runtime. The templates are
10
+ # classified into layers so a future option could split them apart:
11
+ # - generic layer : works without Claude (Pages deploy, build check, forms)
12
+ # - claude layer : opt-in Claude integration (issue/PR handlers, SETUP.md)
13
+ #
14
+ # Re-running OVERWRITES the generated files so a project can follow upstream
15
+ # template changes (e.g. after a ligarb upgrade). The user's own book.yml is
16
+ # never overwritten. Since projects are git repos, `git diff` is the safety
17
+ # net for reviewing/reverting changes after a re-sync.
18
+ class GithubReview
19
+ TEMPLATE_DIR = File.expand_path("../../templates/github_review", __dir__)
20
+
21
+ # Generic layer: no Claude dependency.
22
+ GENERIC_FILES = %w[
23
+ .github/workflows/deploy-book.yml
24
+ .github/workflows/build-check.yml
25
+ .github/ISSUE_TEMPLATE/book-feedback.yml
26
+ .github/ISSUE_TEMPLATE/config.yml
27
+ ].freeze
28
+
29
+ # Claude integration layer: opt-in.
30
+ CLAUDE_FILES = %w[
31
+ .github/workflows/claude-feedback.yml
32
+ .github/workflows/claude-pr-mention.yml
33
+ SETUP.md
34
+ SETUP.sh
35
+ ].freeze
36
+
37
+ TEMPLATE_FILES = (GENERIC_FILES + CLAUDE_FILES).freeze
38
+
39
+ Result = Struct.new(:created, :updated, :unchanged, keyword_init: true)
40
+
41
+ # `ligarb setup-github-review [DIR]` entry point. Sets up the scaffolding in
42
+ # an existing ligarb project (book.yml must exist), enables the reader
43
+ # feedback UI in book.yml, and prints the remaining manual-setup steps.
44
+ # Safe to re-run to pull in updated templates (generated files are
45
+ # overwritten; book.yml is not).
46
+ def self.run(directory = nil)
47
+ target = File.expand_path(directory || ".")
48
+ unless File.exist?(File.join(target, "book.yml"))
49
+ $stderr.puts "Error: book.yml not found in #{target}"
50
+ $stderr.puts "Run 'ligarb init' or 'ligarb write' first, then set up the GitHub review scaffolding."
51
+ exit 1
52
+ end
53
+
54
+ reviewer = new(target)
55
+ # book.yml edits must run BEFORE generate so the templates (SETUP.sh,
56
+ # issue forms, README) are substituted with the resolved repository.
57
+ repository = reviewer.ensure_repository_in_book_yml
58
+ enabled = reviewer.enable_in_book_yml
59
+ readme = reviewer.create_readme_if_absent
60
+ result = reviewer.generate
61
+ reviewer.print_notice(result, repository: repository, enabled: enabled, readme: readme)
62
+ end
63
+
64
+ def initialize(target)
65
+ @target = File.expand_path(target)
66
+ end
67
+
68
+ # Writes all template files into the project, substituting __OWNER__ /
69
+ # __REPO__ from book.yml's `repository:`. Existing files are OVERWRITTEN so a
70
+ # project can follow upstream template changes; only files whose content is
71
+ # already identical are left untouched. Returns a Result listing
72
+ # created/updated/unchanged files.
73
+ def generate
74
+ owner, repo = extract_owner_repo
75
+ created = []
76
+ updated = []
77
+ unchanged = []
78
+
79
+ TEMPLATE_FILES.each do |rel|
80
+ dest = File.join(@target, rel)
81
+ content = render(rel, File.read(File.join(TEMPLATE_DIR, rel)), owner, repo)
82
+
83
+ if !File.exist?(dest)
84
+ write_file(dest, content)
85
+ created << rel
86
+ elsif File.read(dest) == content
87
+ unchanged << rel
88
+ else
89
+ write_file(dest, content)
90
+ updated << rel
91
+ end
92
+ end
93
+
94
+ Result.new(created: created, updated: updated, unchanged: unchanged)
95
+ end
96
+
97
+ # Ensures `github_review.enabled: true` is present in book.yml so the reader
98
+ # feedback UI activates (once `repository` is also set). Appends the key only
99
+ # when absent, preserving existing formatting/comments. Returns :added,
100
+ # :present, or :unsupported (translations hub / unparsable).
101
+ def enable_in_book_yml
102
+ book_yml = File.join(@target, "book.yml")
103
+ data = YAML.safe_load_file(book_yml)
104
+ return :unsupported unless data.is_a?(Hash)
105
+ return :present if data.key?("github_review")
106
+
107
+ content = File.read(book_yml).rstrip
108
+ File.write(book_yml, "#{content}\n\ngithub_review:\n enabled: true\n")
109
+ :added
110
+ end
111
+
112
+ # Seeds a default `repository:` in book.yml when it has none, guessing
113
+ # https://github.com/<os-user>/<dir-name>. This drives __OWNER__/__REPO__
114
+ # substitution and the GH Pages link; the user edits it if the guess is
115
+ # wrong. Returns :added, :present, or :unsupported. (@default_repository is
116
+ # set to the guessed URL when :added, for the notice.)
117
+ def ensure_repository_in_book_yml
118
+ book_yml = File.join(@target, "book.yml")
119
+ data = YAML.safe_load_file(book_yml)
120
+ return :unsupported unless data.is_a?(Hash)
121
+ return :present if data.key?("repository")
122
+
123
+ user = ENV["USER"] || ENV["USERNAME"] || "your-github-account"
124
+ @default_repository = "https://github.com/#{user}/#{File.basename(@target)}"
125
+ content = File.read(book_yml).rstrip
126
+ File.write(book_yml, %(#{content}\n\nrepository: "#{@default_repository}"\n))
127
+ :added
128
+ end
129
+
130
+ # Creates a project README.md that links to the published GitHub Pages site,
131
+ # but only when one does not already exist (the reader's own README is never
132
+ # overwritten). Returns :created or :present.
133
+ def create_readme_if_absent
134
+ readme = File.join(@target, "README.md")
135
+ return :present if File.exist?(readme)
136
+
137
+ owner, repo = extract_owner_repo
138
+ pages = owner && repo ? "https://#{owner}.github.io/#{repo}/" : "https://__OWNER__.github.io/__REPO__/"
139
+ issues = owner && repo ? "https://github.com/#{owner}/#{repo}/issues/new?template=book-feedback.yml" \
140
+ : "https://github.com/__OWNER__/__REPO__/issues/new?template=book-feedback.yml"
141
+ title = book_title.to_s.empty? ? "Book" : book_title
142
+
143
+ File.write(readme, <<~MD)
144
+ # #{title}
145
+
146
+ 📖 **公開版(GitHub Pages)**: #{pages}
147
+
148
+ この本は [ligarb](https://github.com/ko1/ligarb) で生成しています。
149
+
150
+ ## フィードバック
151
+
152
+ 本文の誤り・わかりにくい点・疑問は [Issue](#{issues}) からどうぞ。
153
+ 公開ページでは本文を選択して「Report as issue」からも送れます。
154
+
155
+ ## ローカルでビルド
156
+
157
+ ```bash
158
+ ligarb build # build/index.html を生成
159
+ ligarb serve # ローカルプレビュー
160
+ ```
161
+
162
+ セットアップ手順は [SETUP.md](SETUP.md) を参照してください。
163
+ MD
164
+ :created
165
+ end
166
+
167
+ def print_notice(result, repository:, enabled:, readme:)
168
+ puts "Set up GitHub review scaffolding in #{@target}:"
169
+ result.created.each { |path| puts " created #{path}" }
170
+ result.updated.each { |path| puts " updated #{path}" }
171
+ puts " created README.md (with the GitHub Pages link)" if readme == :created
172
+ case repository
173
+ when :added then puts " updated book.yml (repository: #{@default_repository})"
174
+ end
175
+ case enabled
176
+ when :added then puts " updated book.yml (github_review.enabled: true)"
177
+ when :present then puts " kept book.yml github_review setting"
178
+ end
179
+ unless result.unchanged.empty?
180
+ puts " unchanged #{result.unchanged.size} file(s) already up to date"
181
+ end
182
+ if result.updated.any?
183
+ puts
184
+ puts "Note: existing scaffolding files were overwritten with the latest"
185
+ puts "templates. Review with 'git diff' and revert any local edits you"
186
+ puts "want to keep."
187
+ end
188
+ puts
189
+ puts "Next: edit 'repository:' in book.yml if the guess is wrong, then run"
190
+ puts "the gh CLI quickstart:"
191
+ puts " bash SETUP.sh # repo create + secret + Pages + permissions + labels"
192
+ puts
193
+ puts "It still needs a token (see SETUP.md): generate one with"
194
+ puts "'claude setup-token' before running SETUP.sh (it prompts for it)."
195
+ end
196
+
197
+ private
198
+
199
+ def write_file(dest, content)
200
+ FileUtils.mkdir_p(File.dirname(dest))
201
+ File.write(dest, content)
202
+ File.chmod(0o755, dest) if dest.end_with?(".sh")
203
+ end
204
+
205
+ def book_title
206
+ data = YAML.safe_load_file(File.join(@target, "book.yml"))
207
+ data.is_a?(Hash) ? data["title"] : nil
208
+ rescue StandardError
209
+ nil
210
+ end
211
+
212
+ # Substitute __OWNER__ / __REPO__ placeholders. When repository is unset we
213
+ # leave the placeholders as-is, except config.yml's Discussions link which
214
+ # we comment out so the issue chooser does not show a broken URL.
215
+ def render(rel, content, owner, repo)
216
+ if owner && repo
217
+ content.gsub("__OWNER__", owner).gsub("__REPO__", repo)
218
+ elsif rel == ".github/ISSUE_TEMPLATE/config.yml"
219
+ comment_out_contact_links(content)
220
+ else
221
+ content
222
+ end
223
+ end
224
+
225
+ def comment_out_contact_links(content)
226
+ content.lines.map { |line|
227
+ line.start_with?("contact_links:") || line.start_with?(" ") ? "# #{line}" : line
228
+ }.join
229
+ end
230
+
231
+ # Reads book.yml's `repository:` and extracts [owner, repo].
232
+ # Returns [nil, nil] when book.yml or repository is missing/unparsable.
233
+ def extract_owner_repo
234
+ book_yml = File.join(@target, "book.yml")
235
+ return [nil, nil] unless File.exist?(book_yml)
236
+
237
+ data = YAML.safe_load_file(book_yml)
238
+ url = data.is_a?(Hash) ? data["repository"] : nil
239
+ return [nil, nil] unless url.is_a?(String)
240
+
241
+ m = url.chomp("/").match(%r{github\.com[/:]([^/]+)/([^/]+?)(?:\.git)?\z})
242
+ m ? [m[1], m[2]] : [nil, nil]
243
+ end
244
+ end
245
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
+ require "json"
4
5
 
5
6
  module Ligarb
6
7
  class Template
@@ -12,7 +13,7 @@ module Ligarb
12
13
  @css_path = File.join(ASSETS_DIR, "style.css")
13
14
  end
14
15
 
15
- def render(config:, chapters:, structure:, assets:, index_entries: [], bibliography: [])
16
+ def render(config:, chapters:, structure:, assets:, index_entries: [], bibliography: [], github_review: nil)
16
17
  css = File.read(@css_path)
17
18
  template = File.read(@template_path)
18
19
 
@@ -37,12 +38,13 @@ module Ligarb
37
38
  b.local_variable_set(:bibliography, bibliography)
38
39
  b.local_variable_set(:multilang, false)
39
40
  b.local_variable_set(:langs, [])
41
+ b.local_variable_set(:feedback, load_feedback(github_review))
40
42
 
41
43
  ERB.new(template, trim_mode: "-").result(b)
42
44
  end
43
45
 
44
46
  # Render a single HTML with all languages, switchable via JS
45
- def render_multilang(langs:, assets:, hub_data:)
47
+ def render_multilang(langs:, assets:, hub_data:, github_review: nil)
46
48
  css = File.read(@css_path)
47
49
  template = File.read(@template_path)
48
50
 
@@ -90,12 +92,41 @@ module Ligarb
90
92
  b.local_variable_set(:bibliography, first[:bibliography])
91
93
  b.local_variable_set(:multilang, true)
92
94
  b.local_variable_set(:langs, lang_data)
95
+ b.local_variable_set(:feedback, load_feedback(github_review))
93
96
 
94
97
  ERB.new(template, trim_mode: "-").result(b)
95
98
  end
96
99
 
97
100
  private
98
101
 
102
+ # When github_review (a hash with :base/:issue_template/:labels) is given,
103
+ # returns the inlined feedback assets + a JSON config blob for the template
104
+ # to embed; otherwise nil (the UI is not injected). Inlining keeps the build
105
+ # output self-contained, like the main style.css.
106
+ def load_feedback(github_review)
107
+ return nil unless github_review
108
+
109
+ config_json = JSON.generate(
110
+ base: github_review[:base],
111
+ issueTemplate: github_review[:issue_template],
112
+ labels: github_review[:labels]
113
+ ).gsub("</", '<\/')
114
+
115
+ {
116
+ css: File.read(File.join(ASSETS_DIR, "feedback.css")),
117
+ js: File.read(File.join(ASSETS_DIR, "feedback.js")),
118
+ config_json: config_json,
119
+ }
120
+ end
121
+
122
+ # data-src-* attributes for a chapter <section>, used by the feedback UI to
123
+ # locate the source Markdown. Empty unless the UI is active and the chapter
124
+ # has a resolved source path (set when `repository` is configured).
125
+ def src_attrs(chapter, feedback)
126
+ return "" unless feedback && chapter.relative_path
127
+ %( data-src-file="#{h(chapter.relative_path)}" data-src-title="#{h(chapter.display_title)}")
128
+ end
129
+
99
130
  # HTML-escape helper for ERB templates (available via binding)
100
131
  def h(s)
101
132
  ERB::Util.html_escape(s.to_s)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ligarb
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.1"
5
5
  end
data/lib/ligarb/writer.rb CHANGED
@@ -185,8 +185,8 @@ module Ligarb
185
185
  lines << "Create book.yml first, then each chapter .md file."
186
186
  lines << "Include a cover page for books with 4+ chapters."
187
187
  lines << "Each chapter: substantive content with multiple ## sections."
188
- lines << "Use code blocks, admonitions, mermaid diagrams where appropriate."
189
- lines << "Chapter filenames: 01-topic.md, 02-topic.md, etc."
188
+ lines << "Use code blocks, admonitions, and other ligarb features from the spec where appropriate."
189
+ lines << "Chapter filenames: topic-name.md (e.g. introduction.md, getting-started.md). Do NOT use number prefixes."
190
190
  lines << ""
191
191
  lines << "Create references.bib with real bibliography entries in BibTeX format."
192
192
  lines << "In book.yml, set: bibliography: references.bib"
@@ -16,6 +16,11 @@
16
16
  <%= custom_css %>
17
17
  </style>
18
18
  <%- end -%>
19
+ <%- if feedback -%>
20
+ <style>
21
+ <%= feedback[:css] %>
22
+ </style>
23
+ <%- end -%>
19
24
  <%- if assets.need?(:highlight) -%>
20
25
  <link rel="stylesheet" href="css/highlight.css">
21
26
  <%- end -%>
@@ -247,6 +252,11 @@
247
252
  <main class="content" id="content">
248
253
  <%- if multilang -%>
249
254
  <%- langs.each do |ld| -%>
255
+ <%- ld[:chapters].select(&:cover?).each do |chapter| -%>
256
+ <section class="chapter cover-page" id="chapter-<%= chapter.slug %>" data-lang="<%= ld[:lang] %>" style="display: none;">
257
+ <%= chapter.html %>
258
+ </section>
259
+ <%- end -%>
250
260
  <section class="chapter toc-page" id="chapter-<%= ld[:lang] %>--__toc__" data-lang="<%= ld[:lang] %>" style="display: none;">
251
261
  <h1><%= ld[:language] == 'ja' ? '目次' : 'Contents' %></h1>
252
262
  <nav class="toc-full">
@@ -297,8 +307,9 @@
297
307
  <%- end -%>
298
308
  </nav>
299
309
  </section>
300
- <%- ld[:chapters].each_with_index do |chapter, idx| -%>
301
- <section class="<%= chapter.cover? ? 'chapter cover-page' : 'chapter' %>" id="chapter-<%= chapter.slug %>" data-lang="<%= ld[:lang] %>" style="display: none;">
310
+ <%- nav_chapters = ld[:chapters].reject(&:cover?) -%>
311
+ <%- nav_chapters.each_with_index do |chapter, idx| -%>
312
+ <section class="chapter" id="chapter-<%= chapter.slug %>" data-lang="<%= ld[:lang] %>"<%= src_attrs(chapter, feedback) %> style="display: none;">
302
313
  <%= chapter.html %>
303
314
  <%- if ld[:repository] && !chapter.part_title? && !chapter.cover? -%>
304
315
  <div class="edit-link">
@@ -311,12 +322,12 @@
311
322
  <%- unless chapter.cover? -%>
312
323
  <nav class="chapter-nav">
313
324
  <%- if idx > 0 -%>
314
- <a href="#" class="nav-prev" onclick="showChapter('<%= ld[:chapters][idx-1].slug %>'); return false;">&larr; <%= h(ld[:chapters][idx-1].display_title) %></a>
325
+ <a href="#" class="nav-prev" onclick="showChapter('<%= nav_chapters[idx-1].slug %>'); return false;">&larr; <%= h(nav_chapters[idx-1].display_title) %></a>
315
326
  <%- else -%>
316
327
  <span></span>
317
328
  <%- end -%>
318
- <%- if idx < ld[:chapters].size - 1 -%>
319
- <a href="#" class="nav-next" onclick="showChapter('<%= ld[:chapters][idx+1].slug %>'); return false;"><%= h(ld[:chapters][idx+1].display_title) %> &rarr;</a>
329
+ <%- if idx < nav_chapters.size - 1 -%>
330
+ <a href="#" class="nav-next" onclick="showChapter('<%= nav_chapters[idx+1].slug %>'); return false;"><%= h(nav_chapters[idx+1].display_title) %> &rarr;</a>
320
331
  <%- end -%>
321
332
  </nav>
322
333
  <%- end -%>
@@ -358,6 +369,11 @@
358
369
  <%- end -%>
359
370
  <%- end -%>
360
371
  <%- else -%>
372
+ <%- chapters.select(&:cover?).each do |chapter| -%>
373
+ <section class="chapter cover-page" id="chapter-<%= chapter.slug %>" style="display: none;">
374
+ <%= chapter.html %>
375
+ </section>
376
+ <%- end -%>
361
377
  <section class="chapter toc-page" id="chapter-__toc__" style="display: none;">
362
378
  <h1><%= language == 'ja' ? '目次' : 'Contents' %></h1>
363
379
  <nav class="toc-full">
@@ -408,8 +424,9 @@
408
424
  <%- end -%>
409
425
  </nav>
410
426
  </section>
411
- <%- chapters.each_with_index do |chapter, idx| -%>
412
- <section class="<%= chapter.cover? ? 'chapter cover-page' : 'chapter' %>" id="chapter-<%= chapter.slug %>" style="display: none;">
427
+ <%- nav_chapters = chapters.reject(&:cover?) -%>
428
+ <%- nav_chapters.each_with_index do |chapter, idx| -%>
429
+ <section class="chapter" id="chapter-<%= chapter.slug %>"<%= src_attrs(chapter, feedback) %> style="display: none;">
413
430
  <%= chapter.html %>
414
431
  <%- if repository && !chapter.part_title? && !chapter.cover? -%>
415
432
  <div class="edit-link">
@@ -422,12 +439,12 @@
422
439
  <%- unless chapter.cover? -%>
423
440
  <nav class="chapter-nav">
424
441
  <%- if idx > 0 -%>
425
- <a href="#" class="nav-prev" onclick="showChapter('<%= chapters[idx-1].slug %>'); return false;">&larr; <%= h(chapters[idx-1].display_title) %></a>
442
+ <a href="#" class="nav-prev" onclick="showChapter('<%= nav_chapters[idx-1].slug %>'); return false;">&larr; <%= h(nav_chapters[idx-1].display_title) %></a>
426
443
  <%- else -%>
427
444
  <span></span>
428
445
  <%- end -%>
429
- <%- if idx < chapters.size - 1 -%>
430
- <a href="#" class="nav-next" onclick="showChapter('<%= chapters[idx+1].slug %>'); return false;"><%= h(chapters[idx+1].display_title) %> &rarr;</a>
446
+ <%- if idx < nav_chapters.size - 1 -%>
447
+ <a href="#" class="nav-next" onclick="showChapter('<%= nav_chapters[idx+1].slug %>'); return false;"><%= h(nav_chapters[idx+1].display_title) %> &rarr;</a>
431
448
  <%- end -%>
432
449
  </nav>
433
450
  <%- end -%>
@@ -475,7 +492,8 @@
475
492
  <%- if multilang -%>
476
493
  var langChapters = {
477
494
  <%- langs.each do |ld| -%>
478
- "<%= ld[:lang] %>": ["<%= ld[:lang] %>--__toc__", <%= ld[:chapters].map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ", \"#{ld[:lang]}--__bibliography__\"" unless ld[:bibliography].empty? %><%= ", \"#{ld[:lang]}--__index__\"" unless ld[:index_tree].empty? %>],
495
+ <%- ld_cover = ld[:chapters].find(&:cover?) -%>
496
+ "<%= ld[:lang] %>": [<%= "\"#{ld_cover.slug}\", " if ld_cover %>"<%= ld[:lang] %>--__toc__", <%= ld[:chapters].reject(&:cover?).map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ", \"#{ld[:lang]}--__bibliography__\"" unless ld[:bibliography].empty? %><%= ", \"#{ld[:lang]}--__index__\"" unless ld[:index_tree].empty? %>],
479
497
  <%- end -%>
480
498
  };
481
499
  var allLangs = [<%= langs.map { |ld| "\"#{ld[:lang]}\"" }.join(", ") %>];
@@ -483,7 +501,8 @@
483
501
  if (allLangs.indexOf(currentLang) === -1) currentLang = allLangs[0];
484
502
  var chapters = langChapters[currentLang];
485
503
  <%- else -%>
486
- var chapters = ["__toc__", <%= chapters.map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__bibliography__"' unless bibliography.empty? %><%= ', "__index__"' unless index_tree.empty? %>];
504
+ <%- cover_ch = chapters.find(&:cover?) -%>
505
+ var chapters = [<%= "\"#{cover_ch.slug}\", " if cover_ch %>"__toc__", <%= chapters.reject(&:cover?).map { |c| "\"#{c.slug}\"" }.join(", ") %><%= ', "__bibliography__"' unless bibliography.empty? %><%= ', "__index__"' unless index_tree.empty? %>];
487
506
  <%- end -%>
488
507
  var currentChapter = null;
489
508
 
@@ -1111,5 +1130,11 @@ renderSpecialBlocks();
1111
1130
  <script src="js/function-plot.min.js"></script>
1112
1131
  <script>renderSpecialBlocks();</script>
1113
1132
  <%- end -%>
1133
+ <%- if feedback -%>
1134
+ <script>window._ligarbReview = <%= feedback[:config_json] %>;</script>
1135
+ <script>
1136
+ <%= feedback[:js] %>
1137
+ </script>
1138
+ <%- end -%>
1114
1139
  </body>
1115
1140
  </html>
@@ -0,0 +1,33 @@
1
+ name: 本へのフィードバック
2
+ description: 本文の誤り・わかりにくい点・疑問を報告する
3
+ title: "[feedback] "
4
+ labels: ["feedback"]
5
+ body:
6
+ - type: markdown
7
+ attributes:
8
+ value: |
9
+ 本を読んで気づいた点を報告してください。
10
+ (ligarb の公開ページから「Report as issue」で来た場合、いくつかの項目は自動入力されています。)
11
+ - type: input
12
+ id: location
13
+ attributes:
14
+ label: 対象箇所
15
+ description: 章・節、ソースの Markdown ファイル名、公開サイトの該当 URL など
16
+ placeholder: "例: 第3章 2節 / 03-advanced.md / https://__OWNER__.github.io/__REPO__/#ch3-2"
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: quote
21
+ attributes:
22
+ label: 引用テキスト
23
+ description: 問題の箇所を本文から引用してください
24
+ render: markdown
25
+ validations:
26
+ required: true
27
+ - type: textarea
28
+ id: details
29
+ attributes:
30
+ label: 詳細
31
+ description: 気づいた点を自由に書いてください(誤り・わかりにくい点・疑問など、何でも)。種類の分類は不要です。対応の要否は内容を読んで判断します。
32
+ validations:
33
+ required: true
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: 一般的な質問・議論
4
+ url: https://github.com/__OWNER__/__REPO__/discussions
5
+ about: 本の内容と関係ない質問や雑談はこちら(Discussions を有効にしている場合)
@@ -0,0 +1,25 @@
1
+ name: Build check
2
+
3
+ # PR で ligarb build が通るかを検証する。Claude は関与しない。
4
+ # このチェックを Settings > Branches の必須チェックに指定すると、
5
+ # ビルドが落ちる PR を merge できなくできる。
6
+
7
+ on:
8
+ pull_request:
9
+ branches: [master] # main を使う場合はここを main に変更する
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: "3.3"
22
+ - name: Install ligarb
23
+ run: gem install ligarb
24
+ - name: Build book
25
+ run: ligarb build # 失敗すればチェックが落ちる