Module: Vivlio::Starter::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 config/post_replace_list.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
LEVELS =
{ 'error' => 0, 'warn' => 1, 'info' => 2, 'success' => 2, 'action' => 2, 'debug' => 3 }.freeze
CONFIG_DIR =
'config'
CONTENTS_DIR =
'contents'
STYLESHEETS_DIR =
'stylesheets'
IMAGES_DIR =
'images'
CODES_DIR =
'codes'
TEMPLATES_DIR =
'templates'
COVERS_DIR =
'covers'
VFM_COMMAND =
'vfm'
POST_REPLACE_FILE =
'post_replace_list.yml'
CACHE_DIR =
'.cache/vs'
VIVLIOSTYLE_CONFIG_FILE =
'vivliostyle.config.js'
DETAIL_INDENT =

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

'        '
PAGE_SIZES =

Page Size Utilities

{
  'A4' => { width: '210mm', height: '297mm' },
  'A5' => { width: '148mm', height: '210mm' },
  '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

Class Method Summary collapse

Class Method Details

.abort_with_error(msg) ⇒ Object



630
631
632
633
634
# File 'lib/vivlio/starter/cli/common.rb', line 630

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 配列(単章ビルド時に渡す)



487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/vivlio/starter/cli/common.rb', line 487

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

  # ビルド対象のエントリが渡された場合はその中の付録の順番を使う
  appendix_entries = if entries
                       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



709
# File 'lib/vivlio/starter/cli/common.rb', line 709

def appendix_template_path = template_path('appendix')

.apply_page_preset(cfg) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/vivlio/starter/cli/common.rb', line 160

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
      overrides = page_cfg.reject { PAGE_PRESET_EXCLUDE_KEYS.include?(it) }
      merged = selected.merge(overrides).merge(page_cfg)
      cfg.merge(page: normalize_page_units(merged))
    else
      cfg
    end
  else
    cfg
  end
end

.blank?(v) ⇒ Object



433
# File 'lib/vivlio/starter/cli/common.rb', line 433

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

.cache_cfgObject

キャッシュ関連



713
# File 'lib/vivlio/starter/cli/common.rb', line 713

def cache_cfg          = CONFIG&.cache

.cache_dirObject



714
# File 'lib/vivlio/starter/cli/common.rb', line 714

def cache_dir          = CONFIG&.cache&.dir || CACHE_DIR

.cache_enabled?Object



715
# File 'lib/vivlio/starter/cli/common.rb', line 715

def cache_enabled?     = CONFIG&.cache&.enabled != false

.chapter_template_pathObject



707
# File 'lib/vivlio/starter/cli/common.rb', line 707

def chapter_template_path = template_path('chapter')

.codes_dirObject



699
# File 'lib/vivlio/starter/cli/common.rb', line 699

def codes_dir          = CONFIG&.directories&.codes || CODES_DIR

.config_dirObject

ディレクトリ関連



694
# File 'lib/vivlio/starter/cli/common.rb', line 694

def config_dir         = CONFIG&.directories&.config || CONFIG_DIR

.config_dir_pathObject



695
# File 'lib/vivlio/starter/cli/common.rb', line 695

def config_dir_path    = resolve_path_from_root(config_dir)

.configured?Object

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



681
# File 'lib/vivlio/starter/cli/common.rb', line 681

def configured? = !CONFIG.nil?

.consume_vivliostyle_build_timingsObject



593
594
595
596
597
# File 'lib/vivlio/starter/cli/common.rb', line 593

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

.contents_dirObject



696
# File 'lib/vivlio/starter/cli/common.rb', line 696

def contents_dir       = CONFIG&.directories&.contents || CONTENTS_DIR

.cover_themeObject

カバー設定関連



736
# File 'lib/vivlio/starter/cli/common.rb', line 736

def cover_theme        = CONFIG.dig('output', 'cover')

.covers_dirObject



701
# File 'lib/vivlio/starter/cli/common.rb', line 701

def covers_dir         = CONFIG&.directories&.covers || COVERS_DIR

.current_log_levelObject

–log オプションから現在のログレベルを解決する。error: 0 / warn: 1 / info,success,action: 2 / debug: 3



229
230
231
232
233
234
235
236
# File 'lib/vivlio/starter/cli/common.rb', line 229

def current_log_level
  case ARGV
  in [*, /^--log=(.+)$/, *] then LEVELS[::Regexp.last_match(1).downcase] || 2
  in [*, '--log', level, *] if LEVELS.key?(level) then LEVELS[level]
  in [*, '--log', *] then 2
  else 1
  end
end

.current_step_labelObject



607
608
609
# File 'lib/vivlio/starter/cli/common.rb', line 607

def current_step_label
  Thread.current[VIVLIOSTYLE_CURRENT_STEP_KEY]
end

.default_cacheObject



139
# File 'lib/vivlio/starter/cli/common.rb', line 139

def default_cache = { dir: CACHE_DIR, enabled: true }

.default_commandsObject



140
# File 'lib/vivlio/starter/cli/common.rb', line 140

def default_commands = { vfm: VFM_COMMAND }

.default_directoriesObject

— Hardcoded Defaults (Data objects for immutability) —



127
128
129
130
131
132
133
134
135
136
137
# File 'lib/vivlio/starter/cli/common.rb', line 127

def default_directories
  {
    config: CONFIG_DIR,
    contents: CONTENTS_DIR,
    stylesheets: STYLESHEETS_DIR,
    images: IMAGES_DIR,
    codes: CODES_DIR,
    templates: TEMPLATES_DIR,
    covers: COVERS_DIR
  }
end

.default_filesObject



141
# File 'lib/vivlio/starter/cli/common.rb', line 141

def default_files = { post_replace: POST_REPLACE_FILE }

.default_vfmObject

VFM (Vivliostyle Flavored Markdown) の既定値設定日本語文章の直感的な執筆体験を提供するため、hardLineBreaks をデフォルト有効化



154
155
156
157
158
# File 'lib/vivlio/starter/cli/common.rb', line 154

def default_vfm
  {
    hardLineBreaks: true
  }
end

.default_vivliostyleObject



143
144
145
146
147
148
149
150
# File 'lib/vivlio/starter/cli/common.rb', line 143

def default_vivliostyle
  {
    quiet: true,
    reading_progression: 'ltr',
    entries_file: 'entries.js',
    config_file: VIVLIOSTYLE_CONFIG_FILE
  }
end

.ensure_cache_dir!Object



457
458
459
460
461
# File 'lib/vivlio/starter/cli/common.rb', line 457

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

.ensure_configured!Object



683
684
685
686
687
# File 'lib/vivlio/starter/cli/common.rb', line 683

def ensure_configured!
  return if configured?

  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)

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



351
352
353
354
355
# File 'lib/vivlio/starter/cli/common.rb', line 351

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



91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/vivlio/starter/cli/common.rb', line 91

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}")
    end
  rescue StandardError => e
    abort_with_error("必須設定ファイルの解析に失敗しました (#{path}): #{e.message}")
  end
end

.epub_embed?Object



739
# File 'lib/vivlio/starter/cli/common.rb', line 739

def epub_embed?        = CONFIG.dig('output', 'epub', 'embed') == true

.external_command_available?(cmd) ⇒ Boolean


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


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

Parameters:

  • cmd (String)

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

Returns:

  • (Boolean)


314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/vivlio/starter/cli/common.rb', line 314

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

.fetch_bool(obj, keys, default: false) ⇒ Object

シンボルキーのみを前提としたブール値取得



616
617
618
619
620
621
622
623
624
625
626
627
628
# File 'lib/vivlio/starter/cli/common.rb', line 616

def fetch_bool(obj, keys, default: false)
  cur = obj
  Array(keys).each do |k|
    return default unless cur.respond_to?(:[])

    cur = cur[k.to_sym]
  end
  return default if cur.nil?

  truthy?(cur)
rescue StandardError
  default
end

.format_converter_stderr(text) ⇒ Object

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



402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/vivlio/starter/cli/common.rb', line 402

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_* からのみ呼ばれる内部ヘルパー。



301
302
303
304
305
# File 'lib/vivlio/starter/cli/common.rb', line 301

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

  detail.lines.map(&:chomp)
end

.format_pt(value) ⇒ Object



218
# File 'lib/vivlio/starter/cli/common.rb', line 218

def format_pt(value) = "#{value.to_f.round(3)}pt"

.generate_compressed_pdf_filename(target = 'pdf') ⇒ Object



570
571
572
573
574
# File 'lib/vivlio/starter/cli/common.rb', line 570

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

.generate_epub_filenameObject



568
# File 'lib/vivlio/starter/cli/common.rb', line 568

def generate_epub_filename = generate_output_filename('epub')

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

Output Filename Generation



546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/vivlio/starter/cli/common.rb', line 546

def generate_output_filename(target = 'pdf', suffix: nil)
  project = CONFIG[:project]
  project_name = project&.name || 'vivlio_starter'
  project_version = project&.version
  include_version = CONFIG.dig(:output, :filename, :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'
        else '.pdf'
        end
  filename + ext
end

.generate_print_pdf_filenameObject



567
# File 'lib/vivlio/starter/cli/common.rb', line 567

def generate_print_pdf_filename = generate_output_filename('print_pdf')

.images_dirObject



698
# File 'lib/vivlio/starter/cli/common.rb', line 698

def images_dir         = CONFIG&.directories&.images || IMAGES_DIR

.load_configObject

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



107
108
109
110
111
# File 'lib/vivlio/starter/cli/common.rb', line 107

def load_config
  YAML.load_file(CONFIG_FILE, aliases: true, symbolize_names: true) => raw_config
  cfg = apply_page_preset(raw_config)
  merge_hardcoded_defaults(cfg)
end

.load_page_presetsObject



180
181
182
# File 'lib/vivlio/starter/cli/common.rb', line 180

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

.log_action(msg) ⇒ Object

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



263
264
265
# File 'lib/vivlio/starter/cli/common.rb', line 263

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

.log_always(msg) ⇒ Object

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



295
296
297
# File 'lib/vivlio/starter/cli/common.rb', line 295

def log_always(msg)
  puts(msg)
end

.log_debug(msg) ⇒ Object

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



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

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

.log_error(msg, detail: nil) ⇒ Object

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



257
258
259
260
# File 'lib/vivlio/starter/cli/common.rb', line 257

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

.log_info(msg) ⇒ Object

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



239
240
241
# File 'lib/vivlio/starter/cli/common.rb', line 239

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

.log_inspection(msg) ⇒ Object

詳細診断情報(🔍)。–log=info 以上で表示。



279
280
281
# File 'lib/vivlio/starter/cli/common.rb', line 279

def log_inspection(msg)
  puts "🔍 #{msg}" if current_log_level >= 2
end

.log_result(msg, status:) ⇒ Object

処理の最終結果を報告する(✅/❌/📚)。ログレベルに関わらず常に表示。

Parameters:

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

    アイコンの種別



285
286
287
288
289
290
291
292
# File 'lib/vivlio/starter/cli/common.rb', line 285

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

.log_success(msg) ⇒ Object

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



244
245
246
# File 'lib/vivlio/starter/cli/common.rb', line 244

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

.log_summary(msg, detail: nil) ⇒ Object

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



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

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

.log_warn(msg, detail: nil) ⇒ Object

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



249
250
251
252
253
254
# File 'lib/vivlio/starter/cli/common.rb', line 249

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

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

.merge_hardcoded_defaults(cfg) ⇒ Object

ハードコーディングされた既定値をマージするbook.yml に記述がなくても、これらの値は常に利用可能



115
116
117
118
119
120
121
122
123
124
# File 'lib/vivlio/starter/cli/common.rb', line 115

def merge_hardcoded_defaults(cfg)
  cfg.merge(
    directories: default_directories.merge(cfg[:directories] || {}),
    cache: default_cache.merge(cfg[:cache] || {}),
    commands: default_commands.merge(cfg[:commands] || {}),
    files: default_files.merge(cfg[:files] || {}),
    vivliostyle: default_vivliostyle.merge(cfg[:vivliostyle] || {}),
    vfm: default_vfm.merge(cfg[:vfm] || {})
  )
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)


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

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



195
196
197
198
199
200
201
202
# File 'lib/vivlio/starter/cli/common.rb', line 195

def normalize_font_sizes(pcfg)
  FONT_SIZE_KEYS.each_with_object({}) do |key, memo|
    case pcfg[key]&.to_s&.strip
    in /q\z/i => s then memo[key] = q_to_pt(s)
    else # Skip
    end
  end
end

.normalize_line_height(pcfg) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/vivlio/starter/cli/common.rb', line 204

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

.normalize_page_size!(page_cfg) ⇒ Object



533
534
535
536
537
538
539
540
# File 'lib/vivlio/starter/cli/common.rb', line 533

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

Normalization (Unit conversion)



188
189
190
191
192
193
# File 'lib/vivlio/starter/cli/common.rb', line 188

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

.pdf_combined?Object



737
# File 'lib/vivlio/starter/cli/common.rb', line 737

def pdf_combined?      = CONFIG.dig('output', 'pdf', 'combined') == true

.pdf_compress?Object



738
# File 'lib/vivlio/starter/cli/common.rb', line 738

def pdf_compress?      = CONFIG.dig('output', 'pdf', 'compress') == true

.post_replace_fileObject

ファイル関連



721
# File 'lib/vivlio/starter/cli/common.rb', line 721

def post_replace_file  = CONFIG&.files&.post_replace || POST_REPLACE_FILE

.post_replace_file_pathObject



723
724
725
726
727
728
729
730
731
732
733
# File 'lib/vivlio/starter/cli/common.rb', line 723

def post_replace_file_path
  file = post_replace_file
  return nil if blank?(file)

  pn = Pathname.new(file)
  base = Pathname.new(config_dir)
  pn = base.join(pn) unless pn.absolute?
  pn.cleanpath.to_s
rescue StandardError
  resolve_path_from_root(file)
end

.postface_template_pathObject



710
# File 'lib/vivlio/starter/cli/common.rb', line 710

def postface_template_path = template_path('postface')

.preface_template_pathObject



708
# File 'lib/vivlio/starter/cli/common.rb', line 708

def preface_template_path = template_path('preface')

.pt_value(value) ⇒ Object



217
# File 'lib/vivlio/starter/cli/common.rb', line 217

def pt_value(value) = value&.to_s&.match(/\A([\d.]+)pt\z/i)&.[](1)&.to_f

.q_to_pt(value) ⇒ Object



216
# File 'lib/vivlio/starter/cli/common.rb', line 216

def q_to_pt(value) = format_pt(value.to_f * 0.709)

.record_vivliostyle_build(duration, label = nil) ⇒ Object



587
588
589
590
591
# File 'lib/vivlio/starter/cli/common.rb', line 587

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



449
450
451
452
453
454
455
# File 'lib/vivlio/starter/cli/common.rb', line 449

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)

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



638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'lib/vivlio/starter/cli/common.rb', line 638

def reload_configuration!(silent: false)
  ensure_required_yaml_files!

  # load_configの結果をDataオブジェクトにラップしてフリーズ
  raw_config = load_config
  validate_book_config!(raw_config) unless silent
  new_config = wrap_config(raw_config).freeze

  # 定数の再定義(既存なら削除して警告を回避)
  remove_const(:CONFIG) if const_defined?(:CONFIG)
  const_set(:CONFIG, new_config)

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

.reset_vivliostyle_build_timingsObject



583
584
585
# File 'lib/vivlio/starter/cli/common.rb', line 583

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

.resolve_page_size(page_cfg) ⇒ Object

ページサイズを解決する(シンボルキー前提)



519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/vivlio/starter/cli/common.rb', line 519

def resolve_page_size(page_cfg)
  pcfg = page_cfg.is_a?(Hash) ? page_cfg : {}
  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



439
440
441
442
443
444
445
446
447
# File 'lib/vivlio/starter/cli/common.rb', line 439

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) ⇒ 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 変換’)

Returns:

  • (Boolean)

    成功なら true、失敗なら false



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
# File 'lib/vivlio/starter/cli/common.rb', line 369

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

  _stdout, stderr, status = Open3.capture3(*argv)
  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



697
# File 'lib/vivlio/starter/cli/common.rb', line 697

def stylesheets_dir    = CONFIG&.directories&.stylesheets || STYLESHEETS_DIR

.template_path(name) ⇒ Object



703
704
705
# File 'lib/vivlio/starter/cli/common.rb', line 703

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

.templates_dirObject



700
# File 'lib/vivlio/starter/cli/common.rb', line 700

def templates_dir      = CONFIG&.directories&.templates || TEMPLATES_DIR

.to_roman_lower(n) ⇒ Object

Chapter Utilities



467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/vivlio/starter/cli/common.rb', line 467

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



426
427
428
429
430
431
# File 'lib/vivlio/starter/cli/common.rb', line 426

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

.validate_book_config!(cfg) ⇒ Object

book.yml の主要キー(book.main_title, book.author, project.name)が欠落していないかを検査し、欠落があれば警告を出す。既存の最小構成プロジェクトとの互換性を保つため abort はせず、PDF 生成時にタイトルが空になる等の問題にユーザーが早期に気付けるようにする。

Parameters:

  • cfg (Hash)

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



658
659
660
661
662
663
664
665
666
667
668
# File 'lib/vivlio/starter/cli/common.rb', line 658

def validate_book_config!(cfg)
  missing = []
  missing << 'book.main_title' if blank?(cfg.dig(:book, :main_title))
  missing << 'book.author'     if blank?(cfg.dig(:book, :author))
  missing << 'project.name'    if blank?(cfg.dig(:project, :name))
  return if missing.empty?

  warn "[book.yml] 警告: 以下の推奨キーが未設定です: #{missing.join(', ')}"
  warn "  config/book.yml を編集して値を設定してください。未設定のままでも動作しますが、"
  warn '  PDF のタイトル・著者・出力ファイル名が空欄になります。'
end

.validate_cover_settingsObject

カバー設定のバリデーション



742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# File 'lib/vivlio/starter/cli/common.rb', line 742

def validate_cover_settings
  theme = cover_theme
  unless theme
    log_error('output.cover 設定が見つかりません')
    return false
  end

  # 標準テーマの場合は有効
  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



418
419
420
# File 'lib/vivlio/starter/cli/common.rb', line 418

def verbose?
  current_log_level >= 2
end

.vfm_commandObject

コマンド関連



718
# File 'lib/vivlio/starter/cli/common.rb', line 718

def vfm_command        = CONFIG&.commands&.vfm || VFM_COMMAND

.with_current_step_label(label) ⇒ Object



599
600
601
602
603
604
605
# File 'lib/vivlio/starter/cli/common.rb', line 599

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

.wrap_config(input) ⇒ Object

Hashを再帰的にDataオブジェクトに変換するヘルパードット記法と [] アクセスの両方を提供します



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
78
79
80
81
82
83
84
85
# File 'lib/vivlio/starter/cli/common.rb', line 53

def wrap_config(input)
  case input
  in Hash
    # キーを動的にDataの属性として定義
    keys = input.keys
    cls = Data.define(*keys) do
      # 従来型の [] アクセスも提供
      def [](key) = respond_to?(key) ? public_send(key) : nil
      # パターンマッチング(deconstruct_keys)への対応
      def deconstruct_keys(keys) = to_h.slice(*keys)

      # dig メソッドの提供(既存コードとの互換性)
      def dig(*keys)
        keys.reduce(self) do |obj, key|
          return nil unless obj.respond_to?(:[])

          obj[key]
        end
      end

      # fetch メソッドの提供
      def fetch(key, default = nil)
        val = self[key]
        val.nil? ? default : val
      end
    end
    cls.new(**input.transform_values { wrap_config(it) })
  in Array
    input.map { wrap_config(it) }
  else
    input
  end
end