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
-
.active_entry_exists?(lines, basename) ⇒ Boolean
アクティブエントリとして存在するかチェック.
-
.add_chapter(basename) ⇒ Object
章を catalog.yml に追加.
-
.create_initial_catalog_with(basename) ⇒ Object
catalog.yml が存在しない場合に初期作成.
-
.default_footer_comments ⇒ Object
デフォルトのフッターコメント.
-
.default_header_comments ⇒ Object
デフォルトのヘッダーコメント.
-
.default_section_comments ⇒ Object
デフォルトのセクションコメント.
-
.detect_part_ranges(lines, section_start, section_end) ⇒ Array<Hash>
セクション内の部タイトル範囲を検出.
-
.determine_target_part(lines, parts, chapter_num) ⇒ Hash?
章番号から所属する部を特定(コメント行も含めて判定).
-
.extract_basename_from_line(line) ⇒ String?
行から basename を抽出(アクティブ/コメント両方対応) 例: “ - 21-html” → “21-html” “ # - 22-css” → “22-css”.
-
.find_active_entry_line(lines, basename) ⇒ Integer?
アクティブ(非コメント)エントリ行のインデックスを返す.
-
.find_section_boundaries(lines, section) ⇒ Array(Integer, Integer)
セクションの行範囲を特定.
-
.find_sorted_insert_in_range(lines, range_start, range_end, num, indent) ⇒ Array(Integer, String)
範囲内で章番号順の挿入位置を検索(コメント行も含めてソート位置を計算).
-
.find_text_insertion_point(lines, section, basename) ⇒ Array(Integer, String)
テキスト内の挿入位置とインデントを計算.
-
.read_catalog_lines ⇒ Object
catalog.yml を行配列として読み込む.
-
.remove_chapter(basename) ⇒ Object
章を catalog.yml から削除.
-
.rename_chapter(old_basename, new_basename) ⇒ Object
章の basename を変更.
-
.section_for_basename(basename) ⇒ String
basename から適切なセクションを決定.
-
.write_catalog_lines(lines) ⇒ Object
行配列を catalog.yml に書き戻す.
Class Method Details
.active_entry_exists?(lines, basename) ⇒ 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 に追加
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 << FileUtils.mkdir_p(File.dirname(CATALOG_FILE)) File.write(CATALOG_FILE, "#{output.join("\n")}\n", encoding: 'utf-8') end |
.default_footer_comments ⇒ Object
デフォルトのフッターコメント
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 <<~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_comments ⇒ Object
デフォルトのヘッダーコメント
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_comments ⇒ Object
デフォルトのセクションコメント
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>
セクション内の部タイトル範囲を検出
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?
章番号から所属する部を特定(コメント行も含めて判定)
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"
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?
アクティブ(非コメント)エントリ行のインデックスを返す
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)
セクションの行範囲を特定
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)
範囲内で章番号順の挿入位置を検索(コメント行も含めてソート位置を計算)
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)
テキスト内の挿入位置とインデントを計算
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_lines ⇒ Object
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 から削除
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 を変更
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 から適切なセクションを決定
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 |