Module: VivlioStarter::CLI::PreProcessCommands

Defined in:
lib/vivlio_starter/cli/pre_process.rb,
lib/vivlio_starter/cli/pre_process/css_updater.rb,
lib/vivlio_starter/cli/pre_process/data_render.rb,
lib/vivlio_starter/cli/pre_process/talk_registry.rb,
lib/vivlio_starter/cli/pre_process/issue_registry.rb,
lib/vivlio_starter/cli/pre_process/markdown_utils.rb,
lib/vivlio_starter/cli/pre_process/qr_transformer.rb,
lib/vivlio_starter/cli/pre_process/image_generator.rb,
lib/vivlio_starter/cli/pre_process/table_converter.rb,
lib/vivlio_starter/cli/pre_process/theme_validator.rb,
lib/vivlio_starter/cli/pre_process/math_transformer.rb,
lib/vivlio_starter/cli/pre_process/mermaid_renderer.rb,
lib/vivlio_starter/cli/pre_process/book_settings_css.rb,
lib/vivlio_starter/cli/pre_process/svg_font_embedder.rb,
lib/vivlio_starter/cli/pre_process/data_image_resolver.rb,
lib/vivlio_starter/cli/pre_process/mermaid_transformer.rb,
lib/vivlio_starter/cli/pre_process/link_image_validator.rb,
lib/vivlio_starter/cli/pre_process/markdown_transformer.rb,
lib/vivlio_starter/cli/pre_process/showcase_svg_builder.rb,
lib/vivlio_starter/cli/pre_process/showcase_transformer.rb,
lib/vivlio_starter/cli/pre_process/theme_image_resolver.rb,
lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb,
lib/vivlio_starter/cli/pre_process/generated_asset_cache.rb,
lib/vivlio_starter/cli/pre_process/image_path_normalizer.rb,
lib/vivlio_starter/cli/pre_process/markdown_preprocessor.rb,
lib/vivlio_starter/cli/pre_process/talk_avatar_generator.rb,
lib/vivlio_starter/cli/pre_process/data_render/singularize.rb,
lib/vivlio_starter/cli/pre_process/cross_reference_processor.rb,
lib/vivlio_starter/cli/pre_process/data_render/template_compiler.rb,
lib/vivlio_starter/cli/pre_process/data_render/query_stream_parser.rb

Overview

================================================================

Module: PreProcessCommands

Markdown前処理のコマンド群とヘルパーメソッドを提供

Defined Under Namespace

Modules: BookSettingsCss, CrossReferenceProcessor, CssUpdater, DataImageResolver, DataRender, FrontmatterGenerator, GeneratedAssetCache, ImageGenerator, ImagePathNormalizer, IssueRegistry, LinkImageValidator, MarkdownTransformer, MarkdownUtils, MathTransformer, MermaidTransformer, QrTransformer, ShowcaseSvgBuilder, ShowcaseTransformer, SvgFontEmbedder, TableConverter, TalkAvatarGenerator, TalkRegistry, ThemeImageResolver, ThemeValidator Classes: MarkdownPreprocessor, MermaidRenderer, PreProcessContext

Constant Summary collapse

FRONTISPIECE_DEFAULT_PATH =

テーマ画像のデフォルトパス定数

ThemeImageResolver::FRONTISPIECE_DEFAULT_PATH
ORNAMENT_DEFAULT_PATH =
ThemeImageResolver::ORNAMENT_DEFAULT_PATH

Class Method Summary collapse

Class Method Details

.apply_frontmatter(content, file_type, chapter_num, path: nil) ⇒ Object



148
149
150
# File 'lib/vivlio_starter/cli/pre_process.rb', line 148

def apply_frontmatter(content, file_type, chapter_num, path: nil)
  FrontmatterGenerator.apply_frontmatter(content, file_type, chapter_num, path: path)
end

.convert_book_card_inner_markdown(content) ⇒ Object



232
233
234
# File 'lib/vivlio_starter/cli/pre_process.rb', line 232

def convert_book_card_inner_markdown(content)
  MarkdownTransformer.convert_book_card_inner_markdown(content)
end

.convert_container_blocks(content, class_name:) ⇒ Object



242
243
244
# File 'lib/vivlio_starter/cli/pre_process.rb', line 242

def convert_container_blocks(content, class_name:)
  MarkdownTransformer.convert_container_blocks(content, class_name: class_name)
end

.detect_language(file_path) ⇒ Object

================================================================

Markdown変換関連メソッド (MarkdownTransformer への委譲)



212
213
214
# File 'lib/vivlio_starter/cli/pre_process.rb', line 212

def detect_language(file_path)
  MarkdownTransformer.detect_language(file_path)
end

.enable_verbose(command_or_ctx) ⇒ Object



92
93
94
95
# File 'lib/vivlio_starter/cli/pre_process.rb', line 92

def enable_verbose(command_or_ctx)
  opts = options_of(command_or_ctx)
  ENV['VERBOSE'] = '1' if opts[:verbose]
end

.ensure_variant_generated(source_path, variant) ⇒ Object



260
261
262
# File 'lib/vivlio_starter/cli/pre_process.rb', line 260

def ensure_variant_generated(source_path, variant)
  ImageGenerator.ensure_variant_generated(source_path, variant)
end

.execute_cross_references(entries) ⇒ Object

クロスリファレンス処理を実行する。 preprocess_sections! で全章の前処理が完了した後に1回だけ呼ぶこと。



76
77
78
79
80
81
82
# File 'lib/vivlio_starter/cli/pre_process.rb', line 76

def execute_cross_references(entries)
  entries = resolve_entries(entries)
  output_files = entries.map { File.basename(it.path) }
  Common.log_action("\nクロスリファレンス処理を開始します...")
  result = process_cross_references_for_files(output_files)
  Common.log_error('クロスリファレンス処理でエラーが発生しました') unless result
end

.execute_pre_process(command_or_ctx, entries) ⇒ Object

Parameters:

  • command_or_ctx (Hash, Object)

    コマンドコンテキスト

  • entries (Array<TokenResolver::Entry>)

    Entry オブジェクトの配列



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/vivlio_starter/cli/pre_process.rb', line 61

def execute_pre_process(command_or_ctx, entries)
  ctx = normalized_context(command_or_ctx)
  enable_verbose(ctx)

  entries = resolve_entries(entries)

  Common.log_action('Markdownファイルの前処理を行っています...')
  entries.each { process_single_markdown_file(it.path, it) }

  Common.log_success('Markdownの前処理が完了しました')
end

.fix_image_paths(content, filename) ⇒ Object

================================================================

画像パス正規化関連メソッド (ImagePathNormalizer への委譲)



179
180
181
# File 'lib/vivlio_starter/cli/pre_process.rb', line 179

def fix_image_paths(content, filename)
  ImagePathNormalizer.fix_image_paths(content, filename)
end

.format_book_card_inner_html(inner_html) ⇒ Object



237
238
239
# File 'lib/vivlio_starter/cli/pre_process.rb', line 237

def format_book_card_inner_html(inner_html)
  MarkdownTransformer.format_book_card_inner_html(inner_html)
end

.generate_frontispiece_and_ornament_from(image_spec) ⇒ Object

================================================================

画像生成関連メソッド (ImageGenerator への委譲)



255
256
257
# File 'lib/vivlio_starter/cli/pre_process.rb', line 255

def generate_frontispiece_and_ornament_from(image_spec, **)
  ImageGenerator.generate_frontispiece_and_ornament_from(image_spec, **)
end

.generate_frontmatter(file_type, chapter_num = nil, existing_frontmatter = {}) ⇒ Object

================================================================

フロントマター関連メソッド (FrontmatterGenerator への委譲)



143
144
145
# File 'lib/vivlio_starter/cli/pre_process.rb', line 143

def generate_frontmatter(file_type, chapter_num = nil, existing_frontmatter = {})
  FrontmatterGenerator.generate_frontmatter(file_type, chapter_num, existing_frontmatter)
end

.image_exists_for?(normalized_path) ⇒ Object



189
190
191
# File 'lib/vivlio_starter/cli/pre_process.rb', line 189

def image_exists_for?(normalized_path)
  ImagePathNormalizer.image_exists_for?(normalized_path)
end

.normalize_book_card_md(md_text) ⇒ Object



227
228
229
# File 'lib/vivlio_starter/cli/pre_process.rb', line 227

def normalize_book_card_md(md_text)
  MarkdownTransformer.normalize_book_card_md(md_text)
end

.normalized_context(command_or_ctx) ⇒ Object



85
86
87
88
89
# File 'lib/vivlio_starter/cli/pre_process.rb', line 85

def normalized_context(command_or_ctx)
  return command_or_ctx if command_or_ctx.is_a?(Hash)

  { options: options_of(command_or_ctx) }
end

.options_of(command_or_ctx) ⇒ Object



98
99
100
101
102
103
104
105
106
# File 'lib/vivlio_starter/cli/pre_process.rb', line 98

def options_of(command_or_ctx)
  if command_or_ctx.is_a?(Hash)
    command_or_ctx[:options] || {}
  elsif command_or_ctx.respond_to?(:options)
    command_or_ctx.options || {}
  else
    {}
  end
end

.placeholder_image_path(missing_image_path = nil) ⇒ Object



194
195
196
# File 'lib/vivlio_starter/cli/pre_process.rb', line 194

def placeholder_image_path(missing_image_path = nil)
  ImagePathNormalizer.placeholder_image_path(missing_image_path)
end

.process_code_include(content, source_filename: nil) ⇒ Object



247
248
249
# File 'lib/vivlio_starter/cli/pre_process.rb', line 247

def process_code_include(content, source_filename: nil)
  MarkdownTransformer.process_code_include(content, source_filename: source_filename)
end

.process_cross_references_for_files(md_files) ⇒ Object

全章ファイルのクロスリファレンス処理を一括実行

Parameters:

  • md_files (Array<String>)

    処理対象ファイル(プロジェクトルート直下 .md)の配列



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/vivlio_starter/cli/pre_process.rb', line 271

def process_cross_references_for_files(md_files)
  md_files = md_files.reject { |path| File.basename(path) == '99-colophon.md' }
  return true if md_files.empty?

  Common.log_info('=== クロスリファレンス処理を開始 ===')

  # ------------------------------------------------
  # Phase 1: catalog.yml 登録済みの全章からラベル定義を収集
  # ------------------------------------------------
  all_labels = []
  all_errors = []

  # catalog.yml 登録済みの全章を対象とする(未登録草稿は除外)
  catalog_entries = TokenResolver::Resolver.new.resolve.select(&:in_catalog?)
  contents_files = catalog_entries.map(&:path).select { |p| File.exist?(p) }
  Common.log_info("ラベル収集対象ファイル: #{contents_files.size}")

  contents_files.each do |md_path|
    filename = File.basename(md_path)
    content = File.read(md_path, encoding: 'utf-8')
    chapter_number = CrossReferenceProcessor.display_chapter_number_for_filename(filename)

    result = CrossReferenceProcessor.collect_labels(content, filename, chapter_number)
    all_labels.concat(result[:labels])
    all_errors.concat(result[:errors])
  end

  # ------------------------------------------------
  # Phase 2: ラベルマップ構築 & 重複チェック
  # ------------------------------------------------
  map_result = CrossReferenceProcessor.build_labels_map_with_duplicates_check(all_labels)
  labels_map = map_result[:labels_map]
  duplicates_by_id = map_result[:duplicates_by_id]

  if duplicates_by_id.any?
    duplicates_by_id.each do |_id, labels|
      first = labels.first
      by_file = labels.group_by(&:source_file)
      detail_lines = by_file.map do |file, file_labels|
        "#{file}: #{file_labels.map(&:line).join(', ')}"
      end
      Common.log_error(
        "#{first.source_file}:#{first.line} - ラベルID '#{first.title} @#{first.id}' は重複しています",
        detail: "重複箇所: #{detail_lines.join("\n          ")}"
      )
      # 章別サマリーへブリッジする(preflight-chapter-summary-spec.md §2.2)
      IssueRegistry.record(
        chapter: first.source_file, line: first.line, severity: :error,
        category: :cross_reference, message: "ラベルID '@#{first.id}' が重複しています"
      )
      all_errors << "ラベルID '@#{first.id}' 重複"
    end
    # 重複があっても先勝ちのラベルマップで処理を続行する
  end

  # ------------------------------------------------
  # Phase 3: 対象のワークスペース内(html/).md に対してのみ変換を適用
  # (前処理済み中間 .md の置き場・P4 §3.4-1)
  # ------------------------------------------------
  processed_chapters = {}

  md_files.each do |md_file|
    filename = File.basename(md_file)
    workspace_md = File.join(Common::BUILD_HTML_DIR, filename)
    next unless File.exist?(workspace_md)

    content = File.read(workspace_md, encoding: 'utf-8')

    # キャプション付きブロックをHTML化
    transformed = CrossReferenceProcessor.transform_captioned_blocks(content, filename, labels_map)

    # 本文中の @id を番号付きテキストに置換
    # - 実際の置換はワークスペース内(html/)の .md に対して実行
    # - 警告用の行番号は contents/ 配下の元Markdownに対して計算してログ出力する

    # 1) contents/ 側で未定義参照を検出(警告・行番号用)
    contents_path = File.join(Common::CONTENTS_DIR, filename)
    logging_errors = []
    if File.exist?(contents_path)
      source_content = File.read(contents_path, encoding: 'utf-8')
      logging_result = CrossReferenceProcessor.replace_references(source_content, labels_map, contents_path)
      logging_errors = logging_result[:errors]
    end

    # 2) ワークスペース内 .md に対して置換を適用(こちらのエラーは行番号がずれるため無視)
    ref_result = CrossReferenceProcessor.replace_references(transformed, labels_map, nil)
    processed_chapters[workspace_md] = ref_result[:content]

    # 3) エラー集計とログは contents/ 側の行番号に基づく
    all_errors.concat(logging_errors)

    next unless logging_errors.any?

    Common.log_warn(" #{filename}: #{logging_errors.size}個の未定義参照を検出")
    logging_errors.each do |msg|
      Common.log_warn("    - #{msg}")
      # 章別サマリーへブリッジする(同 spec §2.2)。行番号は contents/ 側の
      # メッセージに埋まっているため、ここでは章単位の件数として積む
      IssueRegistry.record(
        chapter: filename, severity: :warn,
        category: :cross_reference, message: "未定義の参照: #{msg}"
      )
    end
  end

  # ------------------------------------------------
  # Phase 4: 孤立ID検出(定義されているが一度も参照されていないID)
  # contents/ 全ファイルを対象に使用済みIDを収集し、未参照ラベルを警告する
  # ------------------------------------------------
  all_used_ids = Set.new
  contents_files.each do |md_path|
    source_content = File.read(md_path, encoding: 'utf-8')
    result = CrossReferenceProcessor.replace_references(source_content, labels_map, nil)
    all_used_ids.merge(result[:used_ids])
  end

  orphan_labels = labels_map.values.reject { |label| all_used_ids.include?(label.id) || label.auto }
  orphan_labels.each do |label|
    Common.log_warn(
      "#{label.source_file}:#{label.line} - 孤立ラベル '#{label.title} @#{label.id}' は未参照です"
    )
    IssueRegistry.record(
      chapter: label.source_file, line: label.line, severity: :warn,
      category: :cross_reference, message: "孤立ラベル '@#{label.id}' は未参照です"
    )
  end

  # 処理済みのファイルを書き戻す(ワークスペース内の .md)
  processed_chapters.each do |path, content|
    File.write(path, content, encoding: 'utf-8')
    Common.log_success("更新: #{path}")
  end

  Common.log_success("\n=== クロスリファレンス処理が完了しました ===")
  Common.log_info("検出ラベル数: #{all_labels.size}")
  Common.log_info("エラー数: #{all_errors.size}")

  true
end

.process_single_markdown_file(md_file, entry) ⇒ Object

単一 Markdown ファイルを処理

Parameters:

  • md_file (String)

    Markdown ファイルパス

  • entry (TokenResolver::Entry)

    章情報を持つ Entry オブジェクト



135
136
137
# File 'lib/vivlio_starter/cli/pre_process.rb', line 135

def process_single_markdown_file(md_file, entry)
  MarkdownPreprocessor.new(md_file, entry).run
end

.render_markdown_to_html(md_text) ⇒ Object



217
218
219
# File 'lib/vivlio_starter/cli/pre_process.rb', line 217

def render_markdown_to_html(md_text)
  MarkdownTransformer.render_markdown_to_html(md_text)
end

.report_frontmatter_error(error, frontmatter_yaml) ⇒ Object



153
154
155
# File 'lib/vivlio_starter/cli/pre_process.rb', line 153

def report_frontmatter_error(error, frontmatter_yaml)
  FrontmatterGenerator.report_frontmatter_error(error, frontmatter_yaml)
end

.resolve_all_content_entriesObject

contents/ 内の全 Markdown ファイルを Entry として解決



126
127
128
129
# File 'lib/vivlio_starter/cli/pre_process.rb', line 126

def resolve_all_content_entries
  resolver = TokenResolver::Resolver.new
  Dir.glob("#{Common::CONTENTS_DIR}/*.md").map { resolver.resolve_file(it) }
end

.resolve_entries(entries) ⇒ Array<TokenResolver::Entry>

Entry 配列を解決する。空の場合は全ファイルを TokenResolver で解決。

Parameters:

Returns:



112
113
114
115
116
117
118
119
120
121
122
# File 'lib/vivlio_starter/cli/pre_process.rb', line 112

def resolve_entries(entries)
  raw = Array(entries).compact
  return resolve_all_content_entries if raw.empty?

  # Entry オブジェクトならそのまま返す
  return raw if raw.first.respond_to?(:kind)

  # basename/パスの場合は TokenResolver で解決
  resolver = TokenResolver::Resolver.new
  raw.map { resolver.resolve_file(it) }
end

.resolve_frontispiece_path(raw, allow_generation: false) ⇒ Object

================================================================

テーマ画像解決関連メソッド (ThemeImageResolver への委譲)



161
162
163
# File 'lib/vivlio_starter/cli/pre_process.rb', line 161

def resolve_frontispiece_path(raw, allow_generation: false)
  ThemeImageResolver.resolve_frontispiece_path(raw, allow_generation: allow_generation)
end

.resolve_image_path(raw, default_when_nil:, downcase_if: nil) ⇒ Object



171
172
173
# File 'lib/vivlio_starter/cli/pre_process.rb', line 171

def resolve_image_path(raw, default_when_nil:, downcase_if: nil)
  ThemeImageResolver.resolve_image_path(raw, default_when_nil: default_when_nil, downcase_if: downcase_if)
end

.resolve_ornament_path(raw, allow_generation: false) ⇒ Object



166
167
168
# File 'lib/vivlio_starter/cli/pre_process.rb', line 166

def resolve_ornament_path(raw, allow_generation: false)
  ThemeImageResolver.resolve_ornament_path(raw, allow_generation: allow_generation)
end

.resolved_placeholder_or_path(alt_text, normalized_path) ⇒ Object



184
185
186
# File 'lib/vivlio_starter/cli/pre_process.rb', line 184

def resolved_placeholder_or_path(alt_text, normalized_path)
  ImagePathNormalizer.resolved_placeholder_or_path(alt_text, normalized_path)
end

.sanitize_placeholder_text(filename) ⇒ Object



199
200
201
# File 'lib/vivlio_starter/cli/pre_process.rb', line 199

def sanitize_placeholder_text(filename)
  ImagePathNormalizer.sanitize_placeholder_text(filename)
end

.svg_to_data_uri(svg_content) ⇒ Object



204
205
206
# File 'lib/vivlio_starter/cli/pre_process.rb', line 204

def svg_to_data_uri(svg_content)
  ImagePathNormalizer.svg_to_data_uri(svg_content)
end


222
223
224
# File 'lib/vivlio_starter/cli/pre_process.rb', line 222

def transform_links_to_footnotes(md_text)
  MarkdownTransformer.transform_links_to_footnotes(md_text)
end