Module: VivlioStarter::CLI::PreProcessCommands::MarkdownTransformer
- Defined in:
- lib/vivlio_starter/cli/pre_process/markdown_transformer.rb
Overview
Markdown 特殊記法変換モジュール
Defined Under Namespace
Classes: FancyMarker
Constant Summary collapse
- TERMINAL_BLOCK_PATTERN =
:::terminal … ::: の検出パターン(開始・終了とも独立行)。
/^:{3,}[ \t]*\{\.terminal\}[ \t]*\n(.*?)^:{3,}[ \t]*$\n?/m- TERMINAL_FENCE_LANG =
前処理で terminal ブロックを退避するチルダフェンスの言語名。 後処理の TerminalBlockConverter が pre.language-vs-terminal を拾う。
'vs-terminal'- TALK_BLOCK_PATTERN =
:::[オプション] … ::: の検出(開始・終了とも独立行)。 キャプチャ 1 = オプション文字列(
{.talk}なら空)、キャプチャ 2 = ブロック本文。 /^:{3,}[ \t]*\{\s*\.talk((?:[ \t][^}]*)?)\}[ \t]*\n(.*?)^:{3,}[ \t]*$\n?/m- TALK_SPEAKER_PATTERN =
行頭の「キー: 発話」(半角キー+半角コロン+空白)で話者を切り替える。 発話が空(キーのみ)で継続行に続けるケースも許すため、本文はオプショナル。
/\A([A-Za-z0-9][A-Za-z0-9_-]*):(?:[ \t]+(.*))?\z/- TALK_OPTION_KEYS =
ブロック単位で指定できるオプションキー。
%w[style name avatar separator].freeze
- TALK_AVATAR_EXTENSIONS =
アバター画像として探す拡張子(talk.yml の指定が実体と食い違うときの変種)。
%w[.webp .png .jpg .jpeg].freeze
- ROMAN_DIGIT_VALUES =
ローマ数字 1 文字の値(小文字で引く)
{ 'i' => 1, 'v' => 5, 'x' => 10, 'l' => 50, 'c' => 100, 'd' => 500, 'm' => 1000 }.freeze
- FANCY_STYLE_CSS =
出力
- クラス名の様式部・区切り部(§2.1 対応表。vs-list-<様式><区切り接尾辞>)
{ decimal: 'decimal', lower_alpha: 'lower-alpha', upper_alpha: 'upper-alpha', lower_roman: 'lower-roman', upper_roman: 'upper-roman' }.freeze
- FANCY_SEPARATOR_SUFFIX =
{ period: '', paren: '-paren', paren2: '-paren2' }.freeze
- FANCY_TYPE_ATTR =
- へ写せる様式(decimal は HTML 既定のため付けない)
{ lower_alpha: 'a', upper_alpha: 'A', lower_roman: 'i', upper_roman: 'I' }.freeze
Class Method Summary collapse
-
.accept_marker!(marker, stack, ol_queue, line:, source_filename:, after_blank: false) ⇒ Object
マーカー行を現在のリスト文脈で受理できるか判定し、受理時はスタック・キューを更新する。 戻り値: true(項目/新リスト)/ :split(空行を挟んだ様式変更=EOB で別リストに分ける)/ false(不受理。大文字+ピリオド 1 スペースの新規リスト等。呼び出し側が継続行または ブロック終端として扱う)。.
-
.acceptable_list_start?(marker) ⇒ Boolean
大文字様式+ピリオドのリスト開始行は空白 2 つ以上を要求する(`B.
-
.apply_talk_option(options, key, value, source_filename:) ⇒ Object
オプション 1 つを表示設定へ反映する(未知の値は 🟡 で現状維持)。.
-
.auto_talk_avatar_tag(char, source_filename:) ⇒ Object
自動生成した簡易アバターの
。生成物はワークスペース内実体のため asset_prefix を付けない(数式 SVG・showcase と同じ規約)。.
-
.build_ol_marker(indent_str, separator, token, gap, body) ⇒ Object
ol マーカーを組み立てる(トークンが様式に分類できなければ nil =マーカーでない).
-
.classify_fancy_token(token) ⇒ Object
マーカー中身のトークンを [様式, 開始値] へ分類する(§2.2 の判定順)。 単文字の i / v / x 等はローマ数字を優先する(Pandoc 準拠。c 始まりの英字リストは書けない)。.
-
.convert_book_card_inner_markdown(content) ⇒ Object
...
- .convert_container_blocks(content, class_name:) ⇒ Object
::: ... 記法で囲まれたコンテナを div に変換。 コードブロック内の ::: 記法は変換対象外とする。.
- .convert_definition_lists(content) ⇒ Object
標準 Markdown(pandoc / Markdown Extra 風)の定義リスト記法を
- に変換する。 用語 ←
- : 説明 ←
- (複数並べれば複数
- ) 続き行 ← 直前
- の続き(半角スペース字下げ) VFM は定義リストに未対応なので、検出ブロックを Kramdown でレンダリングして
- を生成する(class は索引/奥付の
- と衝突させないため)。 著者は空行なしのコンパクトな形でも書け、内部でエントリ間に空行を補ってから Kramdown に渡す。インラインコード
...等のインライン装飾は Kramdown が処理する。 コードフェンス(``` 可変長)内は対象外。. - ) 続き行 ← 直前
- .convert_fancy_lists(content, source_filename: nil) ⇒ Object
Pandoc fancy_lists 互換マーカー(A.
- .convert_list_block(lines, start, code_lines, source_filename:) ⇒ Object
start 行から始まるリストブロックを走査し、[終端 index(排他的), 置換文字列] を返す。 fancy マーカーを含まないブロックは原文のまま(バイト一致)返す。.
- .convert_standalone_spacing(content) ⇒ Object
単独行の aki / aki2 を縦余白マクロ @vspace に置換する。 aki は本来「段落末に付けて段落へ class を与える」インライン記法のため、 それ自身だけを 1 行に書くと(付与先の本文が無いので)VFM が "aki" を そのまま文字として出力してしまう。空行用途(
のように 1 行空ける)で 書かれた単独行は @vspace:1lh(aki2 は 2lh)へ置き換え、独立段落となるよう 直後に空行を補う。段落末に付いた aki は対象外(後処理の組み込み置換ルールでクラス化)。 コードフェンス内は対象外。.- .convert_talk_blocks(content, registry:, source_filename:) ⇒ Object
:::talk ブロックを会話文 HTML へ変換する。 発話内のインライン Markdown(強調・コード・リンク)は生 HTML 内では VFM が処理 しないため、book-card と同じく発話部分だけ Kramdown でインライン変換して埋め込む。.
- .convert_terminal_blocks(content) ⇒ Object
:::terminal を独自言語名のチルダフェンスへ書き換える。.
- .definition_continuation_line?(line) ⇒ Boolean
継続行: 字下げされた非空行(直前の定義の続き).
- .definition_def_line?(line) ⇒ Boolean
定義行: 行頭が「: 」(コロン+空白)で内容が続くもの.
- .definition_list_end(lines, idx) ⇒ Object
定義リストブロックの終端(排他的 index)を返す。 定義/継続/(定義が続く)用語/内部空行(ルーズ形式の区切り)を取り込む。.
- .definition_list_start?(lines, idx) ⇒ Boolean
用語行の直後が定義行なら、定義リストの開始.
- .definition_term_line?(line) ⇒ Boolean
用語行: 行頭から始まる非空行で、定義行(: )・継続行(字下げ)・他のブロック構文でないもの.
- .escape_html(str) ⇒ Object
生 HTML 属性・テキストへ差し込む文字列を最小エスケープする。.
- .escape_inline_code_html(line) ⇒ Object
インラインコード内の HTML 予約文字をエスケープする.
- .extract_line_range(lines, start_line, end_line) ⇒ Object
content 内の各 include 記法マッチ文字列 → 行番号のマップを構築する 1 始まりの行範囲 [start_line, end_line] の行配列を返す。 終了行がファイル末尾を超える場合はファイル末尾までにクランプする(開始が有効な限り取り込む)。 開始行がファイル末尾を超える・開始 < 1・逆順(end < start)のように救済不能な場合は nil を返し、 呼び出し側は全文取り込みへフォールバックする(lines は範囲外で nil を返し
nil.joinで落ちるため、ここで明示的に弾く)。.- .fancy_block_start?(line) ⇒ Boolean
リストブロックの開始行か(インデント 0-3 のマーカー行).
- .fancy_list_class(style, separator) ⇒ Object
出力
- のクラス名(§2.1 対応表のセル値).
- .fancy_marker?(marker) ⇒ Boolean
fancy マーカーか(標準=数字+ピリオド/片括弧。ul は fancy でない).
- .fancy_marker_literal(marker) ⇒ Object
マーカーの原文表記(警告メッセージの修正例に使う).
- .fancy_ol_attributes(entry) ⇒ Object
1 つの
- に付与する属性のリスト。標準様式(style nil)は start 以外無加工。.
- .format_book_card_inner_html(inner_html) ⇒ Object
book-card の内側を整形.
- .hard_break_line(line) ⇒ Object
非空行の末尾を Markdown のハード改行(半角スペース2つ)へ正規化する。 既存の末尾空白は一度除いてから2つに揃えるため冪等。空行はそのまま返す。.
- .image_talk_avatar_tag(src) ⇒ Object
著者が用意した画像の
。 src には asset_prefix を前置する。アバターは images/ 配下の著者資産であり、 ワークスペース内に実体を持つ生成物(数式 SVG・showcase・mermaid)とは違って pdf/ ミラーへコピーされないため、prefix 無しの相対では PDF 側で解決できない (ImagePathNormalizer が通常画像へ付けるのと同じ規約)。EPUB では EpubBuilder が prefix を剥がし、localize_assets! が images/ を同梱するのでそのまま解決する。.
- .inject_talk_prefix(body, prefix) ⇒ Object
レンダリング済みの発話 HTML の先頭
の内側へ名前+区切りを差し込む。
で始まらない出力(リスト等)になった場合は段落を前置してフォールバックする。.
- .normalize_book_card_md(md_text) ⇒ Object
book-card 内のMarkdownを事前整形.
- .normalize_container_fence_spacing(content) ⇒ Object
:::class コンテナの開始行直後・終了行直前に空行を補い、開始/終了行を独立段落にする。 VFM は hardLineBreaks のため、空行が無いと開始 ":::output" 行を直後の本文行と 1 つの
に結合してしまう。その状態で後処理の置換ルールが ":::class" を
へ 置換するとが分割され、先頭の本文(クロスリファレンスのキャプション 等)が コンテナ直下に取り残されて wrap されず、見出しが素の強調表示になる不具合が起きる。 開始直後・終了直前に空行を挟むことで、内側の各ブロックが独立段落になり正しく整形される。 コードフェンス内の ::: は対象外。既に空行がある場合は二重に入れない。.
- .normalized_continuation_line(line, stack) ⇒ Object
継続行を現在項目の本文開始位置(マーカー行の字下げ + 4 スペース)へ揃える.
- .normalized_marker_line(marker, stack) ⇒ Object
マーカー行を「4 スペース/レベルの字下げ+連番 1.
- .open_fancy_list!(marker, stack, ol_queue) ⇒ Object
新しいリストを開く(スタックへ push・ol なら属性キューへ登録).
- .parse_list_marker(line) ⇒ Object
行をリストマーカーとして解析する(マーカーでなければ nil)。
aa.のような複数英字は様式に該当せずマーカーにしない(§2.2-4)。.- .parse_talk_options(args, display, source_filename:) ⇒ Array(TalkRegistry::TalkDisplay, Array<String>)
ブロック引数(
style=inline name=off .foo)を解析し、talk.yml の表示設定へ重ねる。.- .parse_talk_utterances(body, source_filename:) ⇒ Object
ブロック本文を発話(lines)の配列へ。空行は無視、字下げ行は継続、 「キー:」に一致しない非字下げ行は 🔴(話者キーなし)。.
- .patch_fancy_ol_attributes(html, ol_queue, source_filename:) ⇒ Object
生成 HTML の
- (出現順=ソース順)へ様式クラス・type・start・counter-reset を注入する。 キューと
- 出現数が食い違ったら nil を返し、呼び出し側は原文のまま埋め戻す (防御・ビルドは止めない)。.
- .process_code_include(content, source_filename: nil, source_path: nil) ⇒ Object
include:path[:start-end]を検出し、codes/ または絶対パスから読込。 マークダウンのコードブロックおよびインラインコード内に記述された include 記法は記法の説明例であるためスキップする。.- .render_definition_list(block) ⇒ Object
定義リストブロックを Kramdown で
- 化する。 Kramdown はエントリ間に空行を要求するため、用語行の前へ空行を補ってから渡す。 また本書全体の hardLineBreaks: true(改行=
)に揃えるため、説明(dd)内の 各行末へ Markdown のハード改行(半角スペース2つ)を補い、複数行の説明が
で改行されるようにする(空行=エントリ区切りはそのまま残す)。.- .render_talk_block(body, registry:, source_filename:, spans:, options:, extra_classes:) ⇒ Object
ブロック 1 つ分を
…へ。発話が 1 つも無ければ空文字。 区切り文字は data-talk-sep として容器に持たせる——Kindle 劣化(EpubBuilder)が talk.yml を読み直さずに inline 形式へ組み替えられるようにするため(§2.3)。.- .render_talk_body(lines, spans:) ⇒ Object
発話本文(継続行を含む)を単一段落 HTML へ。継続行は本書の hardLineBreaks に揃え
化する。 インラインコード等は退避済みのため、Kramdown へ渡す前に spans でこの発話ぶんを復元する。.- .render_talk_item(utterance, registry:, source_filename:, spans:, options:) ⇒ Object
発話 1 つを .talk-item へ。未定義キーは 🔴 で追記例を提示し、 表示名=キー・色なしのフォールバックで組む(ビルドは止めない)。.
- .render_talk_item_chat(key, char, name, body, options:, source_filename:) ⇒ Object
chat: 話者の見せ方はアバターの有無で変わる。 アバターあり … 吹き出しの外側に話者列を立て、アバターの下へ名前を積む アバターなし … 名前だけの列は幅を持て余すため、吹き出し上部のラベルにする name=off でも .talk-name は出力し talk-name-off で隠す——Kindle 劣化が この要素を段落内へ移して話者名を見せるため、DOM から消してはならない(§2.3)。.
- .render_talk_item_inline(key, name, body, options:) ⇒ Object
inline: 「名前+区切り+発話」を 1 段落に収める。アバターと左右振り分けは行わない。.
- .resolve_talk_avatar(avatar) ⇒ Object
avatar 指定から images/characters/ 配下の実在ファイル名を解決する(無ければ nil)。 著者が書いた拡張子をまず尊重し、次に .webp/.png/.jpg/.jpeg の変種を探す。 ImagePathNormalizer.image_exists_for? は「正規化済み .webp パス」を前提に .webp だけを 剥がすため、
avatar: a.pngを渡すとa.png.webp等を探して実在する a.png を取りこぼし、 逆にavatar: a.webp(実体は a.png)では実在しない .webp を src に出してしまう。 ここは著者の生の指定を受けるので、常に「実在するファイル」を src にする。.- .roman_to_int(token) ⇒ Object
ローマ数字文字列を整数へ(iv → 4)。減算則: 次の桁より小さい桁は引く。.
- .same_list_style?(marker, top) ⇒ Boolean
マーカーが現在のリストの様式(先頭項目で確定)と一致するか.
- .talk_avatar_tag(char, options:, source_filename:) ⇒ Object
アバター
(吹き出し外側の丸抜き)。出せない場合は nil を返す(ビルドは止めない)。 解決順は talk-auto-avatar-spec.md §1.3: 1.
- .talk_name_classes(options) ⇒ Object
話者名 span のクラス(name=off は隠す。Kindle 劣化時にこのクラスを外す)。.
- .terminal_fence_length(body) ⇒ Object
フェンスの長さ。本文に
~~~が現れても閉じ位置がずれないよう、 本文中の最長のチルダ連続 + 1 と 3 の大きい方を採る。.- .transform_links_to_footnotes(md_text) ⇒ Object
Markdown内のリンク記法を脚注化.
- .unescape_fancy_marker(line) ⇒ Object
行頭マーカーの \ エスケープ((1) 等)を解除して地の文にする(§2.2)。 fancy マーカーの形をした行だけが対象(標準リスト・ul の \ は VFM に委ねる)。.
- .warn_fancy_style_change(top, line, source_filename) ⇒ Object
同一リスト途中の様式変更を警告する(リストごとに 1 回・先頭様式で続行).
- .warn_talk_inline_avatar(source_filename) ⇒ Object
- .warn_talk_inline_nameless(source_filename) ⇒ Object
inline かつ name=off はアバター・左右・話者色のいずれも出ないため、 発話が地の文と区別できず誰の台詞か判別不能になる(§1.6)。.
- .warn_talk_missing_avatar(key, avatar, source_filename) ⇒ Object
- .warn_talk_missing_file(source_filename) ⇒ Object
--- 会話文の警告(§1.2・warning-messages-actionable: 修正例+出現位置を添える)---.
- .warn_talk_missing_speaker(line, source_filename) ⇒ Object
- .warn_talk_option_conflicts(options, avatar_requested, source_filename) ⇒ Object
組み合わせとして無意味・危険な指定を知らせる(§1.6)。.
- .warn_talk_orphan_continuation(line, source_filename) ⇒ Object
- .warn_talk_undefined_key(key, source_filename) ⇒ Object
- .warn_talk_unknown_option(token, source_filename) ⇒ Object
- .warn_talk_unknown_style(value, source_filename) ⇒ Object
Class Method Details
.accept_marker!(marker, stack, ol_queue, line:, source_filename:, after_blank: false) ⇒ Object
マーカー行を現在のリスト文脈で受理できるか判定し、受理時はスタック・キューを更新する。 戻り値: true(項目/新リスト)/ :split(空行を挟んだ様式変更=EOB で別リストに分ける)/ false(不受理。大文字+ピリオド 1 スペースの新規リスト等。呼び出し側が継続行または ブロック終端として扱う)。
873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 873 def accept_marker!(marker, stack, ol_queue, line:, source_filename:, after_blank: false) if stack.empty? open_fancy_list!(marker, stack, ol_queue) return true end # 浅いインデントへ戻ったら、そのレベルまで内側のリストを閉じる stack.pop while stack.size > 1 && marker.indent < stack.last[:indent] top = stack.last if marker.indent >= top[:indent] + 2 # 2 スペース以上深ければ入れ子の新規リスト return false unless acceptable_list_start?(marker) open_fancy_list!(marker, stack, ol_queue) return true end # 同一レベルで ul ⇔ ol が変われば新しいリストを開く(CommonMark と同じ分裂挙動) if marker.list_type != top[:list_type] return false unless acceptable_list_start?(marker) stack.pop open_fancy_list!(marker, stack, ol_queue) return true end # 同一レベル・同一種別: 様式一致なら次項目 if same_list_style?(marker, top) top[:counter] += 1 return true end return false unless acceptable_list_start?(marker) # 空行を挟んだトップレベルの様式変更は別リストの開始(Pandoc と同じ分裂挙動)。 # ネスト中は EOB マーカーが外側のリストまで閉じてしまうため分裂させない。 if after_blank && stack.size == 1 stack.pop open_fancy_list!(marker, stack, ol_queue) return :split end # 空行なしの様式変更は非サポート: 警告して先頭様式のまま続行(§2.2) warn_fancy_style_change(top, line, source_filename) top[:counter] += 1 true end
.acceptable_list_start?(marker) ⇒ Boolean
大文字様式+ピリオドのリスト開始行は空白 2 つ以上を要求する(
B. Russell誤爆防止・§2.2)797 798 799 800 801 802
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 797 def acceptable_list_start?(marker) return true unless marker.list_type == :ol && marker.separator == :period return true unless %i[upper_alpha upper_roman].include?(marker.style) marker.gap.length >= 2 end
.apply_talk_option(options, key, value, source_filename:) ⇒ Object
オプション 1 つを表示設定へ反映する(未知の値は 🟡 で現状維持)。
149 150 151 152 153 154 155 156 157 158 159 160 161 162
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 149 def apply_talk_option(, key, value, source_filename:) case key when 'style' style = value.strip.downcase.to_sym unless TalkRegistry::STYLES.include?(style) warn_talk_unknown_style(value, source_filename) return end .with(style:) when 'name' then .with(name: Common.truthy?(value)) when 'avatar' then .with(avatar: TalkRegistry.parse_avatar_mode(value)) when 'separator' then .with(separator: value) end end
.auto_talk_avatar_tag(char, source_filename:) ⇒ Object
自動生成した簡易アバターの
。生成物はワークスペース内実体のため asset_prefix を付けない(数式 SVG・showcase と同じ規約)。
307 308 309 310
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 307 def auto_talk_avatar_tag(char, source_filename:) src = TalkAvatarGenerator.generate(char, source_filename:) src ? image_talk_avatar_tag(src) : nil end
.build_ol_marker(indent_str, separator, token, gap, body) ⇒ Object
ol マーカーを組み立てる(トークンが様式に分類できなければ nil =マーカーでない)
765 766 767 768 769 770 771
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 765 def build_ol_marker(indent_str, separator, token, gap, body) style, value = classify_fancy_token(token) return nil unless style FancyMarker.new(indent: indent_str.length, list_type: :ol, style:, separator:, token:, value:, gap:, body: body.to_s) end
.classify_fancy_token(token) ⇒ Object
マーカー中身のトークンを [様式, 開始値] へ分類する(§2.2 の判定順)。 単文字の i / v / x 等はローマ数字を優先する(Pandoc 準拠。c 始まりの英字リストは書けない)。
775 776 777 778 779 780 781 782 783
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 775 def classify_fancy_token(token) case token when /\A\d{1,9}\z/ then [:decimal, token.to_i] when /\A[ivxlcdm]+\z/ then [:lower_roman, roman_to_int(token)] when /\A[IVXLCDM]+\z/ then [:upper_roman, roman_to_int(token)] when /\A[a-z]\z/ then [:lower_alpha, token.ord - 96] when /\A[A-Z]\z/ then [:upper_alpha, token.ord - 64] end end
.convert_book_card_inner_markdown(content) ⇒ Object
...の内側MarkdownをHTMLへ490 491 492 493 494 495 496 497 498
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 490 def convert_book_card_inner_markdown(content) content.gsub(%r{<div class="book-card">\n(.*?)\n</div>}m) do inner = ::Regexp.last_match(1) normalized = normalize_book_card_md(inner) html = MarkdownUtils.render_markdown_to_html(normalized) formatted = format_book_card_inner_html(html) "<div class=\"book-card\">\n#{formatted}\n</div>" end end
.convert_container_blocks(content, class_name:) ⇒ Object
::: ... 記法で囲まれたコンテナを div に変換。 コードブロック内の ::: 記法は変換対象外とする。
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 536 def convert_container_blocks(content, class_name:) opened_count = 0 closed_count = 0 # --- Phase: コードブロック退避 --- protected_text, spans = MarkdownUtils.extract_code_spans(content) pattern = /:::\s*\{\.([^}]+)\}\s*\n(.*?)\n:::\s*(?:\n|$)/m converted = protected_text.gsub(pattern) do raw_token_str = ::Regexp.last_match(1) inner = ::Regexp.last_match(2) raw_tokens = raw_token_str.split first_class = raw_tokens.first additional_tokens = raw_tokens.drop(1) additional_classes = additional_tokens.select { |t| t.start_with?('.') }.map { |c| c.delete_prefix('.') } param_tokens = additional_tokens.reject { |t| t.start_with?('.') } next ::Regexp.last_match(0) unless first_class == class_name || additional_classes.include?(class_name) opened_count += 1 closed_count += 1 all_classes = [first_class] + additional_classes class_attr = all_classes.join(' ') style_parts = [] param_tokens.each do |token| if (m_scale = token.match(/^scale=(.+)$/)) raw = m_scale[1].strip scale_percent = raw.end_with?('%') ? raw.to_f : raw.to_f * 100.0 scale_int = scale_percent.round style_parts << "--rotate-table-scale:#{scale_int}%;" end next unless (m_shift = token.match(/^shift-y=(.+)$/)) raw = m_shift[1].strip shift_percent = raw.end_with?('%') ? raw.to_f : raw.to_f * 100.0 shift_int = shift_percent.round sign = shift_int.negative? ? '' : '+' style_parts << "--rotate-table-shift-y:#{sign}#{shift_int}%;" end style_attr = style_parts.empty? ? '' : " style=\"#{style_parts.join(' ')}\"" "<div class=\"#{class_attr}\"#{style_attr}>\n#{inner}\n</div>\n\n" end # --- Phase: コードブロック復元 --- converted = MarkdownUtils.restore_code_spans(converted, spans) [converted, opened_count, closed_count] end
.convert_definition_lists(content) ⇒ Object
標準 Markdown(pandoc / Markdown Extra 風)の定義リスト記法を
- に変換する。
用語 ←
- : 説明 ←
- (複数並べれば複数
- ) 続き行 ← 直前
- の続き(半角スペース字下げ) VFM は定義リストに未対応なので、検出ブロックを Kramdown でレンダリングして
- を生成する(class は索引/奥付の
- と衝突させないため)。
著者は空行なしのコンパクトな形でも書け、内部でエントリ間に空行を補ってから
Kramdown に渡す。インラインコード `...` 等のインライン装飾は Kramdown が処理する。
コードフェンス(``` 可変長)内は対象外。
- ) 続き行 ← 直前
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 601 def convert_definition_lists(content) lines = content.lines code_lines = code_line_numbers(content) out = [] i = 0 while i < lines.size if code_lines.include?(i + 1) out << lines[i] i += 1 elsif definition_list_start?(lines, i) j = definition_list_end(lines, i) out << render_definition_list(lines[i...j].join) i = j else out << lines[i] i += 1 end end out.join end
.convert_fancy_lists(content, source_filename: nil) ⇒ Object
Pandoc fancy_lists 互換マーカー(A. / (a) / i. 等)を含むリストブロックを Kramdown で HTML 化し、
- へ様式クラス・type・start を付与して生 HTML として
インライン展開する(定義リスト convert_definition_lists と同方式)。
fancy マーカーを 1 つも含まないブロックはバイト一致で素通しし、標準リストは
VFM の機能(ルビ・脚注等)を維持する。コードフェンス/インラインコード内は対象外。
行頭 \ でエスケープされた fancy マーカー行は \ を除去して地の文にする。
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 726 def convert_fancy_lists(content, source_filename: nil) lines = content.lines code_lines = code_line_numbers(content) out = [] i = 0 while i < lines.size if code_lines.include?(i + 1) out << lines[i] i += 1 elsif fancy_block_start?(lines[i]) block_end, replacement = convert_list_block(lines, i, code_lines, source_filename:) out << replacement i = block_end else out << unescape_fancy_marker(lines[i]) i += 1 end end out.join end
.convert_list_block(lines, start, code_lines, source_filename:) ⇒ Object
start 行から始まるリストブロックを走査し、[終端 index(排他的), 置換文字列] を返す。 fancy マーカーを含まないブロックは原文のまま(バイト一致)返す。
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 812 def convert_list_block(lines, start, code_lines, source_filename:) # --- Phase: 走査(レベル追跡・マーカー受理・正規化行の生成)--- stack = [] # 開いているリスト(末尾が最内) ol_queue = [] # <ol> 出現順(=リスト開始イベント順)の属性キュー original = [] normalized = [] fancy_found = false pending_blank_at = nil j = start while j < lines.size break if code_lines.include?(j + 1) line = lines[j] if line.strip.empty? break unless pending_blank_at.nil? # 空行 2 連続はブロック終端 pending_blank_at = j j += 1 next end marker = parse_list_marker(line) accepted = marker && accept_marker!(marker, stack, ol_queue, line:, source_filename:, after_blank: !pending_blank_at.nil?) if accepted fancy_found ||= fancy_marker?(marker) # :split(空行を挟んだ様式変更=別リスト)は Kramdown の EOB マーカー ^ で # 前のリストを閉じ、連番どうしでも 1 つのルーズリストに併合されないようにする。 # ^ の前に空行を入れると直前のリストがルーズ化(li が <p> 包み)するため入れない。 normalized << (accepted == :split ? "^\n\n" : "\n") unless pending_blank_at.nil? normalized << normalized_marker_line(marker, stack) elsif stack.any? && line.match?(/\A {2,}\S/) # 字下げ継続行(2 スペース規則で受理されなかった入れ子マーカー含む)は現在項目の続き normalized << "\n" unless pending_blank_at.nil? normalized << normalized_continuation_line(line, stack) else break end original << lines[pending_blank_at] unless pending_blank_at.nil? original << line pending_blank_at = nil j += 1 end block_end = pending_blank_at || j return [block_end, original.join] unless fancy_found # --- Phase: Kramdown レンダリングと <ol> 属性パッチ --- html = MarkdownUtils.render_markdown_to_html(normalized.join).strip patched = patch_fancy_ol_attributes(html, ol_queue, source_filename:) return [block_end, original.join] if patched.nil? [block_end, "#{patched}\n\n"] end
.convert_standalone_spacing(content) ⇒ Object
単独行の aki / aki2 を縦余白マクロ @vspace に置換する。 aki は本来「段落末に付けて段落へ class を与える」インライン記法のため、 それ自身だけを 1 行に書くと(付与先の本文が無いので)VFM が "aki" を そのまま文字として出力してしまう。空行用途(
のように 1 行空ける)で 書かれた単独行は @vspace:1lh(aki2 は 2lh)へ置き換え、独立段落となるよう 直後に空行を補う。段落末に付いた aki は対象外(後処理の組み込み置換ルールでクラス化)。 コードフェンス内は対象外。1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1043 def convert_standalone_spacing(content) lines = content.lines code_lines = code_line_numbers(content) out = [] lines.each_with_index do |line, i| if code_lines.include?(i + 1) out << line next end m = line.strip.match(/\A\{\.(aki2?)\}\z/) prev_blank = out.empty? || out.last.strip.empty? if m && prev_blank out << "@vspace:#{m[1] == 'aki2' ? 2 : 1}lh\n" next_line = lines[i + 1] out << "\n" unless next_line.nil? || next_line.strip.empty? else out << line end end out.join end
.convert_talk_blocks(content, registry:, source_filename:) ⇒ Object
:::talk ブロックを会話文 HTML へ変換する。 発話内のインライン Markdown(強調・コード・リンク)は生 HTML 内では VFM が処理 しないため、book-card と同じく発話部分だけ Kramdown でインライン変換して埋め込む。
記法解説フェンス(```markdown 内の :::talk)を巻き込まないようコードスパンを退避する。
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 102 def convert_talk_blocks(content, registry:, source_filename:) # コードスパン退避で ```markdown フェンス内の :::{.talk} 作例を巻き込まないようにする。 # 退避にはインラインコード(`code`)も含まれるため、発話内インライン Markdown を # Kramdown へ渡す前に spans を使ってその発話ぶんだけ復元する(render_talk_body)。 text, spans = MarkdownUtils.extract_code_spans(content) return content unless text.match?(TALK_BLOCK_PATTERN) # talk.yml 不在のまま .talk を使っている場合は作成を促す。 warn_talk_missing_file(source_filename) unless registry.present? converted = text.gsub(TALK_BLOCK_PATTERN) do args = ::Regexp.last_match(1) body = ::Regexp.last_match(2) , extra_classes = (args, registry.display, source_filename:) render_talk_block(body, registry:, source_filename:, spans:, options:, extra_classes:) end MarkdownUtils.restore_code_spans(converted, spans) end
.convert_terminal_blocks(content) ⇒ Object
:::terminal を独自言語名のチルダフェンスへ書き換える。
.terminalは「端末の逐語転写」であり、中身は Markdown ではない。 素の Markdown として VFM に渡すと*.pngが斜体化し、`date`の バッククォートが消え、桁揃えの連続空白が HTML 生成の時点で失われ、---の行は<hr class="pagebreak">(改ページ)に化ける。 フェンス化してしまえば以降の前処理ステップは Masking.protect_code で 中身を退避し、VFM が HTML エスケープと空白保持を引き受ける。 専用のプレースホルダ機構を持たず既存のコード保護機構へ相乗りする形。記法解説フェンス(```markdown 内の :::terminal)は変換しない。
59 60 61 62 63 64 65 66 67 68 69
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 59 def convert_terminal_blocks(content) protected_text, spans = MarkdownUtils.extract_code_spans(content) converted = protected_text.gsub(TERMINAL_BLOCK_PATTERN) do body = ::Regexp.last_match(1) fence = '~' * terminal_fence_length(body) "\n#{fence}#{TERMINAL_FENCE_LANG}\n#{body}#{fence}\n\n" end MarkdownUtils.restore_code_spans(converted, spans) end
.definition_continuation_line?(line) ⇒ Boolean
継続行: 字下げされた非空行(直前の定義の続き)
640 641 642 643 644
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 640 def definition_continuation_line?(line) return false if line.to_s.strip.empty? line.to_s.start_with?(' ', "\t") end
.definition_def_line?(line) ⇒ Boolean
定義行: 行頭が「: 」(コロン+空白)で内容が続くもの
635 636 637
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 635 def definition_def_line?(line) line.to_s.match?(/\A:[ \t]+\S/) end
.definition_list_end(lines, idx) ⇒ Object
定義リストブロックの終端(排他的 index)を返す。 定義/継続/(定義が続く)用語/内部空行(ルーズ形式の区切り)を取り込む。
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 655 def definition_list_end(lines, idx) j = idx while j < lines.size line = lines[j] if definition_def_line?(line) || definition_continuation_line?(line) j += 1 elsif definition_term_line?(line) && definition_def_line?(lines[j + 1]) j += 1 elsif line.to_s.strip.empty? && definition_list_start?(lines, j + 1) j += 1 else break end end j end
.definition_list_start?(lines, idx) ⇒ Boolean
用語行の直後が定義行なら、定義リストの開始
647 648 649 650 651
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 647 def definition_list_start?(lines, idx) return false unless definition_term_line?(lines[idx]) definition_def_line?(lines[idx + 1]) end
.definition_term_line?(line) ⇒ Boolean
用語行: 行頭から始まる非空行で、定義行(: )・継続行(字下げ)・他のブロック構文でないもの
623 624 625 626 627 628 629 630 631 632
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 623 def definition_term_line?(line) s = line.to_s.chomp return false if s.strip.empty? return false if s.start_with?(' ', "\t") # 字下げ=継続行 return false if s.match?(/\A:[ \t]/) # 定義行 # 見出し / 引用 / 表 / コンテナ / フェンス / 生HTML / 箇条書き・番号リストは用語にしない return false if s.match?(%r{\A(\#|>|\||:::|```|<|[-*+][ \t]|\d+[.)][ \t])}) true end
.escape_html(str) ⇒ Object
生 HTML 属性・テキストへ差し込む文字列を最小エスケープする。
336 337 338
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 336 def escape_html(str) str.to_s.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"') end
.escape_inline_code_html(line) ⇒ Object
インラインコード内の HTML 予約文字をエスケープする
1098 1099 1100
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1098 def escape_inline_code_html(line) MarkdownUtils.escape_inline_code_html(line) end
.extract_line_range(lines, start_line, end_line) ⇒ Object
content 内の各 include 記法マッチ文字列 → 行番号のマップを構築する 1 始まりの行範囲 [start_line, end_line] の行配列を返す。 終了行がファイル末尾を超える場合はファイル末尾までにクランプする(開始が有効な限り取り込む)。 開始行がファイル末尾を超える・開始 < 1・逆順(end < start)のように救済不能な場合は nil を返し、 呼び出し側は全文取り込みへフォールバックする(lines は範囲外で nil を返し
nil.joinで落ちるため、ここで明示的に弾く)。1220 1221 1222 1223 1224 1225 1226
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1220 def extract_line_range(lines, start_line, end_line) return nil if start_line < 1 || end_line < start_line return nil if start_line > lines.size effective_end = [end_line, lines.size].min lines[(start_line - 1)..(effective_end - 1)] end
.fancy_block_start?(line) ⇒ Boolean
リストブロックの開始行か(インデント 0-3 のマーカー行)
805 806 807 808
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 805 def fancy_block_start?(line) marker = parse_list_marker(line) !marker.nil? && marker.indent <= 3 && acceptable_list_start?(marker) end
.fancy_list_class(style, separator) ⇒ Object
出力
- のクラス名(§2.1 対応表のセル値)
1007 1008 1009
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1007 def fancy_list_class(style, separator) "vs-list-#{FANCY_STYLE_CSS.fetch(style)}#{FANCY_SEPARATOR_SUFFIX.fetch(separator)}" end
.fancy_marker?(marker) ⇒ Boolean
fancy マーカーか(標準=数字+ピリオド/片括弧。ul は fancy でない)
792 793 794
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 792 def fancy_marker?(marker) marker.list_type == :ol && (marker.separator == :paren2 || marker.style != :decimal) end
.fancy_marker_literal(marker) ⇒ Object
マーカーの原文表記(警告メッセージの修正例に使う)
946 947 948 949 950 951 952 953
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 946 def fancy_marker_literal(marker) case marker.separator when :paren2 then "(#{marker.token})" when :paren then "#{marker.token})" when :period then "#{marker.token}." else '-' end end
.fancy_ol_attributes(entry) ⇒ Object
1 つの
- に付与する属性のリスト。標準様式(style nil)は start 以外無加工。
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 991 def fancy_ol_attributes(entry) style, separator, start = entry.values_at(:style, :separator, :start) attrs = [] if style attrs << %(class="vs-fancy-list #{fancy_list_class(style, separator)}") type = FANCY_TYPE_ATTR[style] attrs << %(type="#{type}") if type end attrs << %(start="#{start}") if start != 1 # 括弧付き様式は CSS カウンタで自前描画するため、開始値を counter-reset のインライン指定で # 常に与える(start 属性からカウンタ開始値を読む標準手段が無い・§4.1-5) attrs << %(style="counter-reset: vs-fancy #{start - 1}") if style && separator != :period attrs end
.format_book_card_inner_html(inner_html) ⇒ Object
book-card の内側を整形
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 501 def format_book_card_inner_html(inner_html) html = inner_html.to_s.strip img_match = html.match(/<img[^>]*>/i) return inner_html unless img_match img_tag = img_match[0].gsub(%r{\s*/?>}i) { '>' } if html.sub!(%r{<p>\s*#{Regexp.escape(img_match[0])}\s*</p>}i, '') # removed wrapped <p> with img else html.sub!(img_match[0], '') end title_match = html.match(%r{<p>\s*<strong>(.*?)</strong>\s*</p>}im) return inner_html unless title_match title_text = title_match[1].strip html.sub!(title_match[0], '') description_html = html.strip parts = [] parts << " #{img_tag}" parts << ' <div class="book-info">' parts << " <p class=\"book-title\">#{title_text}</p>" parts << ' <div class="book-description">' parts << " #{description_html}" parts << ' </div>' parts << ' </div>' parts.join("\n") end
.hard_break_line(line) ⇒ Object
非空行の末尾を Markdown のハード改行(半角スペース2つ)へ正規化する。 既存の末尾空白は一度除いてから2つに揃えるため冪等。空行はそのまま返す。
690 691 692 693 694
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 690 def hard_break_line(line) return line if line.strip.empty? "#{line.chomp.sub(/[ \t]+\z/, '')} \n" end
.image_talk_avatar_tag(src) ⇒ Object
著者が用意した画像の
。 src には asset_prefix を前置する。アバターは images/ 配下の著者資産であり、 ワークスペース内に実体を持つ生成物(数式 SVG・showcase・mermaid)とは違って pdf/ ミラーへコピーされないため、prefix 無しの相対では PDF 側で解決できない (ImagePathNormalizer が通常画像へ付けるのと同じ規約)。EPUB では EpubBuilder が prefix を剥がし、localize_assets! が images/ を同梱するのでそのまま解決する。
301 302 303
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 301 def image_talk_avatar_tag(src) "<img class=\"talk-icon\" src=\"#{escape_html(src)}\" alt=\"\" />" end
.inject_talk_prefix(body, prefix) ⇒ Object
レンダリング済みの発話 HTML の先頭
の内側へ名前+区切りを差し込む。
で始まらない出力(リスト等)になった場合は段落を前置してフォールバックする。
266 267 268 269 270
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 266 def inject_talk_prefix(body, prefix) return body.sub(/\A<p([^>]*)>/) { "<p#{::Regexp.last_match(1)}>#{prefix}" } if body.match?(/\A<p[ >]/) "<p>#{prefix}</p>\n#{body}" end
.normalize_book_card_md(md_text) ⇒ Object
book-card 内のMarkdownを事前整形
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 464 def normalize_book_card_md(md_text) lines = md_text.to_s.split(/\r?\n/, -1) out = [] lines.each_with_index do |line, i| next_line = lines[i + 1] next_nonblank = next_line && next_line.strip != '' is_image = line.match?(/^\s*!\[[^\]]*\]\([^)]+\)\s*$/) is_title = line.match?(/^\s*\*\*[^*].*\*\*\s*$/) if (is_image || is_title) && next_nonblank # 画像・タイトルの後は段落を分ける(独立した <p> にする) out << line out << '' elsif !line.strip.empty? && next_nonblank # 説明部の連続行(著者・説明など)は本書全体の hardLineBreaks に合わせて # 行ごとに改行させる。Kramdown のソフト改行はそのままだと半角空白へ潰れて # 1 行に連結されるため、Markdown のハード改行(末尾2スペース)を補って <br> 化する。 out << "#{line.rstrip} " else out << line end end out.join("\n") end
.normalize_container_fence_spacing(content) ⇒ Object
:::class コンテナの開始行直後・終了行直前に空行を補い、開始/終了行を独立段落にする。 VFM は hardLineBreaks のため、空行が無いと開始 ":::output" 行を直後の本文行と 1 つの
に結合してしまう。その状態で後処理の置換ルールが ":::class" を
へ 置換するとが分割され、先頭の本文(クロスリファレンスのキャプション 等)が コンテナ直下に取り残されて wrap されず、見出しが素の強調表示になる不具合が起きる。 開始直後・終了直前に空行を挟むことで、内側の各ブロックが独立段落になり正しく整形される。 コードフェンス内の ::: は対象外。既に空行がある場合は二重に入れない。
1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1072 def normalize_container_fence_spacing(content) lines = content.lines code_lines = code_line_numbers(content) out = [] lines.each_with_index do |line, i| if code_lines.include?(i + 1) out << line next end stripped = line.lstrip if stripped.match?(/\A:{3,}\s*\{/) # 開始 :::{.class} out << line nxt = lines[i + 1] out << "\n" if nxt && !nxt.strip.empty? elsif line.strip.match?(/\A:{3,}\z/) # 終了 ::: out << "\n" unless out.empty? || out.last.strip.empty? out << line else out << line end end out.join end
.normalized_continuation_line(line, stack) ⇒ Object
継続行を現在項目の本文開始位置(マーカー行の字下げ + 4 スペース)へ揃える
965 966 967
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 965 def normalized_continuation_line(line, stack) hard_break_line("#{' ' * ((4 * stack.last[:depth]) + 4)}#{line.strip}\n") end
.normalized_marker_line(marker, stack) ⇒ Object
マーカー行を「4 スペース/レベルの字下げ+連番 1. 2. …(ul は -)」へ正規化する。 開始値はここでは反映せず、patch_fancy_ol_attributes が start 属性で与える(§4.1-3)。 hard_break_line は本書全体の hardLineBreaks: true と改行挙動を揃える措置(定義リストと同じ)。
958 959 960 961 962
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 958 def normalized_marker_line(marker, stack) top = stack.last head = top[:list_type] == :ul ? '-' : "#{top[:counter]}." hard_break_line("#{' ' * (4 * top[:depth])}#{head} #{marker.body}\n") end
.open_fancy_list!(marker, stack, ol_queue) ⇒ Object
新しいリストを開く(スタックへ push・ol なら属性キューへ登録)
932 933 934 935 936 937 938 939 940 941 942 943
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 932 def open_fancy_list!(marker, stack, ol_queue) fancy = fancy_marker?(marker) entry = { indent: marker.indent, depth: stack.size, list_type: marker.list_type, style: fancy ? marker.style : nil, separator: fancy ? marker.separator : nil, start: marker.value || 1, counter: 1, head_literal: fancy_marker_literal(marker), warned: false } stack.push(entry) ol_queue << entry if marker.list_type == :ol end
.parse_list_marker(line) ⇒ Object
行をリストマーカーとして解析する(マーカーでなければ nil)。
aa.のような複数英字は様式に該当せずマーカーにしない(§2.2-4)。749 750 751 752 753 754 755 756 757 758 759 760 761 762
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 749 def parse_list_marker(line) s = line.chomp if (m = s.match(/\A( *)([-*+])([ \t]+)(\S.*)?\z/)) return FancyMarker.new(indent: m[1].length, list_type: :ul, style: nil, separator: nil, token: nil, value: nil, gap: m[3], body: m[4].to_s) end if (m = s.match(/\A( *)\(([0-9a-zA-Z]{1,9})\)([ \t]+)(\S.*)?\z/)) return build_ol_marker(m[1], :paren2, m[2], m[3], m[4]) end if (m = s.match(/\A( *)([0-9a-zA-Z]{1,9})([.)])([ \t]+)(\S.*)?\z/)) return build_ol_marker(m[1], m[3] == '.' ? :period : :paren, m[2], m[4], m[5]) end nil end
.parse_talk_options(args, display, source_filename:) ⇒ Array(TalkRegistry::TalkDisplay, Array<String>)
ブロック引数(
style=inline name=off .foo)を解析し、talk.yml の表示設定へ重ねる。123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 123 def (args, display, source_filename:) = display extra_classes = [] avatar_requested = false args.to_s.split.each do |token| if token.start_with?('.') extra_classes << token.delete_prefix('.') next end key, value = token.split('=', 2) unless TALK_OPTION_KEYS.include?(key) && value warn_talk_unknown_option(token, source_filename) next end avatar_requested = true if key == 'avatar' && TalkRegistry.parse_avatar_mode(value) != :off = apply_talk_option(, key, value, source_filename:) end warn_talk_option_conflicts(, avatar_requested, source_filename) [, extra_classes] end
.parse_talk_utterances(body, source_filename:) ⇒ Object
ブロック本文を発話(lines)の配列へ。空行は無視、字下げ行は継続、 「キー:」に一致しない非字下げ行は 🔴(話者キーなし)。
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 189 def parse_talk_utterances(body, source_filename:) utterances = [] body.each_line do |raw| line = raw.chomp next if line.strip.empty? if line.start_with?(' ', "\t") if utterances.empty? warn_talk_orphan_continuation(line, source_filename) else utterances.last[:lines] << line.strip end elsif (m = line.match(TALK_SPEAKER_PATTERN)) first = m[2].to_s.strip utterances << { key: m[1], lines: first.empty? ? [] : [first] } else warn_talk_missing_speaker(line, source_filename) end end utterances end
.patch_fancy_ol_attributes(html, ol_queue, source_filename:) ⇒ Object
生成 HTML の
- (出現順=ソース順)へ様式クラス・type・start・counter-reset を注入する。
キューと
- 出現数が食い違ったら nil を返し、呼び出し側は原文のまま埋め戻す
(防御・ビルドは止めない)。
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 972 def patch_fancy_ol_attributes(html, ol_queue, source_filename:) occurrences = html.scan('<ol>').size unless occurrences == ol_queue.size where = source_filename ? "#{source_filename}: " : '' Common.log_warn( "#{where}fancy list の <ol> 対応付けに失敗したため、このブロックは変換せずそのまま出力します" \ "(検出 #{ol_queue.size} 件 / 生成 #{occurrences} 件)" ) return nil end queue = ol_queue.dup html.gsub('<ol>') do attrs = fancy_ol_attributes(queue.shift) attrs.empty? ? '<ol>' : "<ol #{attrs.join(' ')}>" end end
.process_code_include(content, source_filename: nil, source_path: nil) ⇒ Object
include:path[:start-end]を検出し、codes/ または絶対パスから読込。 マークダウンのコードブロックおよびインラインコード内に記述された include 記法は記法の説明例であるためスキップする。1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1108 def process_code_include(content, source_filename: nil, source_path: nil) matches_found = 0 line_number_map = build_line_number_map(content) occurrence = Hash.new(0) skippable_lines = lines_inside_code_blocks(content) inline_code_lines = lines_with_inline_code_include(content) source_line_map = build_source_include_line_map(source_path) content.gsub!(/```include:([^:`\s]+)(?::(\d+)-(\d+))?\s*```/) do |match| # 同一 include 文字列が複数回現れても行番号を取り違えないよう、出現順に # 対応する行番号を消費する(例: 記法説明フェンス内の例文と、:::{.output} 等に # 置いた本物の include が同一文字列のとき、前者の行番号で後者まで誤スキップするのを防ぐ)。 # ln=前処理後コンテンツ内の行(スキップ判定用)、report_ln=著者向けに原稿ファイルの行を優先。 idx = occurrence[match] occurrence[match] += 1 ln = line_number_map[match][idx] report_ln = source_line_map[match][idx] || ln # コードブロック内の include 記法はスキップ(記法説明用の例文) next match if ln && skippable_lines.include?(ln) # インラインコード内の include 記法はスキップ # `` ```include:file.rb``` `` のようにバッククォートで囲まれた場合 next match if ln && inline_code_lines.include?(ln) matches_found += 1 original_path = ::Regexp.last_match(1) start_line = ::Regexp.last_match(2)&.to_i end_line = ::Regexp.last_match(3)&.to_i Common.log_action("マッチ発見: #{match.strip}") Common.log_info("元のパス: #{original_path}") file_path = if original_path.start_with?('/') original_path else File.join(Common::CODES_DIR, original_path) end Common.log_info("解決されたパス: #{file_path}") if File.exist?(file_path) source_content = File.read(file_path) lines = source_content.lines # 範囲 include が成立したときだけ #L<開始>-L<実効終了> マーカーをフェンス情報文字列へ # 付与し、post_process(prism_lines)が元ファイルの行番号で採番できるようにする。 # 全文取り込み(範囲指定なし・救済不能フォールバック)ではマーカーを付けない。 line_marker = '' code_content = if start_line && end_line selected_lines = extract_line_range(lines, start_line, end_line) warn_prefix = "#{source_filename || '(不明)'}#{report_ln ? ":#{report_ln}" : ''} - " if selected_lines.nil? # 開始行がファイル末尾を超える/逆順など、救済不能な不正指定。全文取り込みへフォールバック。 Common.log_warn( "#{warn_prefix}include の範囲指定が不正です" \ "(#{original_path} は #{lines.size} 行、指定は #{start_line}-#{end_line})。全文を取り込みます。" ) "#{source_content}\n" else # 開始は有効で終了だけがファイル末尾を超える場合は、末尾までにクランプして取り込む。 if end_line > lines.size Common.log_warn( "#{warn_prefix}include の終了行がファイル末尾を超えています" \ "(#{original_path} は #{lines.size} 行、指定は #{start_line}-#{end_line})。" \ "#{start_line}-#{lines.size} 行を取り込みます。" ) end # 終了行は extract_line_range のクランプと同値の実効値を書く(GitHub パーマリンク風)。 line_marker = "#L#{start_line}-L#{[end_line, lines.size].min}" selected_lines.join end else "#{source_content}\n" end language = MarkdownUtils.detect_language(file_path) replacement = "```#{language}:#{original_path}#{line_marker}\n#{code_content}```" Common.log_success("置換完了: #{original_path} (#{language})") replacement else code_name = File.basename(original_path) # 元ファイルの行番号があればそちらを使う source_ln = report_ln if source_filename && source_ln Common.log_error( "#{source_filename}:#{source_ln} - ソースコード '#{code_name}' が見つかりません", detail: "コードの場所: #{file_path}" ) LinkImageValidator.record_code_include_error(source_filename, source_ln, code_name) else Common.log_error( "ソースコード '#{code_name}' が見つかりません", detail: "コードの場所: #{file_path}" ) LinkImageValidator.record_code_include_error(source_filename || '(不明)', 0, code_name) end match end end Common.log_info("#{matches_found}個のinclude記法を処理") if matches_found.positive? content end
.render_definition_list(block) ⇒ Object
定義リストブロックを Kramdown で
- 化する。
Kramdown はエントリ間に空行を要求するため、用語行の前へ空行を補ってから渡す。
また本書全体の hardLineBreaks: true(改行=
)に揃えるため、説明(dd)内の 各行末へ Markdown のハード改行(半角スペース2つ)を補い、複数行の説明が
で改行されるようにする(空行=エントリ区切りはそのまま残す)。677 678 679 680 681 682 683 684 685 686
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 677 def render_definition_list(block) normalized = [] block.lines.each do |line| normalized << "\n" if definition_term_line?(line) && !normalized.empty? && !normalized.last.strip.empty? normalized << hard_break_line(line) end html = MarkdownUtils.render_markdown_to_html(normalized.join).strip html = html.sub(/\A<dl>/, '<dl class="def-list">') "#{html}\n\n" end
.render_talk_block(body, registry:, source_filename:, spans:, options:, extra_classes:) ⇒ Object
ブロック 1 つ分を
…へ。発話が 1 つも無ければ空文字。 区切り文字は data-talk-sep として容器に持たせる——Kindle 劣化(EpubBuilder)が talk.yml を読み直さずに inline 形式へ組み替えられるようにするため(§2.3)。175 176 177 178 179 180 181 182 183 184 185
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 175 def render_talk_block(body, registry:, source_filename:, spans:, options:, extra_classes:) utterances = parse_talk_utterances(body, source_filename:) return '' if utterances.empty? items = utterances.map do render_talk_item(it, registry:, source_filename:, spans:, options:) end classes = ['talk', "talk-style-#{.style}", *extra_classes].join(' ') "\n\n<div class=\"#{classes}\" data-talk-sep=\"#{escape_html(.separator)}\">\n" \ "#{items.join("\n")}\n</div>\n\n" end
.render_talk_body(lines, spans:) ⇒ Object
発話本文(継続行を含む)を単一段落 HTML へ。継続行は本書の hardLineBreaks に揃え
化する。 インラインコード等は退避済みのため、Kramdown へ渡す前に spans でこの発話ぶんを復元する。327 328 329 330 331 332 333
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 327 def render_talk_body(lines, spans:) return '<p></p>' if lines.empty? md = MarkdownUtils.restore_code_spans(lines.join(" \n"), spans) html = MarkdownUtils.render_markdown_to_html(md).strip html.empty? ? '<p></p>' : html end
.render_talk_item(utterance, registry:, source_filename:, spans:, options:) ⇒ Object
発話 1 つを .talk-item へ。未定義キーは 🔴 で追記例を提示し、 表示名=キー・色なしのフォールバックで組む(ビルドは止めない)。
213 214 215 216 217 218 219 220 221 222 223 224 225
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 213 def render_talk_item(utterance, registry:, source_filename:, spans:, options:) key = utterance[:key] char = registry[key] warn_talk_undefined_key(key, source_filename) unless char name = char&.name || key body = render_talk_body(utterance[:lines], spans:) if .style == :inline render_talk_item_inline(key, name, body, options:) else render_talk_item_chat(key, char, name, body, options:, source_filename:) end end
.render_talk_item_chat(key, char, name, body, options:, source_filename:) ⇒ Object
chat: 話者の見せ方はアバターの有無で変わる。 アバターあり … 吹き出しの外側に話者列を立て、アバターの下へ名前を積む アバターなし … 名前だけの列は幅を持て余すため、吹き出し上部のラベルにする name=off でも .talk-name は出力し talk-name-off で隠す——Kindle 劣化が この要素を段落内へ移して話者名を見せるため、DOM から消してはならない(§2.3)。
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 232 def render_talk_item_chat(key, char, name, body, options:, source_filename:) side = char&.side || 'left' avatar = talk_avatar_tag(char, options:, source_filename:) name_span = "<span class=\"#{talk_name_classes()}\">#{escape_html(name)}</span>" parts = ["<div class=\"talk-item talk-#{side} talk-c-#{key}\">"] if avatar parts << '<div class="talk-speaker">' parts << avatar parts << name_span parts << '</div>' end parts << '<div class="talk-body">' parts << name_span unless avatar parts << body parts << '</div>' parts << '</div>' parts.join("\n") end
.render_talk_item_inline(key, name, body, options:) ⇒ Object
inline: 「名前+区切り+発話」を 1 段落に収める。アバターと左右振り分けは行わない。
253 254 255 256 257 258 259
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 253 def render_talk_item_inline(key, name, body, options:) klass = talk_name_classes() prefix = "<span class=\"#{klass}\">#{escape_html(name)}</span>" \ "<span class=\"talk-sep#{.name ? '' : ' talk-name-off'}\">" \ "#{escape_html(.separator)}</span>" "<div class=\"talk-item talk-c-#{key}\">\n#{inject_talk_prefix(body, prefix)}\n</div>" end
.resolve_talk_avatar(avatar) ⇒ Object
avatar 指定から images/characters/ 配下の実在ファイル名を解決する(無ければ nil)。 著者が書いた拡張子をまず尊重し、次に .webp/.png/.jpg/.jpeg の変種を探す。 ImagePathNormalizer.image_exists_for? は「正規化済み .webp パス」を前提に .webp だけを 剥がすため、
avatar: a.pngを渡すとa.png.webp等を探して実在する a.png を取りこぼし、 逆にavatar: a.webp(実体は a.png)では実在しない .webp を src に出してしまう。 ここは著者の生の指定を受けるので、常に「実在するファイル」を src にする。318 319 320 321 322 323
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 318 def resolve_talk_avatar(avatar) base = avatar.sub(/\.[A-Za-z0-9]+\z/, '') [avatar, *TALK_AVATAR_EXTENSIONS.map { "#{base}#{it}" }] .uniq .find { File.exist?(File.join(Common::IMAGES_DIR, 'characters', it)) } end
.roman_to_int(token) ⇒ Object
ローマ数字文字列を整数へ(iv → 4)。減算則: 次の桁より小さい桁は引く。
786 787 788 789
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 786 def roman_to_int(token) digits = token.downcase.chars.map { ROMAN_DIGIT_VALUES.fetch(it) } digits.each_cons(2).sum { |a, b| a < b ? -a : a } + digits.last end
.same_list_style?(marker, top) ⇒ Boolean
マーカーが現在のリストの様式(先頭項目で確定)と一致するか
921 922 923 924 925 926 927 928 929
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 921 def same_list_style?(marker, top) return true if marker.list_type == :ul if fancy_marker?(marker) marker.style == top[:style] && marker.separator == top[:separator] else top[:style].nil? end end
.talk_avatar_tag(char, options:, source_filename:) ⇒ Object
アバター
(吹き出し外側の丸抜き)。出せない場合は nil を返す(ビルドは止めない)。 解決順は talk-auto-avatar-spec.md §1.3:
1. avatar=off なら何もしない 2. 話者が画像を指定 → その画像。見つからなければ 🟡(auto なら自動生成へ落とす) 3. 話者が `auto`、または avatar=auto かつ話者の指定なし → 簡易アバターを自動生成280 281 282 283 284 285 286 287 288 289 290 291 292 293
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 280 def talk_avatar_tag(char, options:, source_filename:) return nil if char.nil? || .avatar == :off spec = char.avatar.to_s.strip auto = .avatar == :auto return auto_talk_avatar_tag(char, source_filename:) if spec.casecmp?(TalkRegistry::AVATAR_AUTO) return auto ? auto_talk_avatar_tag(char, source_filename:) : nil if spec.empty? resolved = resolve_talk_avatar(spec) return image_talk_avatar_tag("#{Common.asset_prefix}images/characters/#{resolved}") if resolved warn_talk_missing_avatar(char.key, spec, source_filename) auto ? auto_talk_avatar_tag(char, source_filename:) : nil end
.talk_name_classes(options) ⇒ Object
話者名 span のクラス(name=off は隠す。Kindle 劣化時にこのクラスを外す)。
262
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 262 def talk_name_classes() = .name ? 'talk-name' : 'talk-name talk-name-off'
.terminal_fence_length(body) ⇒ Object
フェンスの長さ。本文に
~~~が現れても閉じ位置がずれないよう、 本文中の最長のチルダ連続 + 1 と 3 の大きい方を採る。73 74 75 76
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 73 def terminal_fence_length(body) longest = body.to_s.scan(/~+/).map(&:length).max.to_i [3, longest + 1].max end
.transform_links_to_footnotes(md_text) ⇒ Object
Markdown内のリンク記法を脚注化
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 419 def transform_links_to_footnotes(md_text) original = md_text.to_s text, code_spans = MarkdownUtils.extract_code_spans(original) max_n = 0 text.scan(/\[\^url(\d+)\]:/).each do |m| n = m[0].to_i max_n = n if n > max_n end url_id = {} replacements = [] replaced = text.gsub(/(?<!!)\[(.+?)\]\((https?:[^\s)]+)\)(?!\s*\[^url\d+\])/) do |_match| label = ::Regexp.last_match(1) url = ::Regexp.last_match(2) id = (url_id[url] ||= begin max_n += 1 "url#{max_n}" end) replacements << [id, url] "[#{label}](#{url}) [^#{id}]" end existing_defs = {} text.scan(/\[\^(url\d+)\]:\s*(\S+)/) { |id, u| existing_defs[id] = u } new_defs = url_id.filter_map do |u, id| next nil if existing_defs.key?(id) "[^#{id}]: #{u}" end result = if new_defs.empty? replaced elsif replaced.strip.end_with?("\n") "#{replaced}\n#{new_defs.join("\n")}\n" else "#{replaced}\n\n#{new_defs.join("\n")}\n" end MarkdownUtils.restore_code_spans(result, code_spans) end
.unescape_fancy_marker(line) ⇒ Object
行頭マーカーの \ エスケープ((1) 等)を解除して地の文にする(§2.2)。 fancy マーカーの形をした行だけが対象(標準リスト・ul の \ は VFM に委ねる)。
1027 1028 1029 1030 1031 1032 1033 1034
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1027 def unescape_fancy_marker(line) m = line.match(/\A( *)\\(\S.*)\z/m) return line unless m candidate = "#{m[1]}#{m[2]}" marker = parse_list_marker(candidate) marker && fancy_marker?(marker) ? candidate : line end
.warn_fancy_style_change(top, line, source_filename) ⇒ Object
同一リスト途中の様式変更を警告する(リストごとに 1 回・先頭様式で続行)
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 1012 def warn_fancy_style_change(top, line, source_filename) return if top[:warned] top[:warned] = true where = source_filename ? "#{source_filename}: " : '' Common.log_warn( "#{where}リストの途中でマーカー様式が変わっています。" \ "先頭項目の様式(#{top[:head_literal]})のまま採番を続行します", detail: "該当行: #{line.strip} → 修正例: マーカーを #{top[:head_literal]} と同じ様式に揃えるか、" \ '空行を挟んで別のリストに分けてください' ) end
.warn_talk_inline_avatar(source_filename) ⇒ Object
365 366 367 368 369 370
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 365 def warn_talk_inline_avatar(source_filename) Common.log_warn( "#{source_filename}: style=inline ではアバターを表示しません(avatar=on の指定は無視されます)", detail: '→ アバターを見せたい場合は style=chat にしてください' ) end
.warn_talk_inline_nameless(source_filename) ⇒ Object
inline かつ name=off はアバター・左右・話者色のいずれも出ないため、 発話が地の文と区別できず誰の台詞か判別不能になる(§1.6)。
374 375 376 377 378 379 380
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 374 def warn_talk_inline_nameless(source_filename) Common.log_warn( "#{source_filename}: style=inline かつ name=off では話者を判別できません", detail: "→ name=on に戻すか、style=chat にしてください\n" \ '→ inline は話者名だけが手がかりのため、名前を消すと地の文と区別が付きません' ) end
.warn_talk_missing_avatar(key, avatar, source_filename) ⇒ Object
410 411 412 413 414 415 416
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 410 def warn_talk_missing_avatar(key, avatar, source_filename) Common.log_warn( "#{source_filename}: 話者 '#{key}' のアバター images/characters/#{avatar} が見つかりません。" \ 'アバターなしで表示します', detail: '→ images/characters/ に画像を置くか、talk.yml の avatar 指定を見直してください' ) end
.warn_talk_missing_file(source_filename) ⇒ Object
--- 会話文の警告(§1.2・warning-messages-actionable: 修正例+出現位置を添える)---
342 343 344 345 346 347 348 349
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 342 def warn_talk_missing_file(source_filename) Common.log_error( "#{source_filename}: 会話文(:::{.talk})が使われていますが config/talk.yml がありません", detail: "→ config/talk.yml を作成し、話者を定義してください(例):\n" \ " sensei: indigo # 色だけの簡易形\n" \ ' hanako: teal' ) end
.warn_talk_missing_speaker(line, source_filename) ⇒ Object
382 383 384 385 386 387 388
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 382 def warn_talk_missing_speaker(line, source_filename) Common.log_error( %(#{source_filename}: 会話文に話者キーがありません: "#{line}"), detail: "→ 各行は「キー: 発話」で書きます(例: sensei: こんにちは)\n" \ '→ 前の発話の続きなら行頭を空白で字下げしてください' ) end
.warn_talk_option_conflicts(options, avatar_requested, source_filename) ⇒ Object
組み合わせとして無意味・危険な指定を知らせる(§1.6)。
165 166 167 168 169 170
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 165 def warn_talk_option_conflicts(, avatar_requested, source_filename) return unless .style == :inline warn_talk_inline_avatar(source_filename) if avatar_requested warn_talk_inline_nameless(source_filename) unless .name end
.warn_talk_orphan_continuation(line, source_filename) ⇒ Object
390 391 392 393 394 395
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 390 def warn_talk_orphan_continuation(line, source_filename) Common.log_error( %(#{source_filename}: 継続行の前に発話がありません: "#{line.strip}"), detail: '→ 継続行(字下げ行)の前に「キー: 発話」の行が必要です' ) end
.warn_talk_undefined_key(key, source_filename) ⇒ Object
397 398 399 400 401 402 403 404 405 406 407 408
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 397 def warn_talk_undefined_key(key, source_filename) Common.log_error( "#{source_filename}: 未定義の話者キー '#{key}' が使われています", detail: "→ config/talk.yml に追記してください:\n" \ " #{key}: indigo # 色だけの簡易形\n" \ " または詳細形:\n" \ " #{key}:\n" \ " name: 表示名\n" \ " color: indigo\n" \ " avatar: #{key}.webp" ) end
.warn_talk_unknown_option(token, source_filename) ⇒ Object
351 352 353 354 355 356
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 351 def warn_talk_unknown_option(token, source_filename) Common.log_warn( %(#{source_filename}: 会話文の不明な指定です: "#{token}"), detail: "→ 指定できるのは #{TALK_OPTION_KEYS.join(' / ')} です(例: :::{.talk style=inline name=off})" ) end
.warn_talk_unknown_style(value, source_filename) ⇒ Object
358 359 360 361 362 363
# File 'lib/vivlio_starter/cli/pre_process/markdown_transformer.rb', line 358 def warn_talk_unknown_style(value, source_filename) Common.log_warn( "#{source_filename}: 会話文の style '#{value}' は不明な表示形式です。既定のまま続行します", detail: "→ 指定できるのは #{TalkRegistry::STYLES.join(' / ')} です" ) end
- .convert_container_blocks(content, class_name:) ⇒ Object