Module: Vivlio::Starter::CLI::Build::PdfBuilder

Defined in:
lib/vivlio/starter/cli/build/pdf_builder.rb

Overview


PdfBuilder: PDF生成モジュール


Step 8: 全体PDF生成(前書き+目次+本文+付録+後書き+索引)Step 9: 表紙・奥付PDF生成

設計方針:

- PDF分割をスキップし、全体を1つのPDFとして生成
- これにより索引から前書きへのリンクなど内部リンクが維持される
- ローマ数字ノンブルはCSSの @page front で対応

Constant Summary collapse

PREFACE_RANGE =

章レンジ(定数)- 新仕様に合わせて更新

(0..0)
MAIN_RANGE =

01..89 本文

(1..89)
APPX_RANGE =

90..98 付録

(90..98)
POSTFACE_RANGE =

99-postface

(99..99)

Class Method Summary collapse

Class Method Details

.build_front_pages_and_tail!Object

Step 9: 本扉・扉裏・後書き・奥付の生成新仕様: _titlepage, _legalpage, _colophon を使用

設計方針: mtime 比較・キャッシュ判定は行わず、常に HTML と PDF を再生成する。計測上これらの生成はビルド全体への影響が軽微なため、判定ロジックの脆さ(‘FileUtils.cp` による mtime 破壊、book.yml 無関係変更での誤判定等)を排除する方を優先する。詳細は docs/specs/book_yml_regeneration_spec.md 参照。



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/vivlio/starter/cli/build/pdf_builder.rb', line 166

def build_front_pages_and_tail!
  # --- Phase: 特殊ページ HTML を常に再生成 ---
  %w[_titlepage _legalpage _colophon].each do |basename|
    Common.log_info("[HTML] 再生成します: #{basename}.html")
    Build::SectionBuilder.preprocess_single_chapter!(basename)
    Build::SectionBuilder.convert_single_chapter!(basename)
  end

  # --- Phase: 表紙+扉裏 PDF を常に再生成 ---
  front_pdf = '_titlepage_legalpage.pdf'
  EntriesCommands.execute_entries({}, ['_titlepage.html', '_legalpage.html'])
  PdfCommands.execute_pdf({}, front_pdf)
  if File.exist?(front_pdf)
    Common.log_success("[Step 9] #{front_pdf} を生成しました")
  else
    Common.log_warn("[Step 9] #{front_pdf} の生成に失敗しました")
  end

  # --- Phase: 奥付 PDF を常に再生成 ---
  colophon_pdf = '_colophon.pdf'
  EntriesCommands.execute_entries({}, ['_colophon.html'])
  PdfCommands.execute_pdf({}, colophon_pdf)
  if File.exist?(colophon_pdf)
    Common.log_success('[Step 9] _colophon.pdf を生成しました')
  else
    Common.log_warn('[Step 9] _colophon.pdf の生成に失敗しました')
  end
end

.build_overall_pdf_from_dir!(base_dir = '.', entries_or_keep = nil) ⇒ Object

Step 8: 全体PDF生成(ディレクトリスキャン版)前書き+目次+本文+付録+後書き+索引を1つのPDFとして生成

Parameters:

  • base_dir (String) (defaults to: '.')

    ベースディレクトリ

  • entries_or_keep (Array<TokenResolver::Entry>, Array<String>, nil) (defaults to: nil)

    Entry 配列または basename 配列



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/vivlio/starter/cli/build/pdf_builder.rb', line 33

def build_overall_pdf_from_dir!(base_dir = '.', entries_or_keep = nil)
  # 前付け: 00-preface + _toc
  preface_html = [File.join(base_dir, '00-preface.html')].select { |f| File.exist?(f) }
  toc_html = [File.join(base_dir, '_toc.html')].select { |f| File.exist?(f) }

  keep_numbers_main = Build::Utilities.chapter_numbers_for_book(entries_or_keep)
  keep_numbers_appx = nil
  keep_numbers_post = nil
  if entries_or_keep&.any?
    chapter_numbers = extract_chapter_numbers(entries_or_keep)
    keep_numbers_appx = chapter_numbers.select { |n| APPX_RANGE.include?(n) }
    keep_numbers_post = chapter_numbers.select { |n| POSTFACE_RANGE.include?(n) }
  end
  glossary_html = if IndexCommands.index_enabled?
                    [File.join(base_dir, '_glossarypage.html')].select { |f| File.exist?(f) }
                  else
                    []
                  end
  index_html = if IndexCommands.index_enabled?
                 [File.join(base_dir, '_indexpage.html')].select { |f| File.exist?(f) }
               else
                 []
               end

  # 本文章 HTML に中扉を挿入(部タイトルが定義されている場合)
  main_htmls = Build::ChapterConfig.htmls_for_range(base_dir, MAIN_RANGE, keep_numbers_main)
  main_htmls_with_parts = Build::PartTitleGenerator.insert_part_titles_into(main_htmls, base_dir)

  # 書籍構成順序: 前書き → 目次 → [中扉+本文] → 付録 → 用語集 → 後書き → 索引
  # ※ 00-preface, _toc を先頭に含めることで target-counter が正しく解決される
  chapter_htmls_for_pdf = [
    preface_html,
    toc_html,
    main_htmls_with_parts,
    Build::ChapterConfig.htmls_for_range(base_dir, APPX_RANGE, keep_numbers_appx),
    glossary_html,
    Build::ChapterConfig.htmls_for_range(base_dir, POSTFACE_RANGE, keep_numbers_post),
    index_html
  ].flatten

  targets_for_pdf = chapter_htmls_for_pdf
  Common.log_info("[Step 7] targets_for_pdf: #{targets_for_pdf.map { |p| File.basename(p) }.join(', ')}")

  compile_overall_pdf!(targets_for_pdf)
end

.compile_overall_pdf!(targets_for_pdf) ⇒ Object

全体PDF生成(内部メソッド)entries.jsを生成し、VivliostyleでPDFをビルド



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/vivlio/starter/cli/build/pdf_builder.rb', line 136

def compile_overall_pdf!(targets_for_pdf)
  if targets_for_pdf.empty?
    Common.log_warn('[Step 7] 対象HTMLが見つかりません。スキップします。')
    return
  end
  Common.log_info("[Step 7] 対象: #{targets_for_pdf.map { |p| File.basename(p) }.join(', ')}")

  EntriesCommands.execute_entries({}, targets_for_pdf)
  PdfCommands.execute_pdf({})

  pdf_config   = Common::CONFIG['pdf'] || {}
  output_pdf   = pdf_config['output_file'] || 'output.pdf'
  unless File.exist?(output_pdf)
    Common.log_warn("[Step 7] 出力PDFが見つかりません: #{output_pdf}")
    return
  end

  # 全体PDFをそのまま _sections.pdf として使用
  # これにより内部リンク(索引→00-preface等)が維持される
  FileUtils.cp(output_pdf, '_sections.pdf')
  Common.log_success('[Step 7] _sections.pdf を生成しました')
end

.extract_chapter_numbers(entries_or_keep) ⇒ Array<Integer>

Entry 配列または basename 配列から章番号配列を抽出

Parameters:

Returns:

  • (Array<Integer>)

    章番号配列



198
199
200
201
202
203
204
205
206
207
208
# File 'lib/vivlio/starter/cli/build/pdf_builder.rb', line 198

def extract_chapter_numbers(entries_or_keep)
  raw = Array(entries_or_keep).compact
  return [] if raw.empty?

  if raw.first.respond_to?(:number)
    raw.filter_map { it.number&.to_i }
  else
    resolver = TokenResolver::Resolver.new
    raw.filter_map { resolver.resolve_file(it).number&.to_i }
  end
end

.generate_entries_for_sections!(base_dir = '.', entries_or_keep = nil) ⇒ Object

Step 7 (print_pdf only): entries.js のみ生成(PDF ビルドをスキップ)print_pdf ターゲットのみの場合、entries.js は Step 13 で再利用される

Parameters:

  • base_dir (String) (defaults to: '.')

    ベースディレクトリ

  • entries_or_keep (Array<TokenResolver::Entry>, Array<String>, nil) (defaults to: nil)


83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/vivlio/starter/cli/build/pdf_builder.rb', line 83

def generate_entries_for_sections!(base_dir = '.', entries_or_keep = nil)
  preface_html = [File.join(base_dir, '00-preface.html')].select { |f| File.exist?(f) }
  toc_html = [File.join(base_dir, '_toc.html')].select { |f| File.exist?(f) }

  keep_numbers_main = Build::Utilities.chapter_numbers_for_book(entries_or_keep)
  keep_numbers_appx = nil
  keep_numbers_post = nil
  if entries_or_keep&.any?
    chapter_numbers = extract_chapter_numbers(entries_or_keep)
    keep_numbers_appx = chapter_numbers.select { |n| APPX_RANGE.include?(n) }
    keep_numbers_post = chapter_numbers.select { |n| POSTFACE_RANGE.include?(n) }
  end
  glossary_html = if IndexCommands.index_enabled?
                    [File.join(base_dir, '_glossarypage.html')].select do |f|
                      File.exist?(f)
                    end
                  else
                    []
                  end
  index_html = if IndexCommands.index_enabled?
                 [File.join(base_dir, '_indexpage.html')].select do |f|
                   File.exist?(f)
                 end
               else
                 []
               end

  # 本文章 HTML に中扉を挿入(部タイトルが定義されている場合)
  main_htmls = Build::ChapterConfig.htmls_for_range(base_dir, MAIN_RANGE, keep_numbers_main)
  main_htmls_with_parts = Build::PartTitleGenerator.insert_part_titles_into(main_htmls, base_dir)

  chapter_htmls_for_pdf = [
    preface_html,
    toc_html,
    main_htmls_with_parts,
    Build::ChapterConfig.htmls_for_range(base_dir, APPX_RANGE, keep_numbers_appx),
    glossary_html,
    Build::ChapterConfig.htmls_for_range(base_dir, POSTFACE_RANGE, keep_numbers_post),
    index_html
  ].flatten

  if chapter_htmls_for_pdf.empty?
    Common.log_warn('[Step 7] 対象HTMLが見つかりません。スキップします。')
    return
  end

  Common.log_info('[Step 7] entries.js を生成します(PDF ビルドはスキップ)')
  EntriesCommands.execute_entries({}, chapter_htmls_for_pdf)
  Common.log_success('[Step 7] entries.js を生成しました')
end