Module: Vivlio::Starter::CLI::Build::CatalogUpdater

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

Overview

catalog.yml の自動更新モジュールテキストベースで行単位に編集し、コメント行(# - 02-history 等)を完全に保持する

Constant Summary collapse

CATALOG_FILE =
CatalogLoader::CATALOG_FILE

Class Method Summary collapse

Class Method Details

.active_entry_exists?(lines, basename) ⇒ Boolean

アクティブエントリとして存在するかチェック

Returns:

  • (Boolean)


143
144
145
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 143

def active_entry_exists?(lines, basename)
  !!find_active_entry_line(lines, basename)
end

.add_chapter(basename) ⇒ Object

章を catalog.yml に追加

Parameters:

  • basename (String)

    追加する章の basename(拡張子なし)



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 45

def add_chapter(basename)
  unless File.exist?(CATALOG_FILE)
    create_initial_catalog_with(basename)
    Common.log_info("catalog.yml を作成し #{basename} を追加しました")
    return
  end

  lines = read_catalog_lines
  section = section_for_basename(basename)

  # 既にアクティブ(非コメント)として存在する場合はスキップ
  return if active_entry_exists?(lines, basename)

  insert_idx, indent = find_text_insertion_point(lines, section, basename)
  lines.insert(insert_idx, "#{indent}- #{basename}")
  write_catalog_lines(lines)
  Common.log_info("catalog.yml に #{basename} を追加しました(#{section}")
end

.create_initial_catalog_with(basename) ⇒ Object

catalog.yml が存在しない場合に初期作成



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 302

def create_initial_catalog_with(basename)
  section = section_for_basename(basename)
  output = []

  output << default_header_comments
  output << ''

  CatalogLoader::SECTION_KEYS.each do |key|
    comments = default_section_comments[key] || []
    comments.each { output << it }
    output << "#{key}:"
    output << "  - #{basename}" if key == section
    output << ''
  end

  output << default_footer_comments

  FileUtils.mkdir_p(File.dirname(CATALOG_FILE))
  File.write(CATALOG_FILE, "#{output.join("\n")}\n", encoding: 'utf-8')
end

デフォルトのフッターコメント



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

def default_footer_comments
  <<~FOOTER.chomp
    ## 【Tips】
    ##
    ## ・付録やまえがきやあとがきがなければ、空にする。
    ##   例:
    ##
    ##     PREFACE:
    ##
    ##     CHAPTERS:
    ##       - 01-install.re
    ##
    ##     APPENDICES:
    ##
    ##     POSTFACE:
    ##
    ##
    ## ・第I部、第II部、…のように「部」を使うには、次のようにする。
    ##   (部タイトルの最後に半角の「:」をつけることに注意)
    ##
    ##     CHAPTERS:
    ##       - 初級編:
    ##           - 01-install.re
    ##           - 02-tutorial.re
    ##       - 中級編:
    ##           - 03-syntax.re
    ##           - 04-customize.re
    ##       - 上級編:
    ##           - 05-faq.re
    ##           - 06-bestpractice.re
    ##
  FOOTER
end

.default_header_commentsObject

デフォルトのヘッダーコメント



324
325
326
327
328
329
330
331
332
333
334
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 324

def default_header_comments
  <<~HEADER.chomp
    # ========================================
    # ビルド対象にする章の指定
    # ========================================
    # 一部の章のみを対象としたい場合は、以下のように指定します。
      # - 11-install  # 拡張子(.md)は、省略できます。
      # - 12-tutorial
    # CHAPTERS: 11-12, 13-15, 25 # 章番号のみでのカンマ区切り, 範囲指定も指定可能です。
  HEADER
end

.default_section_commentsObject

デフォルトのセクションコメント



337
338
339
340
341
342
343
344
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 337

def default_section_comments
  {
    'PREFACE' => ['## まえがき'],
    'CHAPTERS' => ['## 本文'],
    'APPENDICES' => ['## 付録'],
    'POSTFACE' => ['# ## あとがき']
  }
end

.detect_part_ranges(lines, section_start, section_end) ⇒ Array<Hash>

セクション内の部タイトル範囲を検出

Returns:

  • (Array<Hash>)

    { title:, header:, content_start:, end: }



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 193

def detect_part_ranges(lines, section_start, section_end)
  parts = []

  ((section_start + 1)..section_end).each do |idx|
    line = lines[idx]
    next if line.match?(/^\s+#/) # コメント行はスキップ

    # 部タイトル: インデントされたリスト項目で末尾が ":"
    next unless line.match?(/^\s+-\s+.+:\s*$/)

    parts << {
      title: line.strip.sub(/^-\s+/, '').sub(/:\s*$/, ''),
      header: idx,
      content_start: idx + 1
    }
  end

  # 各部の終了行を設定
  parts.each_with_index do |part, idx|
    part[:end] = if idx + 1 < parts.length
                   parts[idx + 1][:header] - 1
                 else
                   section_end
                 end
  end

  parts
end

.determine_target_part(lines, parts, chapter_num) ⇒ Hash?

章番号から所属する部を特定(コメント行も含めて判定)

Returns:

  • (Hash, nil)

    対象の部情報



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 224

def determine_target_part(lines, parts, chapter_num)
  part_infos = parts.map do |part|
    nums = (part[:content_start]..part[:end]).filter_map do |idx|
      bn = extract_basename_from_line(lines[idx])
      bn && CatalogLoader.extract_chapter_number(bn)
    end
    part.merge(min_num: nums.min, max_num: nums.max)
  end

  part_infos.each_with_index do |part, idx|
    next_min = part_infos[idx + 1]&.dig(:min_num)
    if (part[:min_num].nil? || chapter_num >= part[:min_num]) &&
       (next_min.nil? || chapter_num < next_min)
      return part
    end
  end

  part_infos.last
end

.extract_basename_from_line(line) ⇒ String?

行から basename を抽出(アクティブ/コメント両方対応)例: “ - 21-html” → “21-html”

"      # - 22-css" → "22-css"

Returns:

  • (String, nil)


151
152
153
154
155
156
157
158
159
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 151

def extract_basename_from_line(line)
  return unless line.match?(/^\s+#?\s*-\s+\S/)

  line.strip
      .sub(/^#\s*/, '')
      .sub(/^-\s+/, '')
      .sub(/\.md\s*$/, '')
      .strip
end

.find_active_entry_line(lines, basename) ⇒ Integer?

アクティブ(非コメント)エントリ行のインデックスを返す

Returns:

  • (Integer, nil)


135
136
137
138
139
140
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 135

def find_active_entry_line(lines, basename)
  lines.index do |l|
    stripped = l.strip
    ["- #{basename}", "- #{basename}.md"].include?(stripped)
  end
end

.find_section_boundaries(lines, section) ⇒ Array(Integer, Integer)

セクションの行範囲を特定

Returns:

  • (Array(Integer, Integer))
    開始行, 終了行


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

def find_section_boundaries(lines, section)
  start_idx = lines.index { |l| l.strip == "#{section}:" }
  return [nil, nil] unless start_idx

  # 次のセクションヘッダーを探す
  next_section_idx = nil
  ((start_idx + 1)...lines.length).each do |idx|
    if CatalogLoader::SECTION_KEYS.any? { lines[idx].strip == "#{it}:" }
      next_section_idx = idx
      break
    end
  end

  end_idx = (next_section_idx || lines.length) - 1

  # 末尾の空行・セクションコメント行を除外
  while end_idx > start_idx &&
        (lines[end_idx].strip.empty? || lines[end_idx].match?(/^#/))
    end_idx -= 1
  end

  [start_idx, end_idx]
end

.find_sorted_insert_in_range(lines, range_start, range_end, num, indent) ⇒ Array(Integer, String)

範囲内で章番号順の挿入位置を検索(コメント行も含めてソート位置を計算)

Returns:

  • (Array(Integer, String))
    挿入行インデックス, インデント文字列


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 279

def find_sorted_insert_in_range(lines, range_start, range_end, num, indent)
  last_entry_idx = range_start - 1

  (range_start..range_end).each do |idx|
    bn = extract_basename_from_line(lines[idx])
    next unless bn

    entry_num = CatalogLoader.extract_chapter_number(bn)
    next unless entry_num

    return [idx, indent] if entry_num > num

    last_entry_idx = idx
  end

  [last_entry_idx + 1, indent]
end

.find_text_insertion_point(lines, section, basename) ⇒ Array(Integer, String)

テキスト内の挿入位置とインデントを計算

Returns:

  • (Array(Integer, String))
    挿入行インデックス, インデント文字列


250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 250

def find_text_insertion_point(lines, section, basename)
  num = CatalogLoader.extract_chapter_number(basename) || 0
  section_start, section_end = find_section_boundaries(lines, section)

  # セクションが見つからない場合、末尾にセクションヘッダーを追加
  unless section_start
    lines << ''
    lines << "#{section}:"
    return [lines.length, '  ']
  end

  # 部タイトル構造を検出
  parts = detect_part_ranges(lines, section_start, section_end)

  if parts.any?
    target = determine_target_part(lines, parts, num)
    if target
      return find_sorted_insert_in_range(
        lines, target[:content_start], target[:end], num, '      '
      )
    end
  end

  # フラットなセクション
  find_sorted_insert_in_range(lines, section_start + 1, section_end, num, '  ')
end

.read_catalog_linesObject

catalog.yml を行配列として読み込む



119
120
121
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 119

def read_catalog_lines
  File.readlines(CATALOG_FILE, encoding: 'utf-8', chomp: true)
end

.remove_chapter(basename) ⇒ Object

章を catalog.yml から削除

Parameters:

  • basename (String)

    削除する章の basename(拡張子なし)



66
67
68
69
70
71
72
73
74
75
76
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 66

def remove_chapter(basename)
  return unless File.exist?(CATALOG_FILE)

  lines = read_catalog_lines
  idx = find_active_entry_line(lines, basename)
  return unless idx

  lines.delete_at(idx)
  write_catalog_lines(lines)
  Common.log_info("catalog.yml から #{basename} を削除しました")
end

.rename_chapter(old_basename, new_basename) ⇒ Object

章の basename を変更

Parameters:

  • old_basename (String)

    旧 basename

  • new_basename (String)

    新 basename



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/vivlio/starter/cli/build/catalog_updater.rb', line 81

def rename_chapter(old_basename, new_basename)
  return unless File.exist?(CATALOG_FILE)

  old_section = section_for_basename(old_basename)
  new_section = section_for_basename(new_basename)

  if old_section == new_section
    # 同一セクション内: 行内の basename を置換
    lines = read_catalog_lines
    idx = find_active_entry_line(lines, old_basename)
    if idx
      lines[idx] = lines[idx].sub(old_basename, new_basename)
      write_catalog_lines(lines)
      Common.log_info("catalog.yml: #{old_basename}#{new_basename}")
    end
  else
    # セクション変更: 削除 → 追加
    remove_chapter(old_basename)
    add_chapter(new_basename)
    Common.log_info("catalog.yml: #{old_basename}#{new_basename}#{old_section}#{new_section}")
  end
end

.section_for_basename(basename) ⇒ String

basename から適切なセクションを決定

Parameters:

  • basename (String)

Returns:

  • (String)

    セクションキー



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

def section_for_basename(basename)
  num = CatalogLoader.extract_chapter_number(basename)
  return 'CHAPTERS' unless num

  CatalogLoader.section_for_chapter_number(num)
end

.write_catalog_lines(lines) ⇒ Object

行配列を catalog.yml に書き戻す



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

def write_catalog_lines(lines)
  FileUtils.mkdir_p(File.dirname(CATALOG_FILE))
  File.write(CATALOG_FILE, "#{lines.join("\n")}\n", encoding: 'utf-8')
end