ligarb 0.2.0 → 0.4.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/style.css +28 -0
- data/lib/ligarb/asset_manager.rb +1 -1
- data/lib/ligarb/builder.rb +17 -0
- data/lib/ligarb/chapter.rb +60 -4
- data/lib/ligarb/cli.rb +98 -3
- data/lib/ligarb/config.rb +15 -1
- data/lib/ligarb/template.rb +2 -0
- data/lib/ligarb/version.rb +1 -1
- data/lib/ligarb/writer.rb +173 -0
- data/templates/book.html.erb +17 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab265966761408e494e47dbc0e00d188d86d99d881be30d5055af5d2abe29ef0
|
|
4
|
+
data.tar.gz: ecca92d4ca8f601b2c24be43694b92b5f56b37e1787f3c84540e37f2d111551f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c9be72721b4753bcd00ac2bd5125bc7e4c5361f8c4773ca0c7183a4a0ca5ae80a663d82ac231a0e5b64d70e79f16f7af9de3367a857207d840cb7cf4f5efb6a
|
|
7
|
+
data.tar.gz: 70a5189e3b2758b9a1c145ec34502a6402ec7b72f79454f7ed1bf56cfc0881d38a9f56fffbb7b5d96259b0e18151a5cf2dcaf4649ea97f1bb6ff0f49e0636c7d
|
data/assets/style.css
CHANGED
|
@@ -670,6 +670,34 @@ mark.search-highlight {
|
|
|
670
670
|
text-decoration: underline;
|
|
671
671
|
}
|
|
672
672
|
|
|
673
|
+
/* === AI Generated Notice === */
|
|
674
|
+
.ai-badge {
|
|
675
|
+
display: inline-block;
|
|
676
|
+
font-size: 0.7rem;
|
|
677
|
+
font-weight: 600;
|
|
678
|
+
color: #ca8a04;
|
|
679
|
+
background: #fefce8;
|
|
680
|
+
border: 1px solid #facc15;
|
|
681
|
+
border-radius: 3px;
|
|
682
|
+
padding: 0.1rem 0.4rem;
|
|
683
|
+
margin-top: 0.3rem;
|
|
684
|
+
letter-spacing: 0.03em;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
[data-theme="dark"] .ai-badge {
|
|
688
|
+
color: #fbbf24;
|
|
689
|
+
background: #2e2a1a;
|
|
690
|
+
border-color: #854d0e;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.chapter-footer {
|
|
694
|
+
margin-top: 2rem;
|
|
695
|
+
padding: 0.5rem 0.75rem;
|
|
696
|
+
font-size: 0.8rem;
|
|
697
|
+
color: var(--color-text-muted);
|
|
698
|
+
border-top: 1px solid var(--color-border);
|
|
699
|
+
}
|
|
700
|
+
|
|
673
701
|
/* === Print === */
|
|
674
702
|
@media print {
|
|
675
703
|
.sidebar,
|
data/lib/ligarb/asset_manager.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Ligarb
|
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
katex: {
|
|
25
|
-
fence_pattern: /class="math-block"/,
|
|
25
|
+
fence_pattern: /class="math-(block|inline)"/,
|
|
26
26
|
files: {
|
|
27
27
|
"js/katex.min.js" => "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js",
|
|
28
28
|
"css/katex.min.css" => "https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css",
|
data/lib/ligarb/builder.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Ligarb
|
|
|
16
16
|
structure = load_structure
|
|
17
17
|
|
|
18
18
|
all_chapters = collect_all_chapters(structure)
|
|
19
|
+
resolve_cross_references(all_chapters)
|
|
19
20
|
assign_relative_paths(all_chapters) if @config.repository
|
|
20
21
|
|
|
21
22
|
assets = AssetManager.new(@config.output_path)
|
|
@@ -106,6 +107,22 @@ module Ligarb
|
|
|
106
107
|
end
|
|
107
108
|
end
|
|
108
109
|
|
|
110
|
+
def resolve_cross_references(all_chapters)
|
|
111
|
+
chapter_map = {}
|
|
112
|
+
all_chapters.each do |ch|
|
|
113
|
+
abs_path = File.expand_path(ch.instance_variable_get(:@path))
|
|
114
|
+
chapter_map[abs_path] = {
|
|
115
|
+
slug: ch.slug,
|
|
116
|
+
chapter: ch,
|
|
117
|
+
headings: ch.headings.each_with_object({}) { |h, map| map[h.id] = h }
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
all_chapters.each do |ch|
|
|
122
|
+
ch.resolve_cross_references!(chapter_map)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
109
126
|
def assign_relative_paths(chapters)
|
|
110
127
|
git_root = find_git_root(@config.base_dir)
|
|
111
128
|
chapters.each do |ch|
|
data/lib/ligarb/chapter.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "kramdown-parser-gfm"
|
|
|
5
5
|
|
|
6
6
|
module Ligarb
|
|
7
7
|
class Chapter
|
|
8
|
+
class CrossReferenceError < StandardError; end
|
|
9
|
+
|
|
8
10
|
attr_reader :title, :slug, :html, :headings, :number, :appendix_letter, :index_entries
|
|
9
11
|
attr_accessor :part_title, :cover, :relative_path
|
|
10
12
|
|
|
@@ -43,6 +45,44 @@ module Ligarb
|
|
|
43
45
|
@cover
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
def self.generate_id(text)
|
|
49
|
+
text.downcase
|
|
50
|
+
.gsub(/[^\p{L}\p{N}\s_-]/u, "")
|
|
51
|
+
.strip
|
|
52
|
+
.gsub(/\s+/, "-")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_cross_references!(chapter_map)
|
|
56
|
+
source_dir = File.dirname(@path)
|
|
57
|
+
|
|
58
|
+
@html = @html.gsub(%r{<a\s+href="((?!https?://)[^"]+\.md)(?:#([^"]*))?">(.*?)</a>}m) do
|
|
59
|
+
href_path = $1
|
|
60
|
+
fragment = $2
|
|
61
|
+
link_text = $3
|
|
62
|
+
|
|
63
|
+
target_path = File.expand_path(href_path, source_dir)
|
|
64
|
+
entry = chapter_map[target_path]
|
|
65
|
+
unless entry
|
|
66
|
+
raise CrossReferenceError, "cross-reference target not found: #{href_path} (from #{File.basename(@path)})"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if fragment && !fragment.empty?
|
|
70
|
+
normalized = self.class.generate_id(fragment)
|
|
71
|
+
heading = entry[:headings][normalized]
|
|
72
|
+
unless heading
|
|
73
|
+
raise CrossReferenceError, "cross-reference heading not found: #{href_path}##{fragment} (from #{File.basename(@path)})"
|
|
74
|
+
end
|
|
75
|
+
anchor = "#{entry[:slug]}--#{heading.id}"
|
|
76
|
+
text = link_text.empty? ? heading.display_text : link_text
|
|
77
|
+
else
|
|
78
|
+
anchor = entry[:slug]
|
|
79
|
+
text = link_text.empty? ? entry[:chapter].display_title : link_text
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
%(<a href="##{anchor}">#{text}</a>)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
46
86
|
def display_title
|
|
47
87
|
if @appendix_letter
|
|
48
88
|
"#{@appendix_letter}. #{@title}"
|
|
@@ -61,6 +101,7 @@ module Ligarb
|
|
|
61
101
|
@html = rewrite_image_paths(doc.to_html)
|
|
62
102
|
@html = apply_heading_ids(@html)
|
|
63
103
|
@html = convert_special_code_blocks(@html)
|
|
104
|
+
@html = convert_inline_math(@html)
|
|
64
105
|
@html = convert_admonitions(@html)
|
|
65
106
|
@html = scope_footnote_ids(@html)
|
|
66
107
|
@index_entries = []
|
|
@@ -96,10 +137,7 @@ module Ligarb
|
|
|
96
137
|
end
|
|
97
138
|
|
|
98
139
|
def generate_id(text)
|
|
99
|
-
text
|
|
100
|
-
.gsub(/[^\p{L}\p{N}\s_-]/u, "") # keep letters (any script), digits, spaces, _, -
|
|
101
|
-
.strip
|
|
102
|
-
.gsub(/\s+/, "-")
|
|
140
|
+
self.class.generate_id(text)
|
|
103
141
|
end
|
|
104
142
|
|
|
105
143
|
def apply_heading_ids(html)
|
|
@@ -135,6 +173,24 @@ module Ligarb
|
|
|
135
173
|
end
|
|
136
174
|
end
|
|
137
175
|
|
|
176
|
+
def convert_inline_math(html)
|
|
177
|
+
# Protect <pre>...</pre> and <code>...</code> from conversion
|
|
178
|
+
placeholders = []
|
|
179
|
+
protected = html.gsub(%r{<(pre|code)([ >])(.*?)</\1>}m) do
|
|
180
|
+
placeholders << $&
|
|
181
|
+
"\x00PROTECT#{placeholders.size - 1}\x00"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Convert $...$ to inline math (exclude $$, and $ followed/preceded by space)
|
|
185
|
+
result = protected.gsub(/(?<!\$)\$(?!\$)(?!\s)(.+?)(?<!\s)(?<!\$)\$(?!\$)/m) do
|
|
186
|
+
raw = decode_entities($1)
|
|
187
|
+
%(<span class="math-inline" data-math="#{encode_attr(raw)}"></span>)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Restore protected parts
|
|
191
|
+
result.gsub(/\x00PROTECT(\d+)\x00/) { placeholders[$1.to_i] }
|
|
192
|
+
end
|
|
193
|
+
|
|
138
194
|
def decode_entities(text)
|
|
139
195
|
text.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub(""", '"').gsub("'", "'")
|
|
140
196
|
end
|
data/lib/ligarb/cli.rb
CHANGED
|
@@ -17,6 +17,16 @@ module Ligarb
|
|
|
17
17
|
Builder.new(config_path).build
|
|
18
18
|
when "init"
|
|
19
19
|
Initializer.new(args.first).run
|
|
20
|
+
when "write"
|
|
21
|
+
if args.delete("--init")
|
|
22
|
+
require_relative "writer"
|
|
23
|
+
Writer.init_brief(args.first)
|
|
24
|
+
else
|
|
25
|
+
brief_path = args.reject { |a| a.start_with?("--") }.first || "brief.yml"
|
|
26
|
+
no_build = args.include?("--no-build")
|
|
27
|
+
require_relative "writer"
|
|
28
|
+
Writer.new(brief_path, no_build: no_build).run
|
|
29
|
+
end
|
|
20
30
|
when "--help", "-h", nil
|
|
21
31
|
print_usage
|
|
22
32
|
when "help"
|
|
@@ -37,6 +47,8 @@ module Ligarb
|
|
|
37
47
|
Usage:
|
|
38
48
|
ligarb init [DIRECTORY] Create a new book project
|
|
39
49
|
ligarb build [CONFIG] Build the HTML book (default CONFIG: book.yml)
|
|
50
|
+
ligarb write [BRIEF] Generate a book with AI from brief.yml
|
|
51
|
+
ligarb write --init [DIR] Create DIR/brief.yml template
|
|
40
52
|
ligarb help Show detailed specification (for AI integration)
|
|
41
53
|
ligarb version Show version number
|
|
42
54
|
|
|
@@ -53,6 +65,8 @@ module Ligarb
|
|
|
53
65
|
chapter_numbers (optional) Show chapter/section numbers (default: true)
|
|
54
66
|
style (optional) Custom CSS file path (default: none)
|
|
55
67
|
repository (optional) GitHub repository URL for "Edit on GitHub" links
|
|
68
|
+
ai_generated (optional) Mark as AI-generated (badge + meta tags, default: false)
|
|
69
|
+
footer (optional) Custom text at bottom of each chapter
|
|
56
70
|
|
|
57
71
|
Example:
|
|
58
72
|
ligarb build
|
|
@@ -60,8 +74,8 @@ module Ligarb
|
|
|
60
74
|
USAGE
|
|
61
75
|
end
|
|
62
76
|
|
|
63
|
-
def
|
|
64
|
-
|
|
77
|
+
def spec_text
|
|
78
|
+
<<~SPEC
|
|
65
79
|
ligarb - Generate a single-page HTML book from Markdown files
|
|
66
80
|
|
|
67
81
|
Version: #{VERSION}
|
|
@@ -117,6 +131,14 @@ module Ligarb
|
|
|
117
131
|
When set, each chapter shows a "View on GitHub" link.
|
|
118
132
|
The link points to {repository}/blob/HEAD/{path-from-git-root}.
|
|
119
133
|
The chapter path is resolved relative to the Git repository root.
|
|
134
|
+
ai_generated: (optional) Mark the book as AI-generated content. Default: false.
|
|
135
|
+
When true: adds an "AI Generated" badge in the sidebar header,
|
|
136
|
+
adds a default disclaimer footer to each chapter, and adds
|
|
137
|
+
noindex/noai meta tags to prevent search indexing and AI training.
|
|
138
|
+
The footer text can be overridden with the 'footer' field.
|
|
139
|
+
footer: (optional) Custom text displayed at the bottom of each chapter.
|
|
140
|
+
Overrides the default ai_generated disclaimer if both are set.
|
|
141
|
+
Useful for copyright notices, disclaimers, or other per-chapter text.
|
|
120
142
|
chapters: (required) Book structure. An array that can contain:
|
|
121
143
|
- A cover: a centered title/landing page
|
|
122
144
|
- A string: a chapter Markdown file path (relative to book.yml)
|
|
@@ -262,6 +284,17 @@ module Ligarb
|
|
|
262
284
|
E = mc^2
|
|
263
285
|
```
|
|
264
286
|
|
|
287
|
+
Inline math uses $...$ syntax within text:
|
|
288
|
+
|
|
289
|
+
The equation $E = mc^2$ is well-known.
|
|
290
|
+
|
|
291
|
+
Rules for inline math:
|
|
292
|
+
- $$ is not matched (use ```math for display math)
|
|
293
|
+
- $ followed by a space is not matched (e.g. $10)
|
|
294
|
+
- $ preceded by a space is not matched
|
|
295
|
+
- Content inside <code> and <pre> is not affected
|
|
296
|
+
- The content is rendered with KaTeX (displayMode: false)
|
|
297
|
+
|
|
265
298
|
== Images ==
|
|
266
299
|
|
|
267
300
|
Place image files in the 'images/' directory next to book.yml.
|
|
@@ -374,12 +407,74 @@ module Ligarb
|
|
|
374
407
|
- CAUTION: red (stop)
|
|
375
408
|
- IMPORTANT: purple (exclamation)
|
|
376
409
|
|
|
410
|
+
== Cross-References ==
|
|
411
|
+
|
|
412
|
+
Link to other chapters or headings using standard Markdown relative links.
|
|
413
|
+
ligarb resolves .md file references to internal anchors in the single-page
|
|
414
|
+
output.
|
|
415
|
+
|
|
416
|
+
Syntax:
|
|
417
|
+
|
|
418
|
+
[link text](other-chapter.md) Link to a chapter
|
|
419
|
+
[link text](other-chapter.md#Heading) Link to a specific heading
|
|
420
|
+
[](other-chapter.md) Auto-fill with chapter title
|
|
421
|
+
[](other-chapter.md#Heading) Auto-fill with heading text
|
|
422
|
+
|
|
423
|
+
The .md path is resolved relative to the current Markdown file's directory.
|
|
424
|
+
The heading fragment is matched against heading IDs (case-insensitive,
|
|
425
|
+
normalized the same way heading slugs are generated).
|
|
426
|
+
|
|
427
|
+
When the link text is empty, ligarb fills it with the target's display text:
|
|
428
|
+
- Chapter link: the chapter's display title (e.g. "3. Config Guide")
|
|
429
|
+
- Heading link: the heading's display text (e.g. "3.2 Setup")
|
|
430
|
+
|
|
431
|
+
If a referenced chapter or heading does not exist, the build fails with an
|
|
432
|
+
error message indicating the broken reference and its source file.
|
|
433
|
+
|
|
434
|
+
External URLs ending in .md (e.g. https://example.com/README.md) are not
|
|
435
|
+
affected — only relative paths are resolved.
|
|
436
|
+
|
|
377
437
|
== Previous/Next Navigation ==
|
|
378
438
|
|
|
379
439
|
Each chapter displays Previous and Next navigation links at the bottom.
|
|
380
440
|
These follow the flat chapter order (including across parts and appendix).
|
|
381
|
-
|
|
441
|
+
Cover pages do not show navigation.
|
|
442
|
+
|
|
443
|
+
== Write Command ==
|
|
444
|
+
|
|
445
|
+
ligarb write [BRIEF] Generate a complete book using AI (Claude).
|
|
446
|
+
BRIEF defaults to 'brief.yml' in the current directory.
|
|
447
|
+
Reads the brief, sends a prompt to Claude, and builds
|
|
448
|
+
the generated book. Files are created in the same
|
|
449
|
+
directory as brief.yml.
|
|
450
|
+
|
|
451
|
+
ligarb write --init [DIR] Create a brief.yml template.
|
|
452
|
+
If DIR is given, creates DIR/brief.yml (mkdir as needed).
|
|
453
|
+
If omitted, creates brief.yml in the current directory.
|
|
454
|
+
|
|
455
|
+
ligarb write --no-build Generate files only, skip the build step.
|
|
456
|
+
|
|
457
|
+
brief.yml fields:
|
|
458
|
+
|
|
459
|
+
title: (required) The book title.
|
|
460
|
+
language: (optional) Language. Default: "ja".
|
|
461
|
+
audience: (optional) Target audience (used in the prompt).
|
|
462
|
+
notes: (optional) Additional instructions for Claude (free text).
|
|
463
|
+
author: (optional) Passed through to book.yml.
|
|
464
|
+
output_dir: (optional) Passed through to book.yml.
|
|
465
|
+
chapter_numbers: (optional) Passed through to book.yml.
|
|
466
|
+
style: (optional) Passed through to book.yml.
|
|
467
|
+
repository: (optional) Passed through to book.yml.
|
|
468
|
+
|
|
469
|
+
The book is generated in the directory containing brief.yml.
|
|
470
|
+
Example: 'ligarb write ruby_book/brief.yml' creates files in ruby_book/.
|
|
471
|
+
|
|
472
|
+
Requires the 'claude' CLI to be installed.
|
|
382
473
|
SPEC
|
|
383
474
|
end
|
|
475
|
+
|
|
476
|
+
def print_spec
|
|
477
|
+
puts spec_text
|
|
478
|
+
end
|
|
384
479
|
end
|
|
385
480
|
end
|
data/lib/ligarb/config.rb
CHANGED
|
@@ -13,7 +13,8 @@ module Ligarb
|
|
|
13
13
|
# children: array of StructEntry (for :part and :appendix_group)
|
|
14
14
|
|
|
15
15
|
attr_reader :title, :author, :language, :output_dir, :base_dir,
|
|
16
|
-
:chapter_numbers, :structure, :style, :repository
|
|
16
|
+
:chapter_numbers, :structure, :style, :repository,
|
|
17
|
+
:ai_generated, :footer
|
|
17
18
|
|
|
18
19
|
def initialize(path)
|
|
19
20
|
@base_dir = File.dirname(File.expand_path(path))
|
|
@@ -28,6 +29,8 @@ module Ligarb
|
|
|
28
29
|
@chapter_numbers = data.fetch("chapter_numbers", true)
|
|
29
30
|
@style = data.fetch("style", nil)
|
|
30
31
|
@repository = data.fetch("repository", nil)
|
|
32
|
+
@ai_generated = data.fetch("ai_generated", false)
|
|
33
|
+
@footer = data.fetch("footer", nil)
|
|
31
34
|
@structure = parse_structure(data["chapters"])
|
|
32
35
|
end
|
|
33
36
|
|
|
@@ -43,6 +46,17 @@ module Ligarb
|
|
|
43
46
|
@language == "ja" ? "付録" : "Appendix"
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
def effective_footer
|
|
50
|
+
return @footer if @footer
|
|
51
|
+
return nil unless @ai_generated
|
|
52
|
+
|
|
53
|
+
if @language == "ja"
|
|
54
|
+
"この章の内容は AI によって生成されました。正確性は保証されません。"
|
|
55
|
+
else
|
|
56
|
+
"This chapter was generated by AI. Accuracy is not guaranteed."
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
46
60
|
# Returns a flat list of all chapter file paths (excluding part title pages)
|
|
47
61
|
def chapter_paths
|
|
48
62
|
collect_chapter_paths(@structure)
|
data/lib/ligarb/template.rb
CHANGED
|
@@ -31,6 +31,8 @@ module Ligarb
|
|
|
31
31
|
b.local_variable_set(:assets, assets)
|
|
32
32
|
b.local_variable_set(:repository, config.repository)
|
|
33
33
|
b.local_variable_set(:appendix_label, config.appendix_label)
|
|
34
|
+
b.local_variable_set(:ai_generated, config.ai_generated)
|
|
35
|
+
b.local_variable_set(:footer, config.effective_footer)
|
|
34
36
|
b.local_variable_set(:index_tree, build_index_tree(index_entries, chapters))
|
|
35
37
|
|
|
36
38
|
ERB.new(template, trim_mode: "-").result(b)
|
data/lib/ligarb/version.rb
CHANGED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Ligarb
|
|
7
|
+
class Writer
|
|
8
|
+
BRIEF_FIELDS_FOR_BOOK_YML = %w[author output_dir chapter_numbers style repository].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(brief_path, no_build: false)
|
|
11
|
+
@brief_path = File.expand_path(brief_path)
|
|
12
|
+
@no_build = no_build
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
check_claude_installed!
|
|
17
|
+
brief = load_brief
|
|
18
|
+
output_dir = output_dir_for(brief)
|
|
19
|
+
book_yml_path = File.join(output_dir, "book.yml")
|
|
20
|
+
|
|
21
|
+
if File.exist?(book_yml_path)
|
|
22
|
+
$stderr.puts "Error: #{book_yml_path} already exists. Remove it first to regenerate."
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
FileUtils.mkdir_p(output_dir)
|
|
27
|
+
prompt = build_prompt(brief, output_dir)
|
|
28
|
+
run_claude(prompt)
|
|
29
|
+
|
|
30
|
+
unless File.exist?(book_yml_path)
|
|
31
|
+
$stderr.puts "Error: Claude did not generate book.yml in #{output_dir}"
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
puts "Book files generated in #{output_dir}"
|
|
36
|
+
|
|
37
|
+
unless @no_build
|
|
38
|
+
puts "Building..."
|
|
39
|
+
require_relative "builder"
|
|
40
|
+
Builder.new(book_yml_path).build
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.init_brief(directory = nil)
|
|
45
|
+
dir = directory || "."
|
|
46
|
+
target = File.expand_path(dir)
|
|
47
|
+
path = File.join(target, "brief.yml")
|
|
48
|
+
|
|
49
|
+
if File.exist?(path)
|
|
50
|
+
$stderr.puts "Error: #{path} already exists."
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
FileUtils.mkdir_p(target)
|
|
55
|
+
|
|
56
|
+
File.write(path, <<~YAML)
|
|
57
|
+
# brief.yml - Book brief for ligarb write
|
|
58
|
+
title: "My Book"
|
|
59
|
+
language: ja
|
|
60
|
+
audience: ""
|
|
61
|
+
notes: |
|
|
62
|
+
5章くらいで。
|
|
63
|
+
YAML
|
|
64
|
+
|
|
65
|
+
claude_md = File.join(target, "CLAUDE.md")
|
|
66
|
+
created_claude_md = false
|
|
67
|
+
unless File.exist?(claude_md)
|
|
68
|
+
File.write(claude_md, <<~MD)
|
|
69
|
+
# ligarb book project
|
|
70
|
+
|
|
71
|
+
This is a book project using [ligarb](https://github.com/ko1/ligarb).
|
|
72
|
+
|
|
73
|
+
## Commands
|
|
74
|
+
|
|
75
|
+
- `ligarb build` — Build the book (generates build/index.html)
|
|
76
|
+
- `ligarb help` — Show full specification (Markdown syntax, config options, etc.)
|
|
77
|
+
|
|
78
|
+
## Key rules
|
|
79
|
+
|
|
80
|
+
- All chapter files are Markdown (.md), listed in book.yml
|
|
81
|
+
- The first h1 in each file is the chapter title
|
|
82
|
+
- Use ```mermaid, ```math, admonitions (> [!NOTE]), etc. as needed
|
|
83
|
+
- Run `ligarb build` after changes to verify the output
|
|
84
|
+
MD
|
|
85
|
+
created_claude_md = true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
puts "Created #{path}"
|
|
89
|
+
puts "Created #{claude_md}" if created_claude_md
|
|
90
|
+
brief_arg = directory ? " #{path}" : ""
|
|
91
|
+
puts "Edit brief.yml, then run 'ligarb write#{brief_arg}' to generate the book."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def check_claude_installed!
|
|
97
|
+
unless system("claude", "--version", out: File::NULL, err: File::NULL)
|
|
98
|
+
$stderr.puts "Error: 'claude' command not found. Install Claude Code first."
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def load_brief
|
|
104
|
+
unless File.exist?(@brief_path)
|
|
105
|
+
$stderr.puts "Error: #{@brief_path} not found."
|
|
106
|
+
exit 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
brief = YAML.safe_load_file(@brief_path)
|
|
110
|
+
unless brief.is_a?(Hash) && brief["title"] && !brief["title"].empty?
|
|
111
|
+
$stderr.puts "Error: 'title' is required in #{@brief_path}."
|
|
112
|
+
exit 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
brief
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def output_dir_for(_brief)
|
|
119
|
+
File.dirname(@brief_path)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_prompt(brief, output_dir)
|
|
123
|
+
abs_output_dir = File.expand_path(output_dir)
|
|
124
|
+
spec = CLI.spec_text
|
|
125
|
+
|
|
126
|
+
lines = []
|
|
127
|
+
lines << "You are writing a book using the ligarb tool."
|
|
128
|
+
lines << ""
|
|
129
|
+
lines << "<ligarb-spec>"
|
|
130
|
+
lines << spec
|
|
131
|
+
lines << "</ligarb-spec>"
|
|
132
|
+
lines << ""
|
|
133
|
+
lines << "Write a complete book based on this brief:"
|
|
134
|
+
lines << "- Title: #{brief["title"]}"
|
|
135
|
+
lines << "- Language: #{brief["language"] || "ja"}"
|
|
136
|
+
lines << "- Target audience: #{brief["audience"]}" if brief["audience"] && !brief["audience"].empty?
|
|
137
|
+
|
|
138
|
+
if brief["notes"] && !brief["notes"].strip.empty?
|
|
139
|
+
lines << ""
|
|
140
|
+
lines << "Additional instructions:"
|
|
141
|
+
lines << brief["notes"].strip
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
book_yml_fields = BRIEF_FIELDS_FOR_BOOK_YML.select { |k| brief.key?(k) }
|
|
145
|
+
if book_yml_fields.any?
|
|
146
|
+
settings = book_yml_fields.map { |k| "#{k}: #{brief[k].inspect}" }.join(", ")
|
|
147
|
+
lines << ""
|
|
148
|
+
lines << "In book.yml, set: #{settings}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
lines << ""
|
|
152
|
+
lines << "Create all files in: #{abs_output_dir}"
|
|
153
|
+
lines << "In book.yml, always set: ai_generated: true"
|
|
154
|
+
lines << "Create book.yml first, then each chapter .md file."
|
|
155
|
+
lines << "Include a cover page for books with 4+ chapters."
|
|
156
|
+
lines << "Each chapter: substantive content with multiple ## sections."
|
|
157
|
+
lines << "Use code blocks, admonitions, mermaid diagrams where appropriate."
|
|
158
|
+
lines << "Chapter filenames: 01-topic.md, 02-topic.md, etc."
|
|
159
|
+
|
|
160
|
+
lines.join("\n")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def run_claude(prompt)
|
|
164
|
+
tools = "Write,Bash,WebFetch,WebSearch"
|
|
165
|
+
allowed = "Write,Bash(mkdir:*),Bash(ls:*),Bash(ligarb:*),WebFetch,WebSearch"
|
|
166
|
+
cmd = ["claude", "-p", "--verbose", prompt, "--tools", tools, "--allowedTools", allowed]
|
|
167
|
+
unless system(*cmd)
|
|
168
|
+
$stderr.puts "Error: Claude process failed."
|
|
169
|
+
exit 1
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/templates/book.html.erb
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title><%= title %></title>
|
|
7
|
+
<%- if ai_generated -%>
|
|
8
|
+
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
9
|
+
<meta name="robots" content="noai, noimageai">
|
|
10
|
+
<%- end -%>
|
|
7
11
|
<style>
|
|
8
12
|
<%= css %>
|
|
9
13
|
</style>
|
|
@@ -35,6 +39,9 @@
|
|
|
35
39
|
<%- unless author.empty? -%>
|
|
36
40
|
<p class="book-author"><%= author %></p>
|
|
37
41
|
<%- end -%>
|
|
42
|
+
<%- if ai_generated -%>
|
|
43
|
+
<span class="ai-badge"><%= language == 'ja' ? 'AI 生成' : 'AI Generated' %></span>
|
|
44
|
+
<%- end -%>
|
|
38
45
|
</div>
|
|
39
46
|
<div class="search-box">
|
|
40
47
|
<input type="text" id="toc-search" placeholder="Search..." autocomplete="off">
|
|
@@ -123,7 +130,10 @@
|
|
|
123
130
|
<a href="<%= repository.chomp('/') %>/blob/HEAD/<%= chapter.relative_path %>" target="_blank" rel="noopener">View on GitHub</a>
|
|
124
131
|
</div>
|
|
125
132
|
<%- end -%>
|
|
126
|
-
<%-
|
|
133
|
+
<%- if footer && !chapter.part_title? && !chapter.cover? -%>
|
|
134
|
+
<div class="chapter-footer"><%= footer %></div>
|
|
135
|
+
<%- end -%>
|
|
136
|
+
<%- unless chapter.cover? -%>
|
|
127
137
|
<nav class="chapter-nav">
|
|
128
138
|
<%- if idx > 0 -%>
|
|
129
139
|
<a href="#" class="nav-prev" onclick="showChapter('<%= chapters[idx-1].slug %>'); return false;">← <%= chapters[idx-1].display_title %></a>
|
|
@@ -211,6 +221,12 @@
|
|
|
211
221
|
catch(e) { el.textContent = el.getAttribute('data-math'); }
|
|
212
222
|
}
|
|
213
223
|
});
|
|
224
|
+
section.querySelectorAll('.math-inline[data-math]').forEach(function(el) {
|
|
225
|
+
if (el.childNodes.length === 0) {
|
|
226
|
+
try { katex.render(el.getAttribute('data-math'), el, {displayMode: false, throwOnError: false}); }
|
|
227
|
+
catch(e) { el.textContent = el.getAttribute('data-math'); }
|
|
228
|
+
}
|
|
229
|
+
});
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
232
|
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ligarb contributors
|
|
@@ -82,6 +82,7 @@ files:
|
|
|
82
82
|
- lib/ligarb/initializer.rb
|
|
83
83
|
- lib/ligarb/template.rb
|
|
84
84
|
- lib/ligarb/version.rb
|
|
85
|
+
- lib/ligarb/writer.rb
|
|
85
86
|
- templates/book.html.erb
|
|
86
87
|
homepage: https://github.com/ligarb/ligarb
|
|
87
88
|
licenses:
|