Module: Vivlio::Starter::CLI::Build::SectionBuilder

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

Overview

セクション HTML 生成モジュール

Constant Summary collapse

PREFACE_RANGE =

章レンジ(定数)- 新仕様に合わせて更新

(0..0)
MAIN_RANGE =
(1..89)
APPX_RANGE =
(90..98)
POSTFACE_RANGE =
(99..99)

Class Method Summary collapse

Class Method Details

.build_sections_html!(keep = nil) ⇒ Object

Step 4: セクション(前書き/本文/付録/後書き)をビルド(HTML生成)注: このメソッドは後方互換性のため維持するが、UnifiedBuildPipeline では

preprocess_sections!  convert_sections_html! に分割して呼び出すことを推奨


225
226
227
228
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 225

def build_sections_html!(keep = nil)
  preprocess_sections!(keep)
  convert_sections_html!(keep)
end

.chapter_order_from(basenames, base_dir = '.') ⇒ Object

章順序を取得(ベース名配列から)



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

def chapter_order_from(basenames, base_dir = '.')
  basenames = Array(basenames).map { |bn| bn.to_s.strip }.reject(&:empty?).uniq
  return [] if basenames.empty?

  resolver = TokenResolver::Resolver.new
  sort_key = lambda do |bn|
    entry = resolver.resolve_file(bn)
    entry.number ? [entry.number.to_i, bn] : [Float::INFINITY, bn]
  end

  html_basenames = Dir.glob(File.join(base_dir, '*.html'))
                      .map { |path| File.basename(path, '.html') }
                      .uniq
                      .sort_by { |bn| sort_key.call(bn) }

  ordered = html_basenames.select { |bn| basenames.include?(bn) }

  remaining = basenames - ordered
  remaining_sorted = remaining.sort_by { |bn| sort_key.call(bn) }

  ordered + remaining_sorted
end

.convert_sections_html!(entries_or_keep = nil) ⇒ Object

セクション(前書き/本文/付録/後書き)の変換を一括実行

Parameters:

  • entries_or_keep (Array<TokenResolver::Entry>, Array<String>, nil) (defaults to: nil)

    Entry 配列または basename 配列



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

def convert_sections_html!(entries_or_keep = nil)
  Common.log_action('[Step 4b] セクションの変換(HTML 生成)を実行します…')
  targets = resolve_targets(entries_or_keep)
  return if targets.empty?

  # 並列処理前に章の表示順を確定させる。
  # HeadingProcessor の @main_chapter_order はモジュールレベルのキャッシュなので、
  # 並列スレッドが不完全な HTML リストでキャッシュを作る前に正しい順序を注入する。
  main_tokens = targets.select { |t| t.match?(/\A\d{2}-/) && t[/\A(\d{2})/, 1].to_i.between?(1, 89) }
  PostProcessCommands::HeadingProcessor.chapter_tokens_override = main_tokens unless main_tokens.empty?

  concurrency = determine_concurrency
  if concurrency == 1
    targets.each { |target| convert_single_chapter!(target) }
  else
    parallel_each(targets, concurrency: concurrency) { |target| convert_single_chapter!(target) }
  end
end

.convert_single_chapter!(basename) ⇒ Object

単一章の変換(HTML生成)



147
148
149
150
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 147

def convert_single_chapter!(basename)
  ConvertCommands.execute_convert({}, [basename])
  PostProcessCommands.execute_post_process({}, [basename])
end

.determine_concurrencyObject

並列度を決定



212
213
214
215
216
217
218
219
220
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 212

def determine_concurrency
  concurrency = (ENV['VIVLIO_BUILD_CONCURRENCY'] || '').to_i
  if concurrency <= 0
    n_cores = Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 2
    concurrency = [n_cores, 4].min
    concurrency = 1 if concurrency <= 0
  end
  concurrency
end

.ensure_chapter_html_up_to_date!(basename, extra_sources: []) ⇒ Object

章HTMLの最新性をチェックし、必要なら再生成_titlepage/_legalpage/_colophon/_partN は .cache/vs/ から参照する



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

def ensure_chapter_html_up_to_date!(basename, extra_sources: [])
  html_path = File.join('.', "#{basename}.html")
  cached = TokenResolver::Resolver::CACHED_SYSTEM_FILES.include?(basename) || basename.match?(/\A_part\d+\z/)
  dir = cached ? Common::CACHE_DIR : Common::CONTENTS_DIR
  md_path = File.join(dir, "#{basename}.md")
  sources = [md_path, *Array(extra_sources)].compact

  needs_regeneration = !File.exist?(html_path)
  unless needs_regeneration
    html_mtime = begin
      File.mtime(html_path)
    rescue StandardError
      Time.at(0)
    end
    latest_source_mtime = sources.select { |src| File.exist?(src) }
                                 .map do |src|
      File.mtime(src)
    rescue StandardError
      Time.at(0)
    end
                                 .max
    needs_regeneration = latest_source_mtime && latest_source_mtime > html_mtime
  end

  return unless needs_regeneration

  Common.log_info("[HTML] 再生成します: #{basename}.html")
  preprocess_single_chapter!(basename)
  convert_single_chapter!(basename)
end

.parallel_each(items, concurrency: 1) ⇒ Object

簡易スレッドプールで並列実行



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 84

def parallel_each(items, concurrency: 1, &)
  list = Array(items)
  effective_concurrency = concurrency.to_i
  effective_concurrency = 1 if effective_concurrency <= 0
  Common.log_info("[parallel_each] concurrency=#{effective_concurrency}")
  return list.each(&) if effective_concurrency <= 1

  q = Queue.new
  list.each { |it| q << it }
  sentinel = Object.new
  effective_concurrency.times { q << sentinel }
  workers = Array.new(effective_concurrency) do
    Thread.new do
      loop do
        it = q.pop
        break if it.equal?(sentinel)

        yield(it)
      end
    end
  end
  workers.each(&:join)
end

.preprocess_sections!(entries_or_keep = nil) ⇒ Object

セクション(前書き/本文/付録/後書き)の前処理を一括実行

Parameters:

  • entries_or_keep (Array<TokenResolver::Entry>, Array<String>, nil) (defaults to: nil)

    Entry 配列または basename 配列



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 154

def preprocess_sections!(entries_or_keep = nil)
  Common.log_action('[Step 3] セクションの前処理(Markdown 修正)を実行します…')
  targets = resolve_targets(entries_or_keep)
  return if targets.empty?

  concurrency = determine_concurrency
  if concurrency == 1
    targets.each { |target| preprocess_single_chapter!(target) }
  else
    parallel_each(targets, concurrency: concurrency) { |target| preprocess_single_chapter!(target) }
  end

  # 全章の前処理完了後に1回だけクロスリファレンス処理を実行する
  PreProcessCommands.execute_cross_references(targets)
end

.preprocess_single_chapter!(basename) ⇒ Object

単一章の前処理



142
143
144
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 142

def preprocess_single_chapter!(basename)
  PreProcessCommands.execute_pre_process({}, [basename])
end

.resolve_targets(entries_or_keep = nil) ⇒ Array<String>

対象章を解決(Entry 配列または basename 配列から basename 配列を返す)

Parameters:

Returns:

  • (Array<String>)

    basename 配列



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 194

def resolve_targets(entries_or_keep = nil)
  raw = Array(entries_or_keep).compact
  if raw.any?
    # Entry オブジェクトかどうかを判定
    if raw.first.respond_to?(:basename)
      raw.map(&:basename).sort
    else
      raw.map { |s| File.basename(s.to_s, '.md') }.sort
    end
  else
    Dir[File.join(Common::CONTENTS_DIR, '*.md')]
      .map { |p| File.basename(p, '.md') }
      .reject { |bn| bn.start_with?('_') }
      .sort
  end
end

.time_step_for_chapter(chapter, step) ⇒ Object

章ごとの処理に計時を付与して実行



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/vivlio/starter/cli/build/section_builder.rb', line 68

def time_step_for_chapter(chapter, step)
  label = "#{chapter} / #{step}"
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  begin
    Common.with_current_step_label(label) do
      yield if block_given?
    end
  ensure
    finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    elapsed = finish - start
    Common.log_action("[Timer] #{label} : #{format('%.2f', elapsed)}s")
  end
  elapsed
end