Module: VivlioStarter::CLI::PreProcessCommands::FrontmatterGenerator
- Defined in:
- lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb
Overview
フロントマター生成・CSS 更新を担当するモジュール
Constant Summary collapse
- ALLOWED_COLORS =
%w[yellow orange red magenta purple indigo navy blue cyan teal green lime].freeze
Class Method Summary collapse
-
.apply_frontmatter(content, file_type, chapter_num, path: nil) ⇒ Object
既存フロントマターを併合するか新規生成して Markdown に反映する.
-
.build_base_frontmatter(chapter_css) ⇒ Object
フロントマターのベース構造を構築 link 順は [theme.css, 種別.css, book-settings.css, custom.css]。 book-settings.css(生成物・.cache/vs/ 配下)を 種別.css の後段へ置くことで book.yml 由来の設定値が既存テーマ CSS にカスケードで勝つ(P3)。 href はワークスペース内 HTML からの相対(Common.asset_prefix 前置・P4 §3.3)。.
-
.extract_error_position(error) ⇒ Object
エラーから行・列番号を抽出.
-
.filter_legacy_theme_links(links) ⇒ Object
古いテーマリンクを除外.
-
.find_frontmatter_end(text) ⇒ Integer?
フロントマター終了位置を行単位で検出する。 ファイル先頭の
---\nの直後から走査し、 コードフェンス(```)に入る前に---単独行が現れた位置を返す。. -
.frontmatter_body_code_lines(body) ⇒ Object
先頭
---\nを除いた本文領域について、コードとみなす行番号(1 始まり)を返す。. -
.generate_frontmatter(file_type, _chapter_num = nil, existing_frontmatter = {}) ⇒ Object
フロントマターを生成 CSS 設定の反映は BookSettingsCss 生成器('prepare theme images' ステップ)が 一括で担うため、章ごとの CSS 書換はここでは行わない(P3・調査報告 §7.3-3)。.
-
.log_detailed_snippet(fm_lines, line, column) ⇒ Object
詳細なスニペットをログ出力.
-
.log_frontmatter_error_message(line, column) ⇒ Object
エラーメッセージをログ出力.
-
.log_frontmatter_snippet(frontmatter_yaml, line, column) ⇒ Object
フロントマターの該当箇所をログ出力.
-
.merge_frontmatter(existing_frontmatter, new_frontmatter) ⇒ Object
フロントマターをマージ link は重複を除外して結合、vfm は著者の章別指定(既存側)を book.yml 由来の値より優先.
-
.merge_links(existing_links, new_links) ⇒ Object
リンク配列をマージ(重複を除外).
-
.normalize_char_count(value, label:) ⇒ Integer?
「1 行の文字数」を正の整数へ正規化する。未設定・空欄・解釈不能は nil (呼び出し側が theme.css の既定を生かす。P3 の「書かない条件では宣言しない」)。.
-
.normalize_css_length(value, label:, default: nil, fallback_unit: 'mm') ⇒ Object
CSS長さ値を正規化.
-
.parse_frontispiece_config(frontispiece_raw) ⇒ Object
frontispiece 設定を解析(Data オブジェクト前提) 寸法は mm ではなく「1 行の文字数」で受ける(heading-metrics-spec §1-2)—— 文字サイズが判型に追従するため、文字数なら A4 でも A5 でも同じ意味になる。.
-
.parse_ornament_config(ornament_raw) ⇒ Object
ornament 設定を解析。frontispiece と同じく「スカラー=画像名だけの短縮形」と 「マッピング=画像名+寸法」の両方を受ける(heading-metrics-spec §1-2)。 resolve_ornament_path は raw.to_s を見るため、マッピングは先に image を取り出す。.
-
.parse_theme_color(raw_color) ⇒ Object
テーマカラーをパース.
-
.parse_theme_settings(cfg = nil) ⇒ Object
テーマ設定を解析して構造化データを返す.
-
.parse_theme_style(raw_style) ⇒ Object
テーマスタイルをパース.
-
.report_frontmatter_error(error, frontmatter_yaml) ⇒ Object
フロントマター解析時のエラー内容を詳細ログへ出力する.
-
.resolve_chapter_css(file_type, existing_frontmatter) ⇒ Object
ファイルタイプに応じたCSS名を解決.
- .safe_config_hash(obj) ⇒ Object
-
.warn_unclosed_frontmatter(path) ⇒ Object
フロントマター開始の
---に対応する閉じ---が見つからない場合に警告を出す。 著者が誤って---を書き忘れた/閉じ忘れたケースを検知し、 ビルド結果が意図せず本文扱いになる前に気付かせる。.
Class Method Details
.apply_frontmatter(content, file_type, chapter_num, path: nil) ⇒ Object
既存フロントマターを併合するか新規生成して Markdown に反映する
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 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 173 def apply_frontmatter(content, file_type, chapter_num, path: nil) text = content.dup if text.start_with?('---') # フロントマター終了の `---` を正確に検出する。 # `/\A---\n(.*?)\n---\n/m` の最短マッチはコードブロック内の `---` で # 誤って止まるため、行単位で走査して最初の `---` 単独行を終端とする。 frontmatter_end = find_frontmatter_end(text) unless frontmatter_end warn_unclosed_frontmatter(path) return text end frontmatter_yaml = text[4...frontmatter_end].chomp body_after = text[(frontmatter_end + 4)..] begin existing_frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [], aliases: true) || {} merged_frontmatter = generate_frontmatter(file_type, chapter_num, existing_frontmatter) new_frontmatter_yaml = YAML.dump(merged_frontmatter) Common.log_success('フロントマター併合') Common.log_success('フロントマター更新') "#{new_frontmatter_yaml}---\n#{body_after}" rescue StandardError => e report_frontmatter_error(e, frontmatter_yaml) text end else new_frontmatter = generate_frontmatter(file_type, chapter_num) new_frontmatter_yaml = YAML.dump(new_frontmatter) Common.log_success('フロントマター追加') "#{new_frontmatter_yaml}---\n\n#{text}" end end |
.build_base_frontmatter(chapter_css) ⇒ Object
フロントマターのベース構造を構築 link 順は [theme.css, 種別.css, book-settings.css, custom.css]。 book-settings.css(生成物・.cache/vs/ 配下)を 種別.css の後段へ置くことで book.yml 由来の設定値が既存テーマ CSS にカスケードで勝つ(P3)。 href はワークスペース内 HTML からの相対(Common.asset_prefix 前置・P4 §3.3)。
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 150 def build_base_frontmatter(chapter_css) prefix = Common.asset_prefix hrefs = [ "#{prefix}stylesheets/theme.css", "#{prefix}stylesheets/#{chapter_css}", "#{prefix}#{BookSettingsCss.output_path}", "#{prefix}stylesheets/custom.css" ] lang = (Common::CONFIG.book.language || 'ja').to_s.strip lang = 'ja' if lang.empty? { 'link' => hrefs.map { { 'rel' => 'stylesheet', 'href' => it } }, 'lang' => lang, # 原稿中の改行は常に改行として組む(日本語執筆で直感的なハード改行)。 # 章ごとに変えたい場合は、その章のフロントマターに hardLineBreaks: false と # 書けばよい——merge_frontmatter は vfm について既存の記述を優先する。 'vfm' => { 'hardLineBreaks' => true } } end |
.extract_error_position(error) ⇒ Object
エラーから行・列番号を抽出
258 259 260 261 262 263 264 265 266 267 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 258 def extract_error_position(error) line = error.respond_to?(:line) && error.line ? error.line.to_i : error.[/line (\d+)/i, 1]&.to_i column = if error.respond_to?(:column) && error.column error.column.to_i else error.[/column (\d+)/i, 1]&.to_i end [line, column] end |
.filter_legacy_theme_links(links) ⇒ Object
古いテーマリンクを除外
390 391 392 393 394 395 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 390 def filter_legacy_theme_links(links) links.reject do |lnk| href = (lnk && lnk['href']).to_s href.match(%r{stylesheets/(theme-(yellow|blue|red|accent)\.css|theme-overrides\.css)}) end end |
.find_frontmatter_end(text) ⇒ Integer?
フロントマター終了位置を行単位で検出する。
ファイル先頭の ---\n の直後から走査し、
コードフェンス(```)に入る前に --- 単独行が現れた位置を返す。
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 211 def find_frontmatter_end(text) # 先頭の `---\n` をスキップした本文領域について、コードフェンス内の `---` を # 終端と誤認しないよう、フェンス行の判定は Masking(唯一の実装)へ委ねる。 # 可変長フェンス・入れ子・~~~ に一貫して追従する。 code_lines = frontmatter_body_code_lines(text[4..].to_s) pos = 4 lineno = 0 while pos < text.length line_end = text.index("\n", pos) break unless line_end lineno += 1 line = text[pos...line_end] return pos if line == '---' && !code_lines.include?(lineno) pos = line_end + 1 end nil end |
.frontmatter_body_code_lines(body) ⇒ Object
先頭 ---\n を除いた本文領域について、コードとみなす行番号(1 始まり)を返す。
233 234 235 236 237 238 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 233 def frontmatter_body_code_lines(body) prose = Set.new Masking.each_prose_line(body) { |_line, lineno| prose << lineno } total = body.each_line.count (1..total).reject { prose.include?(it) }.to_set end |
.generate_frontmatter(file_type, _chapter_num = nil, existing_frontmatter = {}) ⇒ Object
フロントマターを生成 CSS 設定の反映は BookSettingsCss 生成器('prepare theme images' ステップ)が 一括で担うため、章ごとの CSS 書換はここでは行わない(P3・調査報告 §7.3-3)。
130 131 132 133 134 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 130 def generate_frontmatter(file_type, _chapter_num = nil, existing_frontmatter = {}) chapter_css = resolve_chapter_css(file_type, existing_frontmatter) new_frontmatter = build_base_frontmatter(chapter_css) merge_frontmatter(existing_frontmatter, new_frontmatter) end |
.log_detailed_snippet(fm_lines, line, column) ⇒ Object
詳細なスニペットをログ出力
292 293 294 295 296 297 298 299 300 301 302 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 292 def log_detailed_snippet(fm_lines, line, column) idx = line - 1 start_idx = [idx - 2, 0].max finish_idx = [idx + 2, fm_lines.length - 1].min snippet = fm_lines[start_idx..finish_idx].each_with_index.map do |l, i| "#{start_idx + i + 1}: #{l.chomp}" end.join("\n") err_line_text = fm_lines[idx].to_s.chomp caret_line = column&.positive? ? "#{' ' * (column - 1)}^" : '' Common.log_info("問題のフロントマター(抜粋):\n---\n#{snippet}\n---\n該当行:\n#{err_line_text}\n#{caret_line}") end |
.log_frontmatter_error_message(line, column) ⇒ Object
エラーメッセージをログ出力
270 271 272 273 274 275 276 277 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 270 def (line, column) if line&.positive? col_str = column&.positive? ? column : '?' Common.log_warn("フロントマター(--- ~ ---)の記述に誤りがあります(位置: 行#{line} 列#{col_str})。内容を見直してください。") else Common.log_warn('フロントマター(--- ~ ---)の記述に誤りがあります。内容を見直してください。') end end |
.log_frontmatter_snippet(frontmatter_yaml, line, column) ⇒ Object
フロントマターの該当箇所をログ出力
280 281 282 283 284 285 286 287 288 289 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 280 def log_frontmatter_snippet(frontmatter_yaml, line, column) fm_lines = frontmatter_yaml.to_s.lines if line&.positive? && line <= fm_lines.length log_detailed_snippet(fm_lines, line, column) else Common.log_info("問題のフロントマター(抜粋):\n---\n#{frontmatter_yaml}\n---") end rescue StandardError Common.log_info("問題のフロントマター(抜粋):\n---\n#{frontmatter_yaml}\n---") end |
.merge_frontmatter(existing_frontmatter, new_frontmatter) ⇒ Object
フロントマターをマージ link は重複を除外して結合、vfm は著者の章別指定(既存側)を book.yml 由来の値より優先
363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 363 def merge_frontmatter(existing_frontmatter, new_frontmatter) merged = existing_frontmatter.dup merged.delete('stylesheet') merged['link'] = filter_legacy_theme_links(merged['link']) if merged['link'].is_a?(Array) new_frontmatter.each do |key, value| merged[key] = case [key, merged[key]] in ['link', Array => existing] then merge_links(existing, value) in ['vfm', Hash => existing] then value.merge(existing) else value end end merged end |
.merge_links(existing_links, new_links) ⇒ Object
リンク配列をマージ(重複を除外)
398 399 400 401 402 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 398 def merge_links(existing_links, new_links) existing_links + new_links.reject do |new_link| existing_links.any? { |existing| existing['href'] == new_link['href'] } end end |
.normalize_char_count(value, label:) ⇒ Integer?
「1 行の文字数」を正の整数へ正規化する。未設定・空欄・解釈不能は nil (呼び出し側が theme.css の既定を生かす。P3 の「書かない条件では宣言しない」)。
112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 112 def normalize_char_count(value, label:) return nil if value.nil? v = value.to_s.strip return nil if v.empty? count = v.to_i if count.positive? && v =~ /\A\d+\z/ count else Common.log_warn("#{label} は 1 行の文字数(正の整数)で指定してください (#{v})。既定値を使います。") nil end end |
.normalize_css_length(value, label:, default: nil, fallback_unit: 'mm') ⇒ Object
CSS長さ値を正規化
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 342 def normalize_css_length(value, label:, default: nil, fallback_unit: 'mm') return default if value.nil? v = value.to_s.strip return default if v.empty? if v =~ /^-?\d+(?:\.\d+)?$/ "#{v}#{fallback_unit}" elsif v =~ /^-?\d+(?:\.\d+)?(?:mm|cm|in|px|pt|pc|em|rem|vw|vh|vmin|vmax|%)$/i v else Common.log_warn("#{label} の形式が想定外です (#{v})。#{fallback_unit}単位として扱います。") numeric = v.gsub(/[^0-9.-]/, '') return default if numeric.empty? "#{numeric}#{fallback_unit}" end end |
.parse_frontispiece_config(frontispiece_raw) ⇒ Object
frontispiece 設定を解析(Data オブジェクト前提) 寸法は mm ではなく「1 行の文字数」で受ける(heading-metrics-spec §1-2)—— 文字サイズが判型に追従するため、文字数なら A4 でも A5 でも同じ意味になる。
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 77 def parse_frontispiece_config(frontispiece_raw) # String の場合はそのまま image 名として使用 source = frontispiece_raw.is_a?(String) ? frontispiece_raw : frontispiece_raw&.dig(:image) path = ThemeImageResolver.resolve_frontispiece_path(source, allow_generation: true) cfg = frontispiece_raw.is_a?(String) ? nil : frontispiece_raw { path: path, edge_inset: normalize_css_length(cfg&.dig(:edge_inset), label: 'theme.frontispiece.edge_inset', default: '0mm'), heading_offset: normalize_css_length(cfg&.dig(:heading_offset), label: 'theme.frontispiece.heading_offset'), heading_chars: normalize_char_count(cfg&.dig(:heading_chars), label: 'theme.frontispiece.heading_chars'), lead_chars: normalize_char_count(cfg&.dig(:lead_chars), label: 'theme.frontispiece.lead_chars') } end |
.parse_ornament_config(ornament_raw) ⇒ Object
ornament 設定を解析。frontispiece と同じく「スカラー=画像名だけの短縮形」と 「マッピング=画像名+寸法」の両方を受ける(heading-metrics-spec §1-2)。 resolve_ornament_path は raw.to_s を見るため、マッピングは先に image を取り出す。
98 99 100 101 102 103 104 105 106 107 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 98 def parse_ornament_config(ornament_raw) scalar = ornament_raw.nil? || ornament_raw.is_a?(String) || ornament_raw.is_a?(Symbol) source = scalar ? ornament_raw : ornament_raw&.dig(:image) cfg = scalar ? nil : ornament_raw { path: ThemeImageResolver.resolve_ornament_path(source, allow_generation: true), heading_chars: normalize_char_count(cfg&.dig(:heading_chars), label: 'theme.ornament.heading_chars') } end |
.parse_theme_color(raw_color) ⇒ Object
テーマカラーをパース
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 305 def parse_theme_color(raw_color) s = raw_color.to_s.strip t = s.downcase hex_ok = t.match(/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i) = t.match(/^(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i) hex_0x_ok = t.match(/^0x(?:[0-9a-f]{6}|[0-9a-f]{8})$/i) if t.empty? ['yellow', 'var(--accent-yellow)'] elsif hex_ok [t, t] elsif normalized = "##{t}" [normalized, normalized] elsif hex_0x_ok normalized = "##{t.sub(/^0x/i, '')}" [normalized, normalized] elsif ALLOWED_COLORS.include?(t) [t, "var(--accent-#{t})"] else # 無効な色名は既定色(yellow)へフォールバックしてビルドを継続する。 # 著者向けの警告は ThemeValidator が Step 2 で一度だけ表示する # (ここは章ごとに呼ばれるため、ログを出すと重複してしまう)。 ['yellow', 'var(--accent-yellow)'] end end |
.parse_theme_settings(cfg = nil) ⇒ Object
テーマ設定を解析して構造化データを返す
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 44 def parse_theme_settings(cfg = nil) cfg ||= Common::CONFIG theme_cfg = cfg.theme theme_color = theme_cfg.color theme_style_raw = theme_cfg.style frontispiece_raw = theme_cfg.frontispiece ornament_raw = theme_cfg.ornament theme_name, theme_accent_value = parse_theme_color(theme_color) theme_style = parse_theme_style(theme_style_raw) frontispiece_cfg = parse_frontispiece_config(frontispiece_raw) ornament_cfg = parse_ornament_config(ornament_raw) { theme_name: theme_name, theme_accent_value: theme_accent_value, theme_style: theme_style, theme_cfg: theme_cfg, frontispiece_path: frontispiece_cfg[:path], edge_inset_value: frontispiece_cfg[:edge_inset], heading_offset_value: frontispiece_cfg[:heading_offset], heading_chars_value: frontispiece_cfg[:heading_chars], lead_chars_value: frontispiece_cfg[:lead_chars], ornament_path: ornament_cfg[:path], ornament_heading_chars_value: ornament_cfg[:heading_chars] } end |
.parse_theme_style(raw_style) ⇒ Object
テーマスタイルをパース
334 335 336 337 338 339 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 334 def parse_theme_style(raw_style) s = (raw_style || 'image').to_s.strip.downcase %w[simple image].include?(s) ? s : 'image' rescue StandardError 'image' end |
.report_frontmatter_error(error, frontmatter_yaml) ⇒ Object
フロントマター解析時のエラー内容を詳細ログへ出力する
251 252 253 254 255 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 251 def report_frontmatter_error(error, frontmatter_yaml) line, column = extract_error_position(error) (line, column) log_frontmatter_snippet(frontmatter_yaml, line, column) end |
.resolve_chapter_css(file_type, existing_frontmatter) ⇒ Object
ファイルタイプに応じたCSS名を解決
137 138 139 140 141 142 143 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 137 def resolve_chapter_css(file_type, existing_frontmatter) return existing_frontmatter['stylesheet'] if existing_frontmatter['stylesheet'] return 'chapter.css' if file_type == 'chapter' return 'part-title.css' if file_type == 'part_title' "#{file_type}.css" end |
.safe_config_hash(obj) ⇒ Object
378 379 380 381 382 383 384 385 386 387 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 378 def safe_config_hash(obj) case obj when Hash obj.dup when nil {} else obj.respond_to?(:to_h) ? obj.to_h : {} end end |
.warn_unclosed_frontmatter(path) ⇒ Object
フロントマター開始の --- に対応する閉じ --- が見つからない場合に警告を出す。
著者が誤って --- を書き忘れた/閉じ忘れたケースを検知し、
ビルド結果が意図せず本文扱いになる前に気付かせる。
244 245 246 247 248 |
# File 'lib/vivlio_starter/cli/pre_process/frontmatter_generator.rb', line 244 def warn_unclosed_frontmatter(path) location = path || '(unknown file)' warn "[frontmatter] 警告: #{location} のフロントマター開始 `---` に対応する閉じ `---` が" \ 'コードフェンス外に見つかりません。フロントマターは適用されず、本文として扱われます。' end |