Module: VivlioStarter::CLI::PreProcessCommands::LinkImageValidator

Defined in:
lib/vivlio_starter/cli/pre_process/link_image_validator.rb

Defined Under Namespace

Classes: ImageIssue, LinkIssue, ValidationReport

Class Method Summary collapse

Class Method Details

.any_issues?Boolean

蓄積されたレポートにエラー(issue)が1件以上あるか判定する

Returns:

  • (Boolean)


217
218
219
220
221
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 217

def any_issues?
  @monitor.synchronize do
    @reports.any? { |r| r.image_issues.any? || r.link_issues.any? }
  end
end

.check_external_urls!Object

蓄積された外部 URL に対して HTTP 到達性チェックを実行する



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 130

def check_external_urls!
  config = resolve_config
  return unless config[:verify_external_links]

  urls = @monitor.synchronize { @external_urls.dup }
  return if urls.empty?

  # URL を重複排除(同じ URL は 1 回だけチェック)
  unique_urls = urls.uniq { it[:url] }
  Common.log_action("[検証] 外部 URL の到達性を確認しています(#{unique_urls.size} 件)…")

  results = check_urls_batch(unique_urls, config)

  # 結果をレポートに反映
  results.each do |result|
    next if result[:ok]

    issue = LinkIssue.new(
      filename: result[:filename],
      line_number: result[:line_number],
      url: result[:url],
      issue_type: :unreachable,
      status_code: result[:status_code],
      message: result[:message]
    )
    add_link_issue_to_report(result[:filename], issue)
    record_to_registry(issue)
  end
end

.merge_into_report(filename, image_issues: [], link_issues: []) ⇒ Object

同じファイルのレポートへ issue を足し込む。 検証は段階ごとに分かれて走る(リンクは原稿、画像はパス解決後、コード インクルードはさらにあと)が、集計は 1 ファイル 1 レポートに保つ。



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 242

def merge_into_report(filename, image_issues: [], link_issues: [])
  @monitor.synchronize do
    idx = @reports.index { it.filename == filename }
    if idx
      prev = @reports[idx]
      @reports[idx] = ValidationReport.new(
        filename: prev.filename,
        image_issues: prev.image_issues + image_issues,
        link_issues: prev.link_issues + link_issues
      )
    else
      @reports << ValidationReport.new(filename:, image_issues:, link_issues:)
    end
  end
end

検証結果のサマリーを表示する



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 161

def print_summary
  reports = @monitor.synchronize { @reports.dup }
  return if reports.empty?

  total_missing_image = reports.sum { it.image_issues.count { |i| i.issue_type == :missing } }
  total_missing_code  = reports.sum { it.image_issues.count { |i| i.issue_type == :missing_code } }
  total_link = reports.sum { it.link_issues.size }

  if total_missing_image.zero? && total_missing_code.zero? && total_link.zero?
    Common.log_info('リンク・画像の検証が完了しました(良好な状態です)')
    return
  end

  # --- サマリー集計 ---
  detail_lines = []
  detail_lines << "画像: #{total_missing_image} 件の課題(存在しない画像: #{total_missing_image}" if total_missing_image.positive?
  detail_lines << "ソースコード: #{total_missing_code} 件の課題(存在しないファイル: #{total_missing_code}" if total_missing_code.positive?

  if total_link.positive?
    bare = reports.sum { it.link_issues.count { |i| i.issue_type == :bare_url } }
    unreachable = reports.sum { it.link_issues.count { |i| i.issue_type == :unreachable } }
    dangerous = reports.sum { it.link_issues.count { |i| i.issue_type == :dangerous_scheme } }
    parts = []
    parts << "危険スキーム: #{dangerous}" if dangerous.positive?
    parts << "リンク切れ: #{unreachable}" if unreachable.positive?
    parts << "裸 URL: #{bare}" if bare.positive?
    detail_lines << "リンク: #{total_link} 件の問題(#{parts.join(', ')}"
  end

  config = resolve_config
  detail_lines << '外部URL到達性チェック: スキップ(--verify-links で有効化)' unless config[:verify_external_links]

  Common.log_summary('リンク・画像検証の結果:', detail: detail_lines.join("\n"))

  # --- 危険スキームの詳細(セキュリティ上の重要度が高いため先頭)---
  reports.each do |report|
    report.link_issues.select { it.issue_type == :dangerous_scheme }.each do |issue|
      Common.log_warn(
        "#{issue.filename}:#{issue.line_number} - 危険なスキームを検出しました",
        detail: "URL: #{issue.url}\n#{issue.message}"
      )
    end
  end

  # --- リンク切れの詳細 ---
  reports.each do |report|
    report.link_issues.select { it.issue_type == :unreachable }.each do |issue|
      Common.log_error(
        "#{issue.filename}:#{issue.line_number} - リンク切れを検出しました",
        detail: "URL: #{issue.url}#{issue.message}"
      )
    end
  end
end

.record_code_include_error(filename, line_number, code_name) ⇒ Object

コードインクルードエラーをレポートに記録する MarkdownTransformer から呼ばれ、preflight の終了コード判定に反映させる

Parameters:

  • filename (String)

    ソースファイル名

  • line_number (Integer)

    行番号

  • code_name (String)

    見つからなかったコードファイル名



228
229
230
231
232
233
234
235
236
237
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 228

def record_code_include_error(filename, line_number, code_name)
  issue = ImageIssue.new(
    filename:,
    line_number:,
    image_path: code_name,
    issue_type: :missing_code
  )
  merge_into_report(filename, image_issues: [issue])
  record_to_registry(issue)
end

.reset!Object

レポート蓄積をリセットする(ビルド開始時に呼ぶ)



54
55
56
57
58
59
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 54

def reset!
  @monitor.synchronize do
    @reports = []
    @external_urls = []
  end
end

.validate(content, filename, source_path: nil, config: resolve_config) ⇒ ValidationReport

リンクと画像をまとめて検証する(単発の検証向けの入口)。 ビルドの前処理は、それぞれを正しい段階で呼ぶために分けて使う。

Returns:



123
124
125
126
127
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 123

def validate(content, filename, source_path: nil, config: resolve_config)
  image_issues = validate_images(content, filename, source_path:, config:)
  link_issues = validate_links(content, filename, config:)
  ValidationReport.new(filename:, image_issues:, link_issues:)
end

.validate_images(content, filename, source_path: nil, config: resolve_config) ⇒ Array<ImageIssue>

画像の存在を検証する。パス正規化とデータ展開のあとに適用すること。

解決済みのパスでないとファイルの有無を判定できないため、この時点の行番号は 原稿とずれている。source_path を渡すと元ファイルから同じ画像を含む行を 引き当てて付け替える(同じ画像を何度も使う原稿では最初の行に丸まる)。

Parameters:

  • content (String)

    Markdown テキスト(画像パス正規化済み)

  • filename (String)

    対象ファイル名

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

    元ファイルのパス(行番号の付け替え用)

  • config (Hash) (defaults to: resolve_config)

    検証設定(verify_images)

Returns:



109
110
111
112
113
114
115
116
117
118
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 109

def validate_images(content, filename, source_path: nil, config: resolve_config)
  image_issues = config[:verify_images] ? scan_missing_images(content, filename) : []

  source_content = source_path && File.exist?(source_path) ? File.read(source_path, encoding: 'utf-8') : nil
  image_issues = image_issues.map { correct_line_number(it, source_content) } if source_content

  image_issues.each { record_to_registry(it) }
  merge_into_report(filename, image_issues: image_issues)
  image_issues
end

原稿に書かれたリンクを検証する。前処理にかける前の原稿へ適用すること。

裸 URL の対象は著者が原稿へ直に書いたものだけで、前処理が生成する要素 (@qr<img alt="URL"> など)は含まない。原稿をそのまま見るので 行番号の付け替えが要らない——同じ URL が何度も出てくる原稿でも、 指摘した行がそのまま著者の手元の行になる。

Parameters:

  • content (String)

    Markdown テキスト(前処理前の原稿)

  • filename (String)

    対象ファイル名

  • config (Hash) (defaults to: resolve_config)

    検証設定(verify_bare_urls, verify_external_links)

Returns:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/vivlio_starter/cli/pre_process/link_image_validator.rb', line 72

def validate_links(content, filename, config: resolve_config)
  link_issues = config[:verify_bare_urls] ? scan_bare_urls(content, filename) : []

  link_issues.each do |issue|
    Common.log_warn(
      "#{issue.filename}:#{issue.line_number} - 裸 URL を検出しました",
      detail: "URL: #{issue.url}"
    )
  end

  # セキュリティ検証(11-1): 危険スキームの検出は常時有効
  # file:// / javascript: 等は --no-verify でも無効化しない
  link_issues += scan_dangerous_schemes(content, filename)

  # 外部 URL チェック用に URL を蓄積(後でバッチ実行)
  if config[:verify_external_links]
    urls = extract_external_urls(content, filename)
    @monitor.synchronize { @external_urls.concat(urls) }
  end

  # 章別サマリー用の横断集計へブリッジする(preflight-chapter-summary-spec.md §2.2)。
  link_issues.each { record_to_registry(it) }
  merge_into_report(filename, link_issues: link_issues)
  link_issues
end