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
data/lib/ligarb/config.rb
CHANGED
|
@@ -6,20 +6,57 @@ module Ligarb
|
|
|
6
6
|
class Config
|
|
7
7
|
REQUIRED_KEYS = %w[title chapters].freeze
|
|
8
8
|
|
|
9
|
+
# Keys that can be inherited from a parent (translations hub) config.
|
|
10
|
+
# output_dir is intentionally excluded — it always comes from the
|
|
11
|
+
# config file that was directly passed to `ligarb build`.
|
|
12
|
+
INHERITABLE_KEYS = %w[author language chapter_numbers style
|
|
13
|
+
repository ai_generated footer bibliography
|
|
14
|
+
github_review].freeze
|
|
15
|
+
|
|
9
16
|
# Represents a structural entry in the book
|
|
10
17
|
StructEntry = Struct.new(:type, :path, :children, keyword_init: true)
|
|
11
18
|
# type: :chapter, :part, or :appendix_group
|
|
12
19
|
# path: markdown file path (for :chapter and :part), nil for :appendix_group
|
|
13
20
|
# children: array of StructEntry (for :part and :appendix_group)
|
|
14
21
|
|
|
22
|
+
# Represents a translation entry
|
|
23
|
+
# output_path: absolute path to the translation's build directory (for link generation)
|
|
24
|
+
TranslationEntry = Struct.new(:lang, :config_path, :title, :language, :output_path, keyword_init: true)
|
|
25
|
+
|
|
15
26
|
attr_reader :title, :author, :language, :output_dir, :base_dir,
|
|
16
27
|
:chapter_numbers, :structure, :style, :repository,
|
|
17
|
-
:ai_generated, :footer, :bibliography, :sources
|
|
28
|
+
:ai_generated, :footer, :bibliography, :sources,
|
|
29
|
+
:translations, :github_review
|
|
18
30
|
|
|
19
|
-
def initialize(path)
|
|
31
|
+
def initialize(path, parent_data: nil)
|
|
20
32
|
@base_dir = File.dirname(File.expand_path(path))
|
|
21
33
|
data = YAML.safe_load_file(path)
|
|
22
34
|
|
|
35
|
+
# If this is a translations hub (has translations but no chapters), skip normal validation
|
|
36
|
+
if data.is_a?(Hash) && data.key?("translations") && !data.key?("chapters")
|
|
37
|
+
@translations_hub = true
|
|
38
|
+
@translations_data = data
|
|
39
|
+
load_translations_hub(data)
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Load inherit file if specified (for standalone builds of child configs)
|
|
44
|
+
if !parent_data && data.is_a?(Hash) && data.key?("inherit")
|
|
45
|
+
inherit_path = File.expand_path(data["inherit"], @base_dir)
|
|
46
|
+
if File.exist?(inherit_path)
|
|
47
|
+
parent_data = YAML.safe_load_file(inherit_path)
|
|
48
|
+
else
|
|
49
|
+
abort "Error: inherit config not found: #{inherit_path}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Merge parent (inheritable) keys as defaults
|
|
54
|
+
if parent_data
|
|
55
|
+
INHERITABLE_KEYS.each do |key|
|
|
56
|
+
data[key] = parent_data[key] if parent_data.key?(key) && !data.key?(key)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
23
60
|
validate!(data)
|
|
24
61
|
|
|
25
62
|
@title = data["title"]
|
|
@@ -32,8 +69,16 @@ module Ligarb
|
|
|
32
69
|
@ai_generated = data.fetch("ai_generated", false)
|
|
33
70
|
@footer = data.fetch("footer", nil)
|
|
34
71
|
@bibliography = data.fetch("bibliography", nil)
|
|
72
|
+
@github_review = data.fetch("github_review", nil)
|
|
35
73
|
@sources = parse_sources(data.fetch("sources", []))
|
|
36
74
|
@structure = parse_structure(data["chapters"])
|
|
75
|
+
@translations = []
|
|
76
|
+
|
|
77
|
+
load_translations(data, path) if data.key?("translations")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def translations_hub?
|
|
81
|
+
@translations_hub == true
|
|
37
82
|
end
|
|
38
83
|
|
|
39
84
|
def output_path
|
|
@@ -52,6 +97,23 @@ module Ligarb
|
|
|
52
97
|
@language == "ja" ? "付録" : "Appendix"
|
|
53
98
|
end
|
|
54
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
|
+
|
|
55
117
|
def effective_footer
|
|
56
118
|
return @footer if @footer
|
|
57
119
|
return nil unless @ai_generated
|
|
@@ -137,6 +199,66 @@ module Ligarb
|
|
|
137
199
|
collect_chapter_paths(entries)
|
|
138
200
|
end
|
|
139
201
|
|
|
202
|
+
def load_translations(data, config_path)
|
|
203
|
+
trans = data["translations"]
|
|
204
|
+
return unless trans.is_a?(Hash)
|
|
205
|
+
|
|
206
|
+
trans.each do |lang, rel_path|
|
|
207
|
+
abs_path = File.expand_path(rel_path, @base_dir)
|
|
208
|
+
unless File.exist?(abs_path)
|
|
209
|
+
abort "Error: translation config not found: #{abs_path}"
|
|
210
|
+
end
|
|
211
|
+
child_data = YAML.safe_load_file(abs_path)
|
|
212
|
+
# Merge parent keys for output_dir resolution
|
|
213
|
+
INHERITABLE_KEYS.each do |key|
|
|
214
|
+
child_data[key] = data[key] if data.key?(key) && !child_data.key?(key)
|
|
215
|
+
end
|
|
216
|
+
child_title = child_data["title"] || @title
|
|
217
|
+
child_lang = child_data.fetch("language", lang)
|
|
218
|
+
child_base = File.dirname(abs_path)
|
|
219
|
+
child_output = File.join(child_base, child_data.fetch("output_dir", "build"))
|
|
220
|
+
@translations << TranslationEntry.new(
|
|
221
|
+
lang: lang,
|
|
222
|
+
config_path: abs_path,
|
|
223
|
+
title: child_title,
|
|
224
|
+
language: child_lang,
|
|
225
|
+
output_path: child_output
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def load_translations_hub(data)
|
|
231
|
+
@title = data.fetch("title", "")
|
|
232
|
+
@translations = []
|
|
233
|
+
|
|
234
|
+
trans = data["translations"]
|
|
235
|
+
return unless trans.is_a?(Hash)
|
|
236
|
+
|
|
237
|
+
trans.each do |lang, rel_path|
|
|
238
|
+
abs_path = File.expand_path(rel_path, @base_dir)
|
|
239
|
+
unless File.exist?(abs_path)
|
|
240
|
+
abort "Error: translation config not found: #{abs_path}"
|
|
241
|
+
end
|
|
242
|
+
child_data = YAML.safe_load_file(abs_path)
|
|
243
|
+
# Merge inheritable keys from hub as defaults
|
|
244
|
+
merged = child_data.dup
|
|
245
|
+
INHERITABLE_KEYS.each do |key|
|
|
246
|
+
merged[key] = data[key] if data.key?(key) && !merged.key?(key)
|
|
247
|
+
end
|
|
248
|
+
child_title = merged["title"] || @title
|
|
249
|
+
child_lang = merged.fetch("language", lang)
|
|
250
|
+
child_base = File.dirname(abs_path)
|
|
251
|
+
child_output = File.join(child_base, merged.fetch("output_dir", "build"))
|
|
252
|
+
@translations << TranslationEntry.new(
|
|
253
|
+
lang: lang,
|
|
254
|
+
config_path: abs_path,
|
|
255
|
+
title: child_title,
|
|
256
|
+
language: child_lang,
|
|
257
|
+
output_path: child_output
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
140
262
|
def validate!(data)
|
|
141
263
|
unless data.is_a?(Hash)
|
|
142
264
|
abort "Error: book.yml must be a YAML mapping"
|
|
@@ -151,6 +273,10 @@ module Ligarb
|
|
|
151
273
|
unless data["chapters"].is_a?(Array) && !data["chapters"].empty?
|
|
152
274
|
abort "Error: 'chapters' must be a non-empty array"
|
|
153
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
|
|
154
280
|
end
|
|
155
281
|
end
|
|
156
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
|
data/lib/ligarb/review_store.rb
CHANGED
data/lib/ligarb/template.rb
CHANGED
|
@@ -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
|
|
|
@@ -35,12 +36,97 @@ module Ligarb
|
|
|
35
36
|
b.local_variable_set(:footer, config.effective_footer)
|
|
36
37
|
b.local_variable_set(:index_tree, build_index_tree(index_entries, chapters))
|
|
37
38
|
b.local_variable_set(:bibliography, bibliography)
|
|
39
|
+
b.local_variable_set(:multilang, false)
|
|
40
|
+
b.local_variable_set(:langs, [])
|
|
41
|
+
b.local_variable_set(:feedback, load_feedback(github_review))
|
|
42
|
+
|
|
43
|
+
ERB.new(template, trim_mode: "-").result(b)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Render a single HTML with all languages, switchable via JS
|
|
47
|
+
def render_multilang(langs:, assets:, hub_data:, github_review: nil)
|
|
48
|
+
css = File.read(@css_path)
|
|
49
|
+
template = File.read(@template_path)
|
|
50
|
+
|
|
51
|
+
first = langs.first
|
|
52
|
+
first_config = first[:config]
|
|
53
|
+
|
|
54
|
+
custom_css = if first_config.style_path && File.exist?(first_config.style_path)
|
|
55
|
+
File.read(first_config.style_path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Build per-language template data
|
|
59
|
+
lang_data = langs.map do |ld|
|
|
60
|
+
cfg = ld[:config]
|
|
61
|
+
{
|
|
62
|
+
lang: ld[:lang],
|
|
63
|
+
title: cfg.title,
|
|
64
|
+
author: cfg.author,
|
|
65
|
+
language: cfg.language,
|
|
66
|
+
chapters: ld[:chapters],
|
|
67
|
+
structure: ld[:structure],
|
|
68
|
+
repository: cfg.repository,
|
|
69
|
+
appendix_label: cfg.appendix_label,
|
|
70
|
+
ai_generated: cfg.ai_generated,
|
|
71
|
+
footer: cfg.effective_footer,
|
|
72
|
+
index_tree: build_index_tree(ld[:index_entries], ld[:chapters]),
|
|
73
|
+
bibliography: ld[:bibliography],
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
b = binding
|
|
78
|
+
# Use first language's values as defaults for shared template vars
|
|
79
|
+
b.local_variable_set(:title, first_config.title)
|
|
80
|
+
b.local_variable_set(:author, first_config.author)
|
|
81
|
+
b.local_variable_set(:language, first_config.language)
|
|
82
|
+
b.local_variable_set(:chapters, first[:chapters])
|
|
83
|
+
b.local_variable_set(:structure, first[:structure])
|
|
84
|
+
b.local_variable_set(:css, css)
|
|
85
|
+
b.local_variable_set(:custom_css, custom_css)
|
|
86
|
+
b.local_variable_set(:assets, assets)
|
|
87
|
+
b.local_variable_set(:repository, first_config.repository)
|
|
88
|
+
b.local_variable_set(:appendix_label, first_config.appendix_label)
|
|
89
|
+
b.local_variable_set(:ai_generated, first_config.ai_generated)
|
|
90
|
+
b.local_variable_set(:footer, first_config.effective_footer)
|
|
91
|
+
b.local_variable_set(:index_tree, build_index_tree(first[:index_entries], first[:chapters]))
|
|
92
|
+
b.local_variable_set(:bibliography, first[:bibliography])
|
|
93
|
+
b.local_variable_set(:multilang, true)
|
|
94
|
+
b.local_variable_set(:langs, lang_data)
|
|
95
|
+
b.local_variable_set(:feedback, load_feedback(github_review))
|
|
38
96
|
|
|
39
97
|
ERB.new(template, trim_mode: "-").result(b)
|
|
40
98
|
end
|
|
41
99
|
|
|
42
100
|
private
|
|
43
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
|
+
|
|
44
130
|
# HTML-escape helper for ERB templates (available via binding)
|
|
45
131
|
def h(s)
|
|
46
132
|
ERB::Util.html_escape(s.to_s)
|
data/lib/ligarb/version.rb
CHANGED
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,
|
|
189
|
-
lines << "Chapter filenames:
|
|
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"
|