Module: Vivlio::Starter::CLI::PreProcessCommands::ImagePathNormalizer
- Defined in:
- lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb
Overview
画像パス正規化・プレースホルダー生成モジュール
Constant Summary collapse
- NO_IMAGE_PLACEHOLDER_SVG =
<<~SVG.freeze <svg width="600" height="400" viewBox="0 0 600 400" fill="none" xmlns="http://www.w3.org/2000/svg"> <defs> <linearGradient id="vivlioTextGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" style="stop-color:#4a86e8;stop-opacity:1" /> <stop offset="100%" style="stop-color:#1c4587;stop-opacity:1" /> </linearGradient> <linearGradient id="starterTextGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" style="stop-color:#6aa84f;stop-opacity:1" /> <stop offset="100%" style="stop-color:#38761d;stop-opacity:1" /> </linearGradient> <linearGradient id="backgroundGradient" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="0%" style="stop-color:#E0F2F7;stop-opacity:1" /> <stop offset="100%" style="stop-color:#E8F5E9;stop-opacity:1" /> </linearGradient> </defs> <rect x="0" y="0" width="600" height="400" fill="url(#backgroundGradient)" /> <text#{' '} x="300"#{' '} y="140"#{' '} font-family="Arial, sans-serif"#{' '} font-size="72"#{' '} font-weight="bold" text-anchor="middle" dominant-baseline="middle" > <tspan fill="url(#vivlioTextGradient)">filename.webp</tspan> </text> <text#{' '} x="300"#{' '} y="260"#{' '} font-family="Arial, sans-serif"#{' '} font-size="72"#{' '} font-weight="bold" text-anchor="middle" dominant-baseline="middle" > <tspan fill="url(#starterTextGradient)">No Image</tspan> </text> </svg> SVG
Class Method Summary collapse
-
.build_source_image_line_map(source_path) ⇒ Object
元ファイルから画像名 → 行番号のマップを構築する。 pre_process で行数が変わる前の正しい行番号を取得するため。.
-
.fix_image_paths(content, filename, source_path: nil) ⇒ Object
Markdown 内の画像リンクを生成規約に合わせて正規化する コードブロック・インラインコード内は MarkdownUtils.extract_code_spans で退避し、 変換対象から除外する。.
-
.image_exists_for?(normalized_path) ⇒ Boolean
画像ディレクトリ内の拡張子違いを含めて存在を確認する.
-
.placeholder_image_path(missing_image_path = nil) ⇒ Object
プレースホルダーSVGを使用してデータURIを生成する.
-
.resolved_placeholder_or_path(alt_text, normalized_path, source_filename = nil, line_number = nil, original_image_name = nil) ⇒ Object
既存画像なら元のパスを、無い場合はプレースホルダーを返す.
-
.sanitize_placeholder_text(filename) ⇒ Object
プレースホルダーに差し込むファイル名をサニタイズする.
-
.svg_to_data_uri(svg_content) ⇒ Object
SVGコンテンツをURLエンコードした data URI に変換する.
Class Method Details
.build_source_image_line_map(source_path) ⇒ Object
元ファイルから画像名 → 行番号のマップを構築する。pre_process で行数が変わる前の正しい行番号を取得するため。
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb', line 146 def build_source_image_line_map(source_path) return {} unless source_path && File.exist?(source_path) map = {} in_code_block = false File.readlines(source_path, encoding: 'utf-8').each_with_index do |line, idx| stripped = line.lstrip if stripped.start_with?('```') in_code_block = !in_code_block next end next if in_code_block line.scan(/!\[[^\]]*\]\(([^)]+)\)/) do image_name = File.basename(::Regexp.last_match(1)) map[image_name] ||= idx + 1 end end map end |
.fix_image_paths(content, filename, source_path: nil) ⇒ Object
Markdown 内の画像リンクを生成規約に合わせて正規化するコードブロック・インラインコード内は MarkdownUtils.extract_code_spans で退避し、変換対象から除外する。
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb', line 80 def fix_image_paths(content, filename, source_path: nil) chapter_dir = filename.sub(/\.md$/, '') # 元ファイルの行番号マップを構築(画像名 → 元ファイルでの行番号) source_line_map = build_source_image_line_map(source_path) # コードブロック・インラインコードを退避して変換対象から除外する protected_text, spans = MarkdownUtils.extract_code_spans(content) # 行番号を保持しながら画像記法を正規化する # プレースホルダーは改行を含まないため、行番号は元のコンテンツと一致する lines = protected_text.lines transformed_lines = lines.each_with_index.map do |line, idx| line_number = idx + 1 line.gsub(%r{!\[(.*?)\]\((?!https?://)([^)]+)\)}) do alt_text = ::Regexp.last_match(1) image_path = ::Regexp.last_match(2) # すでに images/ から始まる場合はそのまま。相対パスは images/<章ディレクトリ>/ に正規化 normalized = if image_path.start_with?('images/') image_path else "images/#{chapter_dir}/#{image_path}" end # 生成物ポリシーに合わせて拡張子を .webp に寄せる(png/jpg のみ対象) normalized = normalized.sub(/\.(png|jpe?g)\z/i, '.webp') original_image_name = File.basename(image_path) # 元ファイルの行番号があればそちらを使う source_ln = source_line_map[original_image_name] || line_number resolved_placeholder_or_path(alt_text, normalized, filename, source_ln, original_image_name) end end # 退避したコードブロック・インラインコードを復元する MarkdownUtils.restore_code_spans(transformed_lines.join, spans) end |
.image_exists_for?(normalized_path) ⇒ Boolean
画像ディレクトリ内の拡張子違いを含めて存在を確認する
168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb', line 168 def image_exists_for?(normalized_path) relative_path = normalized_path.sub(%r{\Aimages/}, '') base_path = File.(relative_path, Common::IMAGES_DIR) # SVGの場合は直接チェック return File.exist?(base_path) if base_path.end_with?('.svg') # その他の画像形式は拡張子違いをチェック base_without_ext = base_path.sub(/\.webp\z/i, '') %w[.webp .png .jpg .jpeg].any? do |ext| File.exist?("#{base_without_ext}#{ext}") end end |
.placeholder_image_path(missing_image_path = nil) ⇒ Object
プレースホルダーSVGを使用してデータURIを生成する
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb', line 183 def placeholder_image_path(missing_image_path = nil) unless missing_image_path return 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E' end begin filename = File.basename(missing_image_path) replacement = sanitize_placeholder_text(filename) svg_with_filename = NO_IMAGE_PLACEHOLDER_SVG.gsub('filename.webp', replacement) svg_to_data_uri(svg_with_filename) rescue StandardError => e Common.log_warn("プレースホルダー画像の生成に失敗しました: #{e.class}: #{e.}") 'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%2F%3E' end end |
.resolved_placeholder_or_path(alt_text, normalized_path, source_filename = nil, line_number = nil, original_image_name = nil) ⇒ Object
既存画像なら元のパスを、無い場合はプレースホルダーを返す
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb', line 121 def resolved_placeholder_or_path(alt_text, normalized_path, source_filename = nil, line_number = nil, original_image_name = nil) return "" if image_exists_for?(normalized_path) image_name = original_image_name || File.basename(normalized_path) if source_filename && line_number Common.log_error( "#{source_filename}:#{line_number} - 画像 '#{image_name}' が見つかりません(代替画像を使用します)", detail: "画像の場所: #{normalized_path}" ) else # 位置情報が取れない場合のフォールバック Common.log_error( "画像 '#{image_name}' が見つかりません(代替画像を使用します)", detail: "画像の場所: #{normalized_path}" ) end placeholder_path = placeholder_image_path(normalized_path) "" end |
.sanitize_placeholder_text(filename) ⇒ Object
プレースホルダーに差し込むファイル名をサニタイズする
200 201 202 203 204 |
# File 'lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb', line 200 def sanitize_placeholder_text(filename) text = filename.to_s.strip text = 'missing image' if text.empty? CGI.escapeHTML(text) end |
.svg_to_data_uri(svg_content) ⇒ Object
SVGコンテンツをURLエンコードした data URI に変換する
207 208 209 210 211 |
# File 'lib/vivlio/starter/cli/pre_process/image_path_normalizer.rb', line 207 def svg_to_data_uri(svg_content) escaped = CGI.escape(svg_content.encode('utf-8')) escaped = escaped.gsub('+', '%20') "data:image/svg+xml;charset=utf-8,#{escaped}" end |