Module: VivlioStarter::CLI::Common

Defined in:
lib/vivlio_starter/cli/common.rb

Constant Summary collapse

REQUIRED_YAML_FILES =

--- 定数定義 ---

%w[
  config/book.yml config/catalog.yml config/page_presets.yml
].freeze
CONFIG_FILE =
'config/book.yml'
PAGE_PRESETS_FILE =
'config/page_presets.yml'
FONT_SIZE_KEYS =
%i[base_font_size column_font_size folio_font_size].freeze
PAGE_PRESET_EXCLUDE_KEYS =
%i[preset use preset_name].freeze
DIRECT_PAGE_PRESET =

直接ビルド(book.yml を持たない単一 Markdown)の版面。日本の技術書の主流サイズであり、 resolve_page_size の既定フォールバック(B5)とも一致する。

'b5_standard'
LEVELS =
{ 'error' => 0, 'warn' => 1, 'info' => 2, 'success' => 2, 'action' => 2, 'debug' => 3 }.freeze
DEFAULT_LOG_LEVEL =

--log 未指定時のレベル(🟡 警告と 🔴 エラーだけを出す)

LEVELS['warn']
LOG_LEVEL_NAMES =

利用者に案内するログレベル名(success / action は内部の分類なので出さない)

%w[error warn info debug].freeze
CONFIG_DIR =
'config'
CONTENTS_DIR =
'contents'
STYLESHEETS_DIR =
'stylesheets'
IMAGES_DIR =
'images'
DATA_DIR =
'data'
CODES_DIR =
'codes'
TEMPLATES_DIR =
'templates'
COVERS_DIR =
'covers'
VFM_COMMAND =
'vfm'
CACHE_DIR =
'.cache/vs'
VIVLIOSTYLE_CONFIG_FILE =

旧バージョン(撤去済み手動フロー)のルート config 名。 doctor の旧プロジェクト検出マーカーとしてのみ参照する。

'vivliostyle.config.js'
BUILD_DIR =

ビルドワークスペース(P4: 中間生成物の分離場所)

中間 md/HTML/中間 PDF/EPUB 作業物はルートではなくこの配下に閉じる。 4 消費者 dir は同一深度(ルートから 4 階層)にすることが仕様の要: 資産への相対プレフィックスが全消費者で共通になり、html/ から 消費者 dir へのコピーが無加工(バイト同一)で成立する(P4 §3.1)。

"#{CACHE_DIR}/build"
BUILD_HTML_DIR =
"#{BUILD_DIR}/html"
BUILD_PDF_DIR =
"#{BUILD_DIR}/pdf"
BUILD_EPUB_DIR =
"#{BUILD_DIR}/epub"
BUILD_KINDLE_DIR =
"#{BUILD_DIR}/kindle"
BUILD_VIVLIOSTYLE_PDF_DIR =

vivliostyle の作業ディレクトリ(生成 config の workspaceDir)。 中身はプロジェクトの木の写し(実測 15,263 ファイル)で、直下の publication.json は各ビルドが自分のエントリ一覧で上書きする。1 つを共有した まま 2 つの vivliostyle が同時に走れば確実に踏み合うため、PDF 枝と EPUB 枝で 分ける(build-target-parallelization-spec.md §3.1)。 消費者 dir と同じ 4 階層に置くこと——.vivliostyle/pdf のように 1 段 深くすると資産への相対プレフィックス(ASSET_PREFIX)が合わなくなる。

"#{BUILD_DIR}/.vivliostyle-pdf"
BUILD_VIVLIOSTYLE_EPUB_DIR =
"#{BUILD_DIR}/.vivliostyle-epub"
INDEX_MATCHES_FILE =

索引スキャン結果の中間 YAML(書き手 IndexMatchScanner・読み手 UnifiedPageBuilder)。 ルートではなくワークスペース直下へ置き、ルート無汚染を保つ(P4b §2.5)。

"#{BUILD_DIR}/_index_matches.yml"
COVER_CACHE_DIR =

再生成コストの高い生成資産のキャッシュ(generated-assets 移設仕様 §2)

BUILD_DIR の外に置くのが要点: final clean(rm_rf BUILD_DIR)を生き延び、 waifu2x を伴う高コストなバリアント生成や covers の毎ビルド再生成を避ける。 前処理の生成資産(mermaid / showcase / math)も同方針で CACHE_DIR 配下に置く (PreProcessCommands::GeneratedAssetCache が .cache/vs/<種別>/ を管理する)。

"#{CACHE_DIR}/covers"
THEME_IMAGES_CACHE_DIR =
"#{CACHE_DIR}/theme-images"
ASSET_PREFIX =

中間 HTML/md から著者資産(stylesheets/ images/ 等)への相対プレフィックス。 生成時に正しいプレフィックスで書く(コピー時 gsub はしない)が P4 §3.3 の方針。 資産参照を生成する choke point(FrontmatterGenerator / ImagePathNormalizer / MathTransformer / Techbook::Processor / TocGenerator / UnifiedPageBuilder)は 必ずこの値を参照すること。 ワークスペース(ルートから 4 階層)からルート資産への上方参照。

'../../../../'
RESERVED_CONFIG_KEYS =

Data の既存メソッドと衝突すると [] やドット記法が member を返せなくなるため、 ロード時に警告する(dig は wrap_config が後付けするメソッド)

(Data.instance_methods | %i[dig]).freeze
RETIRED_CONFIG_KEYS =

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

廃止した設定キー

キーを廃止したらここへ 1 行足すだけでよい。検出も案内もこの表が担い、 各コマンドは何も書かない。

なぜ CONFIG では判定できないか:

CONFIG は既定値スキーマとマージした「実効値」の view であって、著者が
何を書いたかを答えるものではない。スキーマ外のキーは自由拡張のために
素通しするので、今は CONFIG の形からでも記述の有無を言い当てられる。
だがそれは「そのキーがスキーマに無い」ことに寄りかかった判定で、同名の
キーが既定値付きで復活した途端、誰も書いていないのに警告が出るようになる。
ここで問うているのは記述の有無なので、マージ前の生の記述を見る。

値は「代わりにどうするか」。著者が読んで行動できる文言にすること (警告は具体的な修正案とセットにする、が本プロジェクトの流儀)。

ConfigKeys.retirement_notices.freeze
DETAIL_INDENT =

detail 行のインデント幅(半角スペース 8 文字)

'        '
EMIT_SINK_KEY =

出力先を差し替えるためのスレッドローカル鍵(with_emit_sink が管理する)

:vivlio_starter_emit_sink
PAGE_SIZES =

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

Page Size Utilities

{
  'A4' => { width: '210mm', height: '297mm' },
  'A5' => { width: '148mm', height: '210mm' },
  # 'B5' は技術書慣習により JIS 寸法(182×257)の別名。ISO B5(176×250)は非サポート。
  'B5' => { width: '182mm', height: '257mm' },
  'JIS-B5' => { width: '182mm', height: '257mm' }
}.freeze
VIVLIOSTYLE_TIMINGS_KEY =

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

Build Timing & Step Tracking

:vivlio_starter_vivliostyle_timings
VIVLIOSTYLE_CURRENT_STEP_KEY =
:vivlio_starter_current_step_label
REQUIRED_BOOK_KEYS =

未設定だと成果物が目に見えて欠ける主要キーと、その記入例。 記入例は警告の「直し方」としてそのまま見せるので、著者が貼って直せる形で書く。

ConfigKeys.authored_examples.freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.log_levelObject



362
# File 'lib/vivlio_starter/cli/common.rb', line 362

def log_level = @log_level || DEFAULT_LOG_LEVEL

Class Method Details

.abort_with_error(msg) ⇒ Object



855
856
857
858
859
# File 'lib/vivlio_starter/cli/common.rb', line 855

def abort_with_error(msg)
  log_error(msg)
  log_error('コマンドを中止します')
  exit 1
end

.appendix_number_to_letter(num, entries: nil) ⇒ Object

付録の章番号をビルド対象の付録の順番に基づいてレター(a〜i)に変換する。 entries が渡された場合はその中の付録の順番を使い、 渡されない場合は catalog.yml の付録一覧から順番を取得する。

Parameters:

  • num (Integer, String)

    付録の章番号(90〜98)

  • entries (Array, nil) (defaults to: nil)

    ビルド対象の Entry 配列(単章ビルド時に渡す)



698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
# File 'lib/vivlio_starter/cli/common.rb', line 698

def appendix_number_to_letter(num, entries: nil)
  n = num.to_i
  return nil unless n.between?(90, 98)

  # ビルド対象のエントリ(付録を含む非空配列)が渡された場合のみ、その並びを使う。
  # 空配列は「指定なし」として扱い catalog 全体から付録順を取り直す。
  # (フルビルドでは本文章のみの override が渡され、付録抽出後に空配列となるため、
  #   ここで全体 resolve に委ねないと末尾フォールバックに落ちて採番がずれる)
  appendix_entries = if entries && !entries.empty?
                       entries.select { it.kind == :appendix }.sort_by { it.number.to_i }
                     else
                       resolver = TokenResolver::Resolver.new
                       resolver.resolve.select { it.kind == :appendix }.sort_by { it.number.to_i }
                     end

  index = appendix_entries.index { it.number.to_i == n }
  return ('a'..'i').to_a[index] if index

  # 見つからない場合は章番号から直接計算(フォールバック)
  ('a'..'i').to_a[n - 90]
rescue StandardError
  nil
end

.appendix_template_pathObject



995
# File 'lib/vivlio_starter/cli/common.rb', line 995

def appendix_template_path = template_path('appendix')

.apply_log_level!(command) ⇒ Object

解析済みコマンドの --log 指定からログレベルを確定する。 呼ぶのは CLI.start の 1 箇所だけ(各所で ARGV を読み直さない)。



370
371
372
# File 'lib/vivlio_starter/cli/common.rb', line 370

def apply_log_level!(command)
  Common.log_level = resolve_log_level(log_option_value(command))
end

.apply_page_preset(cfg) ⇒ Object

--- Hardcoded Defaults (Data objects for immutability) ---



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/vivlio_starter/cli/common.rb', line 287

def apply_page_preset(cfg)
  case cfg
  in { page: { **page_cfg } }
    preset_name = page_cfg.values_at(*PAGE_PRESET_EXCLUDE_KEYS).find { _1 }
    return cfg if blank?(preset_name)

    presets = load_page_presets
    case presets[preset_name.to_sym]
    in Hash => selected
      # プリセット既定 < 著者インライン値(page_cfg)。page_cfg は use 等の選択子キーも
      # 含むが、選択子は既に消費済みで害はない(仕様 §3.6)。
      cfg.merge(page: normalize_page_units(selected.merge(page_cfg)))
    else
      cfg
    end
  else
    cfg
  end
end

.asset_prefixObject

ワークスペース関連(P4)



1008
# File 'lib/vivlio_starter/cli/common.rb', line 1008

def asset_prefix       = ASSET_PREFIX

.blank?(v) ⇒ Object



644
# File 'lib/vivlio_starter/cli/common.rb', line 644

def blank?(v) = v.nil? || v.to_s.strip.empty?

.build_dirObject



1009
# File 'lib/vivlio_starter/cli/common.rb', line 1009

def build_dir          = BUILD_DIR

.build_direct_configuration(overrides = {}) ⇒ Data

直接ビルド(vs build myawesome.md)用の CONFIG を YAML を読まずに組み立てる。 「設定ファイルを介さない」が本機能の定義のため book.yml は一切参照せず、 既定値スキーマ+版面プリセットに呼び出し側の上書きを重ねるだけで完結させる。 仕様: direct-build-spec.md §2.3

Parameters:

  • overrides (Hash) (defaults to: {})

    既定値へ深くマージする設定(シンボルキー)

Returns:

  • (Data)

    frozen な CONFIG 相当オブジェクト



893
894
895
896
# File 'lib/vivlio_starter/cli/common.rb', line 893

def build_direct_configuration(overrides = {})
  base = merge_hardcoded_defaults({ page: direct_page_settings })
  wrap_config(deep_merge_config(base, overrides)).freeze
end

.build_html_dirObject



1010
# File 'lib/vivlio_starter/cli/common.rb', line 1010

def build_html_dir     = BUILD_HTML_DIR

.build_pdf_dirObject



1011
# File 'lib/vivlio_starter/cli/common.rb', line 1011

def build_pdf_dir      = BUILD_PDF_DIR

.cache_dirObject

キャッシュ関連(directories 同様、2026-08-08 にシステム定数へ戻した)



999
# File 'lib/vivlio_starter/cli/common.rb', line 999

def cache_dir          = CACHE_DIR

.cache_enabled?Object



1000
# File 'lib/vivlio_starter/cli/common.rb', line 1000

def cache_enabled?     = true

.chapter_template_pathObject



993
# File 'lib/vivlio_starter/cli/common.rb', line 993

def chapter_template_path = template_path('chapter')

.collect_key_paths(node, prefix = [], into = ) ⇒ Object

ネストしたハッシュのキーパスを集合にする(authored_keys の作成用)



267
268
269
270
271
272
273
274
275
276
# File 'lib/vivlio_starter/cli/common.rb', line 267

def collect_key_paths(node, prefix = [], into = Set[])
  return into unless node.is_a?(Hash)

  node.each do |key, value|
    path = prefix + [key]
    into << path
    collect_key_paths(value, path, into)
  end
  into
end

.config_dirObject

ディレクトリ関連

かつては book.yml の directories.* で改名できたが、2026-08-08 にシステム定数へ戻した。 設定として提供しながら半分のコードしか見ていなかった——定数直参照が 48 箇所、 このメソッド経由が 43 箇所で、改名すると両者が別の場所を指す。加えて vs create の 案内文や原稿の解説(「contents/ に原稿を書きます」)がすべて嘘になる。 メソッドは呼び出し 43 箇所のために残す(定数を返すだけ)。



980
# File 'lib/vivlio_starter/cli/common.rb', line 980

def config_dir         = CONFIG_DIR

.config_dir_pathObject



981
# File 'lib/vivlio_starter/cli/common.rb', line 981

def config_dir_path    = resolve_path_from_root(config_dir)

.configured?Object

CONFIG が未ロード(プロジェクト外)の場合に呼び出し元で検査するためのヘルパー



956
# File 'lib/vivlio_starter/cli/common.rb', line 956

def configured? = !CONFIG.nil?

.confirm?(message, default: false, input: $stdin) ⇒ Boolean

取り返しのつかない操作の前に確認を取る(❓)。ログレベルに関わらず常に表示する。 各コマンドが独自に print していると絵文字も表記も揃わないため、ここに集約する。

Parameters:

  • message (String)

    「〜しますか?」までの質問文

  • default (Boolean) (defaults to: false)

    Enter だけで確定したときの答え(既定は「いいえ」)

  • input (IO) (defaults to: $stdin)

    応答の読み取り先(テストや DI で差し替える)

Returns:

  • (Boolean)

    実行してよいか



497
498
499
500
501
502
503
504
505
506
# File 'lib/vivlio_starter/cli/common.rb', line 497

def confirm?(message, default: false, input: $stdin)
  Spinner.clear_active_line
  $stdout.print("#{message} #{default ? '[Y/n]' : '[y/N]'}: ")
  $stdout.flush

  answer = input.gets&.strip&.downcase
  return default if answer.nil? || answer.empty?

  %w[y yes].include?(answer)
end

.consume_vivliostyle_build_timingsObject



829
830
831
832
833
# File 'lib/vivlio_starter/cli/common.rb', line 829

def consume_vivliostyle_build_timings
  timings = Thread.current[VIVLIOSTYLE_TIMINGS_KEY] || []
  Thread.current[VIVLIOSTYLE_TIMINGS_KEY] = []
  timings
end

.contents_dirObject



982
# File 'lib/vivlio_starter/cli/common.rb', line 982

def contents_dir       = CONTENTS_DIR

.cover_cache_dirObject

生成資産キャッシュ(covers 生成物・テーマ画像バリアント)。 cache.dir 設定で cache_dir が変わっても追従するようヘルパ経由で参照する。



1004
# File 'lib/vivlio_starter/cli/common.rb', line 1004

def cover_cache_dir        = File.join(cache_dir, 'covers')

.cover_themeObject

カバー設定関連(CONFIG&. は CONFIG 未ロード(プロジェクト外)を吸収する。 各セクションは既定値スキーマで存在保証されるため、以降はドットで辿れる)



1025
# File 'lib/vivlio_starter/cli/common.rb', line 1025

def cover_theme        = CONFIG&.output&.cover

.covers_dirObject



987
# File 'lib/vivlio_starter/cli/common.rb', line 987

def covers_dir         = COVERS_DIR

.current_log_levelObject

現在のログレベル。error: 0 / warn: 1 / info,success,action: 2 / debug: 3



366
# File 'lib/vivlio_starter/cli/common.rb', line 366

def current_log_level = Common.log_level

.current_step_labelObject



851
852
853
# File 'lib/vivlio_starter/cli/common.rb', line 851

def current_step_label
  Thread.current[VIVLIOSTYLE_CURRENT_STEP_KEY]
end

.data_dirObject



985
# File 'lib/vivlio_starter/cli/common.rb', line 985

def data_dir           = DATA_DIR

.deep_merge_config(default, user) ⇒ Object

既定値と book.yml の値を再帰的にマージする。 著者が「キーだけ書いて値を空欄」にした場合(nil)は既定値を採用する。 false は明示的な設定として尊重する(nil のみ既定値扱い)。



210
211
212
213
214
215
216
# File 'lib/vivlio_starter/cli/common.rb', line 210

def deep_merge_config(default, user)
  case [default, user]
  in [Hash => d, Hash => u] then d.merge(u) { |_k, dv, uv| deep_merge_config(dv, uv) }
  in [_, nil] then default
  else user
  end
end

.default_config_schemaObject

book.yml の全セクションの既定値スキーマ。 コードが参照する既知キーを列挙し、未設定時のドット記法アクセスを保証する。 nil は「既定値なし(未設定)」を表し、実際の既定値は従来どおり参照側が決める。 book.yml の全セクションの既定値スキーマ。 宣言は ConfigKeys::KEYS が持つ(config-defaults-design-spec.md)。 ここは葉キーの宣言から入れ子ハッシュを組み立て直すだけで、値は持たない。



284
# File 'lib/vivlio_starter/cli/common.rb', line 284

def default_config_schema = ConfigKeys.default_schema

.direct_page_settingsObject

直接ビルドの版面。page の版面キー(size/margin_* 等)は既定値スキーマに無く page_presets.yml 由来のため、ここでプリセットを 1 つ解決して埋める。 プリセットが読めない環境でも組版自体は page-settings.css の既定で成立する。



901
902
903
904
905
906
# File 'lib/vivlio_starter/cli/common.rb', line 901

def direct_page_settings
  apply_page_preset({ page: { use: DIRECT_PAGE_PRESET } })[:page]
rescue StandardError => e
  log_debug("[direct] 版面プリセットの解決に失敗しました(既定値で続行): #{e.message}")
  { use: DIRECT_PAGE_PRESET }
end

.emit(line) ⇒ Object

ログ行を出力する唯一の出口。スピナーが回っていれば行を消してから出す (消さないとスピナーの残骸とログが同じ行に重なる)。

出力先はスレッドごとに差し替えられる。並列ビルドの子枝はここへ配列を挿し、 親が合流時にまとめて吐く——スピナーと 2 つの枝が同じ TTY を奪い合うと行が 混ざるため、書き手を常に 1 つに保つ(build-target-parallelization-spec.md §3.4)。



408
409
410
411
412
413
414
# File 'lib/vivlio_starter/cli/common.rb', line 408

def emit(line)
  sink = Thread.current[EMIT_SINK_KEY]
  return sink << line if sink

  Spinner.clear_active_line
  puts(line)
end

.ensure_build_workspace!Object

4 消費者 dir を作成してワークスペースを準備する



1015
1016
1017
1018
# File 'lib/vivlio_starter/cli/common.rb', line 1015

def ensure_build_workspace!
  [BUILD_HTML_DIR, BUILD_PDF_DIR, BUILD_EPUB_DIR, BUILD_KINDLE_DIR].each { FileUtils.mkdir_p(it) }
  BUILD_DIR
end

.ensure_cache_dir!Object



668
669
670
671
672
# File 'lib/vivlio_starter/cli/common.rb', line 668

def ensure_cache_dir!
  dir = cache_dir
  FileUtils.mkdir_p(dir)
  dir
end

.ensure_configured!Object



958
959
960
961
962
963
964
965
966
967
# File 'lib/vivlio_starter/cli/common.rb', line 958

def ensure_configured!
  warn_retired_config_keys
  warn_missing_book_config
  return if configured?

  # 欠落と破損で正確な理由を出し分けるため、ファイル単位の検証に委ねて abort する
  # (破損時は vs doctor --fix による修復導線も案内される)
  ensure_required_yaml_files!
  abort_with_error('設定ファイルの読み込みに失敗しました: config/book.yml')
end

.ensure_external_command!(cmd, purpose: nil) ⇒ Object

コマンドが見つからない場合は vs doctor 案内付きで例外を送出する。

Parameters:

  • cmd (String)

    実行形式コマンド名

  • purpose (String, nil) (defaults to: nil)

    用途説明

Raises:

  • (StandardError)

    コマンドが見つからない場合



560
561
562
563
564
# File 'lib/vivlio_starter/cli/common.rb', line 560

def ensure_external_command!(cmd, purpose: nil)
  return if external_command_available?(cmd)

  raise missing_external_command_message(cmd, purpose: purpose)
end

.ensure_required_yaml_files!Object

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

Validation & Loading



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/vivlio_starter/cli/common.rb', line 163

def ensure_required_yaml_files!
  REQUIRED_YAML_FILES.each do |path|
    abort_with_error("必須設定ファイルが見つかりません: #{path}") unless File.file?(path)

    case YAML.safe_load(File.read(path, encoding: 'utf-8'), aliases: true, symbolize_names: true)
    in Hash | Array
      # Valid
    else
      abort_with_error("必須設定ファイルの内容が空、または形式が不正です: #{path}\n        修復するには vs doctor --fix を実行してください(破損ファイルはバックアップを取得します)")
    end
  rescue StandardError => e
    abort_with_error("必須設定ファイルの解析に失敗しました (#{path}): #{e.message}\n        修復するには vs doctor --fix を実行してください(破損ファイルはバックアップを取得します)")
  end
end

.epub_embed?Object



1028
1029
# File 'lib/vivlio_starter/cli/common.rb', line 1028

def epub_embed?        = CONFIG&.output&.epub&.embed == true
# Kindle 表紙の埋め込み。未設定時は false(二重表紙回避・§1-6)。

.external_command_available?(cmd) ⇒ Boolean


外部コマンド可用性チェック

PATH を走査してコマンドが実行可能か判定する。

Parameters:

  • cmd (String)

    実行形式コマンド名(絶対パスも可)

Returns:

  • (Boolean)


523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/vivlio_starter/cli/common.rb', line 523

def external_command_available?(cmd)
  candidate = cmd.to_s.strip
  return false if candidate.empty?

  if candidate.include?(File::SEPARATOR)
    return File.executable?(candidate) && !File.directory?(candidate)
  end

  ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
    path = File.join(dir, candidate)
    File.executable?(path) && !File.directory?(path)
  end
end

.format_converter_stderr(text) ⇒ Object

run_svg_converter! 用に stderr テキストをユーザー向けに整形する。 空のとき / 長すぎるときを吸収する。



613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
# File 'lib/vivlio_starter/cli/common.rb', line 613

def format_converter_stderr(text)
  trimmed = text.to_s.strip
  return 'stderr: (出力なし)' if trimmed.empty?

  lines = trimmed.lines.map(&:chomp)
  shown = if lines.size > 12
            head = lines.first(8)
            tail = lines.last(3)
            [*head, '  ... (中略) ...', *tail]
          else
            lines
          end
  indented = shown.map { |l| "  #{l}" }.join("\n")
  "stderr:\n#{indented}"
end

.format_detail(detail) ⇒ Object

detail 文字列を行配列に変換する。nil の場合は空配列を返す。 log_* からのみ呼ばれる内部ヘルパー。



510
511
512
513
514
# File 'lib/vivlio_starter/cli/common.rb', line 510

def format_detail(detail)
  return [] if detail.nil?

  detail.lines.map(&:chomp)
end

.generate_compressed_pdf_filename(target = 'pdf') ⇒ Object



806
807
808
809
810
# File 'lib/vivlio_starter/cli/common.rb', line 806

def generate_compressed_pdf_filename(target = 'pdf')
  # 新しい設定構造ではsuffixは"_compressed"に固定
  suffix = 'compressed'
  generate_output_filename(target, suffix: suffix)
end

.generate_cover_output_filename(side) ⇒ String

印刷カバー PDF のルート成果品名(generated-assets 移設仕様 §3.4)。 generate_output_filename と同じ include_version 規則に従う。 例: vivlio_starter_frontcover_v1.0.0.pdf

Parameters:

  • side (String, Symbol)

    'front' | 'back'

Returns:

  • (String)


791
792
793
794
795
796
797
798
# File 'lib/vivlio_starter/cli/common.rb', line 791

def generate_cover_output_filename(side)
  project = CONFIG&.project
  include_version = CONFIG&.output&.include_version || false

  filename = "#{project&.name || 'vivlio_starter'}_#{side}cover"
  filename += "_v#{project&.version}" if include_version && !blank?(project&.version)
  "#{filename}.pdf"
end

.generate_epub_filenameObject



799
800
# File 'lib/vivlio_starter/cli/common.rb', line 799

def generate_epub_filename = generate_output_filename('epub')
# Kindle の最終成果物(KPF・ルート直下)。例: vivlio_starter_v1.0.0.kpf

.generate_kindle_epub_filenameObject

Kindle 用の中間 EPUB(kindlepreviewer の入力)。KPF 生成後は削除される(§1-4)。 例: vivlio_starter_v1.0.0-kindle.epub



804
# File 'lib/vivlio_starter/cli/common.rb', line 804

def generate_kindle_epub_filename = generate_kpf_filename.sub(/\.kpf\z/, '-kindle.epub')

.generate_kpf_filenameObject

Kindle の最終成果物(KPF・ルート直下)。例: vivlio_starter_v1.0.0.kpf



801
802
803
# File 'lib/vivlio_starter/cli/common.rb', line 801

def generate_kpf_filename = generate_output_filename('kindle')
# Kindle 用の中間 EPUB(kindlepreviewer の入力)。KPF 生成後は削除される(§1-4)。
# 例: vivlio_starter_v1.0.0-kindle.epub

.generate_output_filename(target = 'pdf', suffix: nil) ⇒ Object

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

Output Filename Generation



762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
# File 'lib/vivlio_starter/cli/common.rb', line 762

def generate_output_filename(target = 'pdf', suffix: nil)
  project = CONFIG&.project
  project_name = project&.name || 'vivlio_starter'
  project_version = project&.version
  include_version = CONFIG&.output&.include_version || false

  filename = project_name.to_s.dup
  filename += '_print' if target == 'print_pdf'
  filename += "_v#{project_version}" if include_version && !blank?(project_version)
  if suffix && !blank?(suffix) && target == 'pdf'
    filename += (suffix.to_s.start_with?('_') ? suffix : "_#{suffix}")
  end

  ext = case target
        when 'pdf', 'print_pdf' then '.pdf'
        when 'epub' then '.epub'
        when 'kindle' then '.kpf'
        else '.pdf'
        end
  filename + ext
end

.generate_print_pdf_filenameObject



784
# File 'lib/vivlio_starter/cli/common.rb', line 784

def generate_print_pdf_filename = generate_output_filename('print_pdf')

.images_dirObject



984
# File 'lib/vivlio_starter/cli/common.rb', line 984

def images_dir         = IMAGES_DIR

.index_matches_fileObject



1012
# File 'lib/vivlio_starter/cli/common.rb', line 1012

def index_matches_file = INDEX_MATCHES_FILE

.install_configuration!(config) ⇒ Object

CONFIG 定数を差し替える唯一の入口(既存なら削除して再定義警告を回避)。 book.yml 由来の reload_configuration! と、YAML を読まずに組み立てる 直接ビルド(build_direct_configuration)の両方がここを通る。

Parameters:

  • config (Data, nil)

    wrap_config 済みの設定(nil = 未ロード状態へ戻す)



882
883
884
885
# File 'lib/vivlio_starter/cli/common.rb', line 882

def install_configuration!(config)
  remove_const(:CONFIG) if const_defined?(:CONFIG)
  const_set(:CONFIG, config)
end

.kindle_embed?Object

Kindle 表紙の埋め込み。未設定時は false(二重表紙回避・§1-6)。



1030
1031
1032
# File 'lib/vivlio_starter/cli/common.rb', line 1030

def kindle_embed?      = CONFIG&.output&.kindle&.embed == true
# 本文にフチなし(塗り足しまで届く)要素があるか。true の本は閲覧用 PDF から
# 塗り足しを復元できないため、入稿用 PDF を個別レンダリングする(既定 false = 導出)。

.load_configObject

book.yml を読み込み、ハードコーディングされた既定値をマージして返す



191
192
193
194
195
196
197
# File 'lib/vivlio_starter/cli/common.rb', line 191

def load_config
  YAML.load_file(CONFIG_FILE, aliases: true, symbolize_names: true) => raw_config
  # 既定値をマージすると未知キーが落ちるので、著者の記述はここで記録する
  @authored_keys = collect_key_paths(raw_config)
  cfg = apply_page_preset(raw_config)
  merge_hardcoded_defaults(cfg)
end

.load_page_presetsObject

safe_load_file に統一(ensure_required_yaml_files! の検証経路と同一ポリシー)。 aliases はプリセットの差分定義(<<: *a5_std)に必須のため許可する。



309
310
311
# File 'lib/vivlio_starter/cli/common.rb', line 309

def load_page_presets
  YAML.safe_load_file(PAGE_PRESETS_FILE, aliases: true, symbolize_names: true)
end

.log_action(msg) ⇒ Object

処理ステップの開始・進行(🔧)。--log=info 以上で表示。



451
452
453
# File 'lib/vivlio_starter/cli/common.rb', line 451

def log_action(msg)
  emit("🔧 #{msg}") if current_log_level >= 2
end

.log_always(msg) ⇒ Object

アイコンなしで常に表示する汎用出力。



487
488
489
# File 'lib/vivlio_starter/cli/common.rb', line 487

def log_always(msg)
  emit(msg)
end

.log_debug(msg) ⇒ Object

デバッグ情報(🧪)。--log=debug のみ表示。



456
457
458
# File 'lib/vivlio_starter/cli/common.rb', line 456

def log_debug(msg)
  emit("🧪 #{msg}") if current_log_level >= 3
end

.log_error(msg, detail: nil) ⇒ Object

エラー(🔴)。ログレベルに関わらず常に表示。



445
446
447
448
# File 'lib/vivlio_starter/cli/common.rb', line 445

def log_error(msg, detail: nil)
  emit("🔴 #{msg}")
  format_detail(detail).each { |line| emit("#{DETAIL_INDENT}#{line}") }
end

.log_info(msg) ⇒ Object

補足情報・処理の詳細(🔵)。--log=info 以上で表示。



427
428
429
# File 'lib/vivlio_starter/cli/common.rb', line 427

def log_info(msg)
  emit("🔵 #{msg}") if current_log_level >= 2
end

.log_option_value(command) ⇒ Object

ルートコマンドが受け取った実行対象(サブコマンド)の --log を読む。 --log を宣言していないコマンドでは nil になり、既定レベルのままとなる。



376
377
378
379
# File 'lib/vivlio_starter/cli/common.rb', line 376

def log_option_value(command)
  target = (command.command if command.respond_to?(:command)) || command
  target.options[:log_level] if target.respond_to?(:options)
end

.log_result(msg, status:) ⇒ Object

処理の最終結果を報告する(✅/❗/❌/📚)。ログレベルに関わらず常に表示。 :warning は「警告はあるが処理は成立した」中間の結末を表す (preflight の警告のみ=終了コード 0 のケース)。

異体字セレクタ(U+FE0F)を伴う絵文字は使わないこと。 ⚠️(U+26A0 + FE0F)は East Asian Width が N の文字記号を VS16 で絵文字化しているだけなので、 幅の扱いが端末に委ねられ半角で描かれる——桁が揃わず表が崩れる。 ここで使う 4 つはいずれも単一コードポイントで EAW=W(全角固定)。 新しいアイコンを足すときも同じ条件を満たすものを選ぶ。

Parameters:

  • status (:success, :warning, :failure, :artifact)

    アイコンの種別



476
477
478
479
480
481
482
483
484
# File 'lib/vivlio_starter/cli/common.rb', line 476

def log_result(msg, status:)
  icon = case status
        when :success  then ""
        when :warning  then ""
        when :failure  then ""
        when :artifact then "📚"
        end
  emit("#{icon} #{msg}")
end

.log_success(msg) ⇒ Object

処理の成功(✅)。--log=info 以上で表示。



432
433
434
# File 'lib/vivlio_starter/cli/common.rb', line 432

def log_success(msg)
  emit("#{msg}") if current_log_level >= 2
end

.log_summary(msg, detail: nil) ⇒ Object

検証結果の集計サマリー(🔍)。ログレベルに関わらず常に表示。



461
462
463
464
# File 'lib/vivlio_starter/cli/common.rb', line 461

def log_summary(msg, detail: nil)
  emit("🔍 #{msg}")
  format_detail(detail).each { |line| emit("#{DETAIL_INDENT}#{line}") }
end

.log_warn(msg, detail: nil) ⇒ Object

注意・警告(🟡)。--log=warn 以上(既定)で表示。



437
438
439
440
441
442
# File 'lib/vivlio_starter/cli/common.rb', line 437

def log_warn(msg, detail: nil)
  return unless current_log_level >= 1

  emit("🟡 #{msg}")
  format_detail(detail).each { |line| emit("#{DETAIL_INDENT}#{line}") }
end

.merge_hardcoded_defaults(cfg) ⇒ Object

ハードコーディングされた既定値スキーマをマージする book.yml に記述がなくても全セクション・既知キーが常に存在し、 CONFIG.lint.config のようなドット記法が安全になる(値未設定なら nil)。 仕様: config-access-unification-spec.md §2.2



203
204
205
# File 'lib/vivlio_starter/cli/common.rb', line 203

def merge_hardcoded_defaults(cfg)
  default_config_schema.merge(cfg) { |_key, default_val, user_val| deep_merge_config(default_val, user_val) }
end

.merge_vivliostyle_build_timings(entries) ⇒ Object

別スレッドで集めた計時を、このスレッドの記録へ合流させる。 計時はスレッドローカルなので競合はしないが、そのままでは親が子枝の内訳を 見られない(build-target-parallelization-spec.md §3.5)。



838
839
840
841
# File 'lib/vivlio_starter/cli/common.rb', line 838

def merge_vivliostyle_build_timings(entries)
  timings = Thread.current[VIVLIOSTYLE_TIMINGS_KEY] ||= []
  timings.concat(Array(entries))
end

.missing_book_config_example(missing) ⇒ Object

未設定キーを book.yml の記法どおり(セクションごと)に並べた記入例を組み立てる。



936
937
938
939
940
# File 'lib/vivlio_starter/cli/common.rb', line 936

def missing_book_config_example(missing)
  missing.group_by(&:first).flat_map do |section, paths|
    ["#{section}:", *paths.map { "  #{it.last}: #{REQUIRED_BOOK_KEYS[it]}" }]
  end.join("\n")
end

.missing_book_config_keys(cfg) ⇒ Array<Array<Symbol>>

主要キーのうち book.yml に書かれていないものを返す。

Parameters:

  • cfg (Hash)

    シンボルキー化された book.yml の内容

Returns:

  • (Array<Array<Symbol>>)

    未設定キーのパス(欠落なしなら空配列)



915
916
917
# File 'lib/vivlio_starter/cli/common.rb', line 915

def missing_book_config_keys(cfg)
  REQUIRED_BOOK_KEYS.keys.select { blank?(cfg.dig(*it)) }
end

.missing_external_command_message(cmd, purpose: nil) ⇒ String

外部コマンドが見つからない際の案内メッセージを生成する。 vs doctor / vs doctor --fix への誘導を含む。

Parameters:

  • cmd (String)

    不足しているコマンド名

  • purpose (String, nil) (defaults to: nil)

    用途の人間向け説明(例: 'カバー画像生成')

Returns:

  • (String)


542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/vivlio_starter/cli/common.rb', line 542

def missing_external_command_message(cmd, purpose: nil)
  header = if purpose && !purpose.to_s.strip.empty?
             "#{purpose}に必要な外部コマンドが見つかりません: #{cmd}"
           else
             "必要な外部コマンドが見つかりません: #{cmd}"
           end
  <<~MSG.strip
    #{header}
    環境診断と自動セットアップを試すには:
        vs doctor         # 不足しているツールの一覧を表示
        vs doctor --fix   # macOS なら Homebrew で自動インストールを試行
  MSG
end

.normalize_font_sizes(pcfg) ⇒ Object



326
327
328
329
330
331
# File 'lib/vivlio_starter/cli/common.rb', line 326

def normalize_font_sizes(pcfg)
  FONT_SIZE_KEYS.each_with_object({}) do |key, memo|
    normalized = Units.font_size_to_pt(pcfg[key])
    memo[key] = normalized if normalized
  end
end

.normalize_line_height(pcfg) ⇒ Object



333
334
335
336
337
338
339
340
341
342
343
# File 'lib/vivlio_starter/cli/common.rb', line 333

def normalize_line_height(pcfg)
  case [pcfg[:base_line_height]&.to_s&.strip, Units.pt_value(pcfg[:base_font_size])]
  in [nil | '', _]             then nil
  in [/pt\z/i => s, _]         then s
  in [/q\z/i => s, _]          then Units.format_pt(s.to_f * Units::PT_PER_Q)
  in [_, nil]                  then pcfg[:base_line_height]
  in [/em\z/i => s, f_pt]      then Units.format_pt(f_pt * s.to_f)
  in [/\A[\d.]+\z/ => s, f_pt] then Units.format_pt(f_pt * s.to_f)
  in [other, _]                then other
  end
end

.normalize_page_size!(page_cfg) ⇒ Object



749
750
751
752
753
754
755
756
# File 'lib/vivlio_starter/cli/common.rb', line 749

def normalize_page_size!(page_cfg)
  return page_cfg unless page_cfg.is_a?(Hash)

  w, h = resolve_page_size(page_cfg)
  page_cfg[:width] = w
  page_cfg[:height] = h
  page_cfg
end

.normalize_page_units(pcfg) ⇒ Object

page 設定の単位を正規化する(仕様: page-unit-conversion-spec.md §3.3)。 文字サイズを先に pt 化し、その結果を基準に行送り(倍率/em)を絶対 pt へ解決する。 行送りを倍率のまま CSS へ渡さないのは、参照箇所ごとの font-size に依存させず 版面の行グリッドを揃えるため(同 §1.3)。



321
322
323
324
# File 'lib/vivlio_starter/cli/common.rb', line 321

def normalize_page_units(pcfg)
  sized = pcfg.merge(**normalize_font_sizes(pcfg))
  sized.merge(base_line_height: normalize_line_height(sized)).compact
end

.pdf_combined?Object



1026
# File 'lib/vivlio_starter/cli/common.rb', line 1026

def pdf_combined?      = CONFIG&.output&.pdf&.combined == true

.pdf_compress?Object



1027
# File 'lib/vivlio_starter/cli/common.rb', line 1027

def pdf_compress?      = CONFIG&.output&.pdf&.compress == true

.postface_template_pathObject



996
# File 'lib/vivlio_starter/cli/common.rb', line 996

def postface_template_path = template_path('postface')

.preface_template_pathObject



994
# File 'lib/vivlio_starter/cli/common.rb', line 994

def preface_template_path = template_path('preface')

本文にフチなし(塗り足しまで届く)要素があるか。true の本は閲覧用 PDF から 塗り足しを復元できないため、入稿用 PDF を個別レンダリングする(既定 false = 導出)。



1033
# File 'lib/vivlio_starter/cli/common.rb', line 1033

def print_pdf_full_bleed? = truthy?(CONFIG&.output&.print_pdf&.full_bleed)

.record_vivliostyle_build(duration, label = nil) ⇒ Object



823
824
825
826
827
# File 'lib/vivlio_starter/cli/common.rb', line 823

def record_vivliostyle_build(duration, label = nil)
  timings = Thread.current[VIVLIOSTYLE_TIMINGS_KEY] ||= []
  label_text = label.to_s.empty? ? 'Vivliostyle build' : label.to_s
  timings << { duration: duration.to_f, label: label_text }
end

.relative_path_from_root(path) ⇒ Object



660
661
662
663
664
665
666
# File 'lib/vivlio_starter/cli/common.rb', line 660

def relative_path_from_root(path)
  return path if blank?(path)

  Pathname.new(path).relative_path_from(Pathname.new(Dir.pwd)).to_s
rescue StandardError
  path.to_s
end

.reload_configuration!(silent: false) ⇒ Object

定数を安全に(警告なしで)再定義する

Parameters:

  • silent (Boolean) (defaults to: false)

    初期ロード時はログ出力を抑制



863
864
865
866
867
868
869
870
871
872
873
874
875
876
# File 'lib/vivlio_starter/cli/common.rb', line 863

def reload_configuration!(silent: false)
  ensure_required_yaml_files!

  # load_configの結果をDataオブジェクトにラップしてフリーズ
  raw_config = load_config
  # 検査は毎回するが、案内は ensure_configured! まで持ち越す。
  # ここで出すと module load 時(= まだログ level も決まっていない時点)に流れてしまい、
  # かつ CONFIG を読まない new / doctor / help にも無関係な警告が付く。
  @missing_book_keys = missing_book_config_keys(raw_config)
  @missing_book_keys_reported = false
  install_configuration!(wrap_config(raw_config).freeze)

  puts("🧪 Configuration reloaded: #{CONFIG_FILE}") if !silent && current_log_level >= 3
end

.required_yaml_files_loadable?Object

必須 YAML がすべて存在し、かつ解析可能かを abort せずに判定する。 モジュール初期ロードで使用(破損時に起動ごと止めてしまうと、修復手段で ある vs doctor --fix 自体が実行できなくなるため)



181
182
183
184
185
186
187
188
# File 'lib/vivlio_starter/cli/common.rb', line 181

def required_yaml_files_loadable?
  REQUIRED_YAML_FILES.all? do |path|
    File.file?(path) &&
      (YAML.safe_load(File.read(path, encoding: 'utf-8'), aliases: true, symbolize_names: true) in Hash | Array)
  rescue StandardError
    false
  end
end

.reset_vivliostyle_build_timingsObject



819
820
821
# File 'lib/vivlio_starter/cli/common.rb', line 819

def reset_vivliostyle_build_timings
  Thread.current[VIVLIOSTYLE_TIMINGS_KEY] = []
end

.resolve_log_level(raw) ⇒ Object

値なし --log は OptionTokenNormalizer が info へ開いて渡すため、ここには 常に具体的な値か nil だけが来る。大文字は記法を問わず downcase して扱う (従来は --log=DEBUG だけが downcase され --log DEBUG は info に落ちていた)。



384
385
386
387
388
# File 'lib/vivlio_starter/cli/common.rb', line 384

def resolve_log_level(raw)
  return DEFAULT_LOG_LEVEL if raw.nil?

  LEVELS[raw.to_s.downcase] || warn_unknown_log_level(raw)
end

.resolve_page_size(pcfg) ⇒ Object

ページサイズを解決する(シンボルキーの Hash 前提) CONFIG.page(Data)を渡す場合は呼び出し側の境界で .to_h してから渡す(spec §2.4)



736
737
738
739
740
741
742
743
744
745
746
747
# File 'lib/vivlio_starter/cli/common.rb', line 736

def resolve_page_size(pcfg)
  size = pcfg[:size].to_s.strip.upcase
  defaults = PAGE_SIZES[size] || PAGE_SIZES['B5']

  width  = pcfg[:width]&.to_s&.strip
  height = pcfg[:height]&.to_s&.strip

  [
    width.to_s.empty? ? defaults[:width] : width,
    height.to_s.empty? ? defaults[:height] : height
  ]
end

.resolve_path_from_root(path) ⇒ Object

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

Path Utilities



650
651
652
653
654
655
656
657
658
# File 'lib/vivlio_starter/cli/common.rb', line 650

def resolve_path_from_root(path)
  return nil if blank?(path)

  pn = Pathname.new(path)
  pn = Pathname.new(Dir.pwd).join(pn) unless pn.absolute?
  pn.cleanpath.to_s
rescue StandardError
  path
end

.run_svg_converter!(argv, input_path:, output_path: nil, purpose: nil, env: nil) ⇒ Boolean

外部 SVG 変換コマンド(rsvg-convert / ImageMagick 等)を実行し、 失敗した場合はユーザー向けの整形済みエラーメッセージを出力する。

堅牢性仕様 7-1: 不正な SVG XML 等で外部コマンドが失敗した際に、 従来はサイレントに下流で No such file となっていた問題を解消する。

Parameters:

  • argv (Array<String>)

    Kernel#system 相当のコマンド配列

  • input_path (String)

    入力 SVG パス(エラーメッセージ表示用)

  • output_path (String, nil) (defaults to: nil)

    期待する出力ファイルのパス (nil 以外の場合、exit 成功でもファイル未生成なら失敗扱い)

  • purpose (String, nil) (defaults to: nil)

    用途の人間向け説明(例: 'カバー PDF 変換')

  • env (Hash, nil) (defaults to: nil)

    追加の環境変数(例: FONTCONFIG_FILE)

Returns:

  • (Boolean)

    成功なら true、失敗なら false



579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/vivlio_starter/cli/common.rb', line 579

def run_svg_converter!(argv, input_path:, output_path: nil, purpose: nil, env: nil)
  require 'open3'

  capture_args = env&.any? ? [env, *argv] : argv
  _stdout, stderr, status = Open3.capture3(*capture_args)
  exit_ok   = status.success?
  file_ok   = output_path.nil? || File.exist?(output_path)
  return true if exit_ok && file_ok

  command_name = argv.first
  purpose_hint = purpose && !purpose.to_s.strip.empty? ? "#{purpose}" : ''
  reason       = if !exit_ok
                   "終了コード: #{status.exitstatus || 'unknown'}"
                 else
                   '出力ファイルが生成されませんでした'
                 end
  stderr_digest = format_converter_stderr(stderr)
  log_error(<<~MSG.strip)
    SVG 変換に失敗しました#{purpose_hint}: #{input_path}
      実行コマンド: #{command_name}
      #{reason}
      #{stderr_digest}
  MSG
  false
rescue Errno::ENOENT => e
  log_error("SVG 変換コマンドが見つかりません: #{argv.first} (#{e.message})")
  false
rescue StandardError => e
  log_error("SVG 変換中に予期せぬ例外が発生しました: #{e.class}: #{e.message} (input=#{input_path})")
  false
end

.stylesheets_dirObject



983
# File 'lib/vivlio_starter/cli/common.rb', line 983

def stylesheets_dir    = STYLESHEETS_DIR

.template_path(name) ⇒ Object



989
990
991
# File 'lib/vivlio_starter/cli/common.rb', line 989

def template_path(name)
  File.join(templates_dir, "#{name}.md")
end

.templates_dirObject



986
# File 'lib/vivlio_starter/cli/common.rb', line 986

def templates_dir      = TEMPLATES_DIR

.theme_images_cache_dirObject



1005
# File 'lib/vivlio_starter/cli/common.rb', line 1005

def theme_images_cache_dir = File.join(cache_dir, 'theme-images')

.to_roman_lower(n) ⇒ Object

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

Chapter Utilities



678
679
680
681
682
683
684
685
686
687
688
689
690
691
# File 'lib/vivlio_starter/cli/common.rb', line 678

def to_roman_lower(n)
  return '' if n.to_i <= 0

  n = n.to_i
  mapping = [
    [1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'],
    [100, 'c'], [90, 'xc'], [50, 'l'], [40, 'xl'],
    [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i']
  ]
  mapping.each_with_object(String.new) do |(val, sym), res|
    count, n = n.divmod(val)
    res << (sym * count)
  end
end

.truthy?(val) ⇒ Object

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

Helpers



637
638
639
640
641
642
# File 'lib/vivlio_starter/cli/common.rb', line 637

def truthy?(val)
  case val&.to_s&.strip&.downcase
  in true | 'true' | 'yes' | 'on' | '1' then true
  else false
  end
end

.validate_cover_settingsObject

カバー設定のバリデーション。 output.cover は既定値 master を持つため「未設定」の状態は存在しない (従来も cover.rb が || 'master' で補っており、実際の出力は同じだった)。 画像が足りない場合は下の存在チェックが具体的に指摘する。



1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
# File 'lib/vivlio_starter/cli/common.rb', line 1039

def validate_cover_settings
  theme = cover_theme

  # 標準テーマの場合は有効
  return true if %w[light dark].include?(theme)

  # masterテーマは特別扱い(既存のmaster.pngファイルを使用)
  if theme == 'master'
    front_path = File.join(covers_dir, "frontcover_#{theme}.png")
    back_path  = File.join(covers_dir, "backcover_#{theme}.png")

    unless File.exist?(front_path) && File.exist?(back_path)
      log_error("マスター画像 '#{theme}' のPNGファイルが見つかりません")
      return false
    end
    return true
  end

  # カスタムテーマの場合は命名規則をチェック
  unless theme.match?(/\A[a-z0-9_]+\z/)
    log_error("テーマ名 '#{theme}' は無効な形式です")
    return false
  end

  # カスタムテーマの場合はPNGファイルの存在を確認
  front_path = File.join(covers_dir, "frontcover_#{theme}.png")
  back_path  = File.join(covers_dir, "backcover_#{theme}.png")

  unless File.exist?(front_path) && File.exist?(back_path)
    log_error("カスタム画像 '#{theme}' のPNGファイルが見つかりません")
    return false
  end

  true
end

.verbose?Object



629
630
631
# File 'lib/vivlio_starter/cli/common.rb', line 629

def verbose?
  current_log_level >= 2
end

.vfm_commandObject

コマンド関連



1021
# File 'lib/vivlio_starter/cli/common.rb', line 1021

def vfm_command        = VFM_COMMAND

.warn_missing_book_configObject

主要キーの欠落を、廃止キー案内と同じ関門(ensure_configured!)で 1 回だけ案内する。 「タイトルが空欄の PDF ができてから気付く」のを避けるのが目的。 既存の最小構成プロジェクトを弾かないよう abort はしない。



922
923
924
925
926
927
928
929
930
931
932
933
# File 'lib/vivlio_starter/cli/common.rb', line 922

def warn_missing_book_config
  return if @missing_book_keys_reported

  @missing_book_keys_reported = true
  missing = Array(@missing_book_keys)
  return if missing.empty?

  log_warn("config/book.yml の推奨キーが未設定です: #{missing.map { it.join('.') }.join(', ')}",
           detail: "→ config/book.yml に次のように書いてください。\n" \
                   "#{missing_book_config_example(missing)}\n" \
                   'このままでも動作しますが、PDF のタイトル・著者・出力ファイル名が空欄になります。')
end

.warn_reserved_config_keys(keys) ⇒ Object

book.yml のキーが Data の予約メソッド名と衝突していないかを検査する。 衝突すると member 定義がメソッドを上書き(またはその逆)して静かに誤動作するため、 ロード時に著者へ改名を促す。



152
153
154
155
156
157
# File 'lib/vivlio_starter/cli/common.rb', line 152

def warn_reserved_config_keys(keys)
  reserved = keys.map { it.respond_to?(:to_sym) ? it.to_sym : it } & RESERVED_CONFIG_KEYS
  return if reserved.empty?

  log_warn("book.yml のキー名 #{reserved.join(', ')} は予約名のため正しく参照できません。別名への変更を推奨します(例: hash → hash_value)")
end

.warn_retired_config_keysObject

廃止キーが書かれていたら、まとめて 1 回だけ案内する。 黙って無視すると「設定したのに効かない」という最悪の形になる。



252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/vivlio_starter/cli/common.rb', line 252

def warn_retired_config_keys
  return if @retired_keys_reported

  @retired_keys_reported = true
  found = RETIRED_CONFIG_KEYS.select { |path, _| authored_key?(*path) }
  return if found.empty?

  found.each do |path, guidance|
    log_warn("config/book.yml の #{path.join('.')} は廃止されました", detail: guidance)
  end
  log_warn('上記のキーは読み込まれません。book.yml から削除してください',
           detail: '指定できるキーは vs doctor か各コマンドの --help で確認できます')
end

.warn_unknown_log_level(raw) ⇒ Object

不明なログレベルは 🟡 で知らせて info へ倒す。ログ指定のタイプミスでビルドを 止めるのは過剰だが、黙って既定に落とすと間違いに気づけない。



392
393
394
395
396
397
# File 'lib/vivlio_starter/cli/common.rb', line 392

def warn_unknown_log_level(raw)
  log_warn("--log=#{raw} は不明なログレベルです。",
           detail: "対処: #{LOG_LEVEL_NAMES.join(' / ')} のいずれかを指定してください" \
                   '(今回は info で続行します)。')
  LEVELS['info']
end

.with_current_step_label(label) ⇒ Object



843
844
845
846
847
848
849
# File 'lib/vivlio_starter/cli/common.rb', line 843

def with_current_step_label(label)
  previous = Thread.current[VIVLIOSTYLE_CURRENT_STEP_KEY]
  Thread.current[VIVLIOSTYLE_CURRENT_STEP_KEY] = label.to_s
  yield
ensure
  Thread.current[VIVLIOSTYLE_CURRENT_STEP_KEY] = previous
end

.with_emit_sink(sink) ⇒ Object

ブロックの間、このスレッドのログ行を sink(<< を受ける任意のオブジェクト)へ 溜める。溜めた行は呼び出し側が持つので、ブロックが例外で抜けても失われない。



418
419
420
421
422
423
424
# File 'lib/vivlio_starter/cli/common.rb', line 418

def with_emit_sink(sink)
  previous = Thread.current[EMIT_SINK_KEY]
  Thread.current[EMIT_SINK_KEY] = sink
  yield
ensure
  Thread.current[EMIT_SINK_KEY] = previous
end

.wrap_config(input) ⇒ Object

Hashを再帰的にDataオブジェクトに変換するヘルパー ドット記法と [] アクセスの両方を提供します 正規記法は config-access-unification-spec.md §2 を参照



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/vivlio_starter/cli/common.rb', line 112

def wrap_config(input)
  case input
  in Hash
    # キーを動的にDataの属性として定義
    keys = input.keys
    warn_reserved_config_keys(keys)
    cls = Data.define(*keys) do
      # 動的キー用の [] アクセス(Symbol 限定・member 限定)。
      # respond_to? ベースだと to_h 等のメソッド戻り値が漏れるため member 限定とし、
      # String キーは記法混在の再発を防ぐため即座にエラーにする(The One Way)。
      def [](key)
        raise ArgumentError, "CONFIG のキーは Symbol で指定してください(String は不可): #{key.inspect}" if key.is_a?(String)

        members.include?(key) ? public_send(key) : nil
      end

      # パターンマッチング(deconstruct_keys)への対応。
      # keys が nil のとき全体を返すのは Ruby の規約(`in { **rest }` で全キーを束縛可能にする)
      def deconstruct_keys(keys) = keys.nil? ? to_h : to_h.slice(*keys)

      # 動的な多段アクセス用の dig(Symbol キーのみ。配列添字の Integer は可)
      def dig(*keys)
        keys.reduce(self) do |obj, key|
          return nil unless obj.respond_to?(:[])

          obj[key]
        end
      end
    end
    cls.new(**input.transform_values { wrap_config(it) })
  in Array
    input.map { wrap_config(it) }
  else
    input
  end
end