Module: Vivlio::Starter::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/markdown_utils.rb,
lib/vivlio/starter/cli/pre_process/image_generator.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/theme_image_resolver.rb,
lib/vivlio/starter/cli/pre_process/frontmatter_generator.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/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: CrossReferenceProcessor, CssUpdater, DataRender, FrontmatterGenerator, ImageGenerator, ImagePathNormalizer, LinkImageValidator, MarkdownTransformer, MarkdownUtils, ThemeImageResolver Classes: MarkdownPreprocessor, 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



146
147
148
# File 'lib/vivlio/starter/cli/pre_process.rb', line 146

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



230
231
232
# File 'lib/vivlio/starter/cli/pre_process.rb', line 230

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

.convert_container_blocks(content, class_name:) ⇒ Object



250
251
252
# File 'lib/vivlio/starter/cli/pre_process.rb', line 250

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

.convert_table_container_inner_markdown(content, class_name) ⇒ Object



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

def convert_table_container_inner_markdown(content, class_name)
  MarkdownTransformer.convert_table_container_inner_markdown(content, class_name)
end

.convert_table_rotate_inner_markdown(content) ⇒ Object



240
241
242
# File 'lib/vivlio/starter/cli/pre_process.rb', line 240

def convert_table_rotate_inner_markdown(content)
  MarkdownTransformer.convert_table_rotate_inner_markdown(content)
end

.detect_language(file_path) ⇒ Object

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



210
211
212
# File 'lib/vivlio/starter/cli/pre_process.rb', line 210

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

.enable_verbose(command_or_ctx) ⇒ Object



90
91
92
93
# File 'lib/vivlio/starter/cli/pre_process.rb', line 90

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



273
274
275
# File 'lib/vivlio/starter/cli/pre_process.rb', line 273

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

.execute_cross_references(entries) ⇒ Object

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



74
75
76
77
78
79
80
# File 'lib/vivlio/starter/cli/pre_process.rb', line 74

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 オブジェクトの配列



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/vivlio/starter/cli/pre_process.rb', line 59

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 への委譲)



177
178
179
# File 'lib/vivlio/starter/cli/pre_process.rb', line 177

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

.format_book_card_inner_html(inner_html) ⇒ Object



245
246
247
# File 'lib/vivlio/starter/cli/pre_process.rb', line 245

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 への委譲)



268
269
270
# File 'lib/vivlio/starter/cli/pre_process.rb', line 268

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 への委譲)



141
142
143
# File 'lib/vivlio/starter/cli/pre_process.rb', line 141

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



187
188
189
# File 'lib/vivlio/starter/cli/pre_process.rb', line 187

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

.normalize_book_card_md(md_text) ⇒ Object



225
226
227
# File 'lib/vivlio/starter/cli/pre_process.rb', line 225

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

.normalized_context(command_or_ctx) ⇒ Object



83
84
85
86
87
# File 'lib/vivlio/starter/cli/pre_process.rb', line 83

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



96
97
98
99
100
101
102
103
104
# File 'lib/vivlio/starter/cli/pre_process.rb', line 96

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

.pipe_table_to_html(md_text) ⇒ Object



235
236
237
# File 'lib/vivlio/starter/cli/pre_process.rb', line 235

def pipe_table_to_html(md_text)
  MarkdownTransformer.pipe_table_to_html(md_text)
end

.placeholder_image_path(missing_image_path = nil) ⇒ Object



192
193
194
# File 'lib/vivlio/starter/cli/pre_process.rb', line 192

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

.process_code_include(content, source_filename: nil) ⇒ Object



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

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

.process_cross_references(chapters) ⇒ Object

クロスリファレンス関連メソッド (MarkdownTransformer への委譲)



281
282
283
# File 'lib/vivlio/starter/cli/pre_process.rb', line 281

def process_cross_references(chapters)
  MarkdownTransformer.process_cross_references(chapters)
end

.process_cross_references_for_files(md_files) ⇒ Object

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

Parameters:

  • md_files (Array<String>)

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



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 288

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          ")}"
      )
      all_errors << "ラベルID '@#{first.id}' 重複"
    end
    # 重複があっても先勝ちのラベルマップで処理を続行する
  end

  # ------------------------------------------------
  # Phase 3: 対象のルート直下 .md に対してのみ変換を適用
  # ------------------------------------------------
  processed_chapters = {}

  md_files.each do |md_file|
    filename = File.basename(md_file)
    next unless File.exist?(filename)

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

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

    # 本文中の @id を番号付きテキストに置換
    # - 実際の置換はプロジェクトルート直下の .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[filename] = 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}")
    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}' は未参照です"
    )
  end

  # 処理済みのファイルを書き戻す(プロジェクトルート直下の .md)
  processed_chapters.each do |filename, content|
    File.write(filename, content, encoding: 'utf-8')
    Common.log_success("更新: #{filename}")
  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 オブジェクト



133
134
135
# File 'lib/vivlio/starter/cli/pre_process.rb', line 133

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

.render_markdown_to_html(md_text) ⇒ Object



215
216
217
# File 'lib/vivlio/starter/cli/pre_process.rb', line 215

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

.report_frontmatter_error(error, frontmatter_yaml) ⇒ Object



151
152
153
# File 'lib/vivlio/starter/cli/pre_process.rb', line 151

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

.resolve_all_content_entriesObject

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



124
125
126
127
# File 'lib/vivlio/starter/cli/pre_process.rb', line 124

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:



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/vivlio/starter/cli/pre_process.rb', line 110

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 への委譲)



159
160
161
# File 'lib/vivlio/starter/cli/pre_process.rb', line 159

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



169
170
171
# File 'lib/vivlio/starter/cli/pre_process.rb', line 169

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



164
165
166
# File 'lib/vivlio/starter/cli/pre_process.rb', line 164

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



182
183
184
# File 'lib/vivlio/starter/cli/pre_process.rb', line 182

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

.sanitize_placeholder_text(filename) ⇒ Object



197
198
199
# File 'lib/vivlio/starter/cli/pre_process.rb', line 197

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

.svg_to_data_uri(svg_content) ⇒ Object



202
203
204
# File 'lib/vivlio/starter/cli/pre_process.rb', line 202

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


220
221
222
# File 'lib/vivlio/starter/cli/pre_process.rb', line 220

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