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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c88870d66d05c780d5dd46b93ebad2f0daffa3ce201d8a51b6cec8c61eedcb73
4
- data.tar.gz: 3044c7b204c390fe1ed67f187598d50164e27d917e82dae9897878c0d418f334
3
+ metadata.gz: ab265966761408e494e47dbc0e00d188d86d99d881be30d5055af5d2abe29ef0
4
+ data.tar.gz: ecca92d4ca8f601b2c24be43694b92b5f56b37e1787f3c84540e37f2d111551f
5
5
  SHA512:
6
- metadata.gz: 8f1f24fa71f51560116db5ee1390701988e2640ed17dd4680124aad7c1f59ae82d9f513379ed38de3f8b187874399c41322f6649ea588b205e50ae760087bc62
7
- data.tar.gz: 927eb04a33dcc3b24525a5899a412f852d2a0f872c6422b0034d881ed67ba27c0151b602e01d42ebf7900324ad3f4d9392d6b7843abc9ac472d9e707466b618c
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,
@@ -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",
@@ -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|
@@ -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.downcase
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("&amp;", "&").gsub("&lt;", "<").gsub("&gt;", ">").gsub("&quot;", '"').gsub("&#39;", "'")
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 print_spec
64
- puts <<~SPEC
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
- Part title pages do not show navigation.
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)
@@ -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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ligarb
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -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
@@ -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
- <%- unless chapter.part_title? || chapter.cover? -%>
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;">&larr; <%= 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.2.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: