Module: VivlioStarter::CLI::Masking

Defined in:
lib/vivlio_starter/cli/masking.rb

Overview

Markdown のコード領域解釈の唯一実装。

Constant Summary collapse

FENCE =

行頭フェンス(``` または ~~~ を 3 連以上)のマーカー。

/\A(?:`{3,}|~{3,})/
BLOCKQUOTE_PREFIX =

引用(>)の行頭記号。CommonMark はブロック引用の中のフェンスドコード ブロックを認めており、GitHub 等も描画する。剥がさないと > ```html が 地の文と見なされ、索引付与などがコード内へ食い込む(実測: 引用で示した AI との対話例のコードに <span class="index-term"> が注入された)。 入れ子引用(> >)と、> 前の 3 文字までの字下げを許す。

/\A[ ]{0,3}(?:>[ ]?)+/
CODE_SPAN_PLACEHOLDER_PREFIX =

protect_code が用いるプレースホルダの接頭辞(既存 MarkdownUtils と互換)。

'__VS_CODE_SPAN__'
INLINE_CODE_SPAN =

インラインコードスパン(N 個の連続バッククォート同士の対)。 (?<!) / (?!) で開き・閉じの両端が「ちょうど N 個のラン」であることを担保し、 foo`bar のように内部にバッククォートを含むケースも 1 スパンとして保護する。

/(?<!`)(`+)(?!`).+?(?<!`)\1(?!`)/m
HTML_INLINE_CODE =

HTML 化されたインラインコード()。 前処理が表やコンテナを生 HTML へ変換したあとはバッククォートが残らないため、 INLINE_CODE_SPAN では保護できない——索引スキャンは変換のに走るので、 早見表のセルに書いた [用語|読み] が裸のマークアップに見えてしまう (index-html-code-protection-spec.md §1)。属性つきも受ける。

非貪欲であること。 貪欲だと 1 行に複数ある の最初の開始から 最後の終了までを 1 つと見なし、間の地の文まで保護して索引が静かに減る。

%r{<code\b[^>]*>.*?</code>}m
EMPHASIS_PAIR =

強調・打ち消し線の記号(記号のすぐ内側に非空白がある対だけ)。 長い記号を先に置く(順序が逆だと ***強****強* になって内側が残る)。

アンダースコアだけ前後に ASCII の 英数字を許さない。snake_case_name を 壊さないためで、和文は制限しない——判断の根拠は VFM の実際の出力である。 厳密な CommonMark なら __太字__の語 は強調にならないが、VFM は <strong>太字</strong>の語 を出す。仕様書ではなくレンダラに合わせる。

記号の内側に空白がある形(2 * 3 * 4)は外さない。VFM はここも強調と解釈するが、 markdown-it の flanking 規則の再実装になるうえ、外さなくても失うのは 「まれな取りこぼし」だけで、誤って外すより安全側に倒れる。

/
  (?:(\*\*\*|\*\*|\*|~~)(?=\S)(.+?)(?<=\S)\1
  |(?<![A-Za-z0-9_])(___|__|_)(?=\S)(.+?)(?<=\S)\3(?![A-Za-z0-9_]))
/xm

Class Method Summary collapse

  • .each_prose_line(text) ⇒ Object

    コード(フェンス区切り行・フェンス内容行)を除いた「地の文」の行だけを 行番号(1 始まり)つきで yield する。ブロック未指定なら Enumerator を返す。 行番号は入力テキスト全体に対する通し番号で、コード行を飛ばしても維持される。.

  • .protect_code(text) ⇒ Array(String, Hash)

    コードフェンスブロックとインラインコードスパンを一時プレースホルダへ退避し、 後続のテキスト変形処理から除外できるようにする。 フェンス判定は状態機械へ統一(可変長・入れ子に追従)。.

  • .replace_top_level_fences(text) {|block, lineno| ... } ⇒ String

    トップレベルのフェンスドコードブロック(開始区切り〜終了区切り)を 1 つずつ yield し、戻り値で置き換える。nil を返したブロックは原文のまま残す。 yield には開始フェンス行の行番号(1 始まり)も渡す——著者向け警告に 「ファイル:行」を添えるため(warning-messages の流儀)。.

  • .restore_code(text, spans) ⇒ Object

    protect_code で退避したコードを元に戻す。 後から退避したインラインの原文が、先に退避したフェンスのプレースホルダを 内包しうる(行を跨ぐバッククォート対がフェンス置換後のプレースホルダを 巻き込むケース)。この入れ子を正しく巻き戻すため、挿入の逆順(LIFO / reverse_each)で外側→内側の順に開く。FIFO だと未復元のプレースホルダが残留する。.

  • .strip_code(text) ⇒ Object

    フェンス・インラインコードを取り除いたテキストを返す。 フェンス(区切り行・内容行)は空行に置換して行数を保つため、周辺文脈の 行対応は崩れない。インラインコードは空白 1 文字へ潰す。.

  • .strip_emphasis(line) ⇒ Array(String, Array<Integer>)

    強調記法を外した文字列と、元の行への添字表を返す。.

  • .strip_inline_code(text) ⇒ Object

    インラインコード ... を空白に置き換える。.

Class Method Details

.each_prose_line(text) ⇒ Object

コード(フェンス区切り行・フェンス内容行)を除いた「地の文」の行だけを 行番号(1 始まり)つきで yield する。ブロック未指定なら Enumerator を返す。 行番号は入力テキスト全体に対する通し番号で、コード行を飛ばしても維持される。



60
61
62
63
64
65
66
# File 'lib/vivlio_starter/cli/masking.rb', line 60

def each_prose_line(text)
  return enum_for(:each_prose_line, text) unless block_given?

  scan_lines(text) do |line, lineno, in_code|
    yield line, lineno unless in_code
  end
end

.protect_code(text) ⇒ Array(String, Hash)

コードフェンスブロックとインラインコードスパンを一時プレースホルダへ退避し、 後続のテキスト変形処理から除外できるようにする。 フェンス判定は状態機械へ統一(可変長・入れ子に追従)。

Returns:

  • (Array(String, Hash))

    退避後テキストと { placeholder => 原文 } の対応表



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/vivlio_starter/cli/masking.rb', line 109

def protect_code(text)
  spans = {}
  counter = 0

  # 第 2 引数はフェンス経由でのみ渡る行番号(gsub 経由の呼び出しでは省略される)
  alloc = lambda do |chunk, _lineno = nil|
    key = "#{CODE_SPAN_PLACEHOLDER_PREFIX}#{counter}__"
    spans[key] = chunk
    counter += 1
    key
  end

  # まずフェンスドコードブロック全体を退避(インラインコードより先に処理)。
  protected_text = replace_fenced_blocks(text, &alloc)

  # 次にインラインコードスパンを退避。
  protected_text = protected_text.gsub(INLINE_CODE_SPAN, &alloc)

  [protected_text, spans]
end

.replace_top_level_fences(text) {|block, lineno| ... } ⇒ String

トップレベルのフェンスドコードブロック(開始区切り〜終了区切り)を 1 つずつ yield し、戻り値で置き換える。nil を返したブロックは原文のまま残す。 yield には開始フェンス行の行番号(1 始まり)も渡す——著者向け警告に 「ファイル:行」を添えるため(warning-messages の流儀)。

コード保護(protect_code)より前段で特定言語のフェンスを横取りする変換 (例: ```mermaid の図化)のための公開 API。フェンス解釈(可変長・入れ子・

独自の再実装を作らせないための入り口でもある(P1「唯一の実装」)。

Parameters:

  • text (String)

    処理対象テキスト

Yield Parameters:

  • block (String)

    フェンスブロック全体(開始行〜終了行。末尾改行は含まない)

  • lineno (Integer)

    開始フェンス行の行番号(1 始まり)

Yield Returns:

  • (String, nil)

    置換文字列(nil なら置換しない)

Returns:

  • (String)

    置換後のテキスト



101
# File 'lib/vivlio_starter/cli/masking.rb', line 101

def replace_top_level_fences(text, &) = replace_fenced_blocks(text, &)

.restore_code(text, spans) ⇒ Object

protect_code で退避したコードを元に戻す。 後から退避したインラインの原文が、先に退避したフェンスのプレースホルダを 内包しうる(行を跨ぐバッククォート対がフェンス置換後のプレースホルダを 巻き込むケース)。この入れ子を正しく巻き戻すため、挿入の逆順(LIFO / reverse_each)で外側→内側の順に開く。FIFO だと未復元のプレースホルダが残留する。



135
136
137
138
139
140
141
# File 'lib/vivlio_starter/cli/masking.rb', line 135

def restore_code(text, spans)
  restored = text.to_s
  spans.reverse_each do |placeholder, original|
    restored = restored.gsub(placeholder) { original }
  end
  restored
end

.strip_code(text) ⇒ Object

フェンス・インラインコードを取り除いたテキストを返す。 フェンス(区切り行・内容行)は空行に置換して行数を保つため、周辺文脈の 行対応は崩れない。インラインコードは空白 1 文字へ潰す。



73
74
75
76
77
78
79
# File 'lib/vivlio_starter/cli/masking.rb', line 73

def strip_code(text)
  stripped = +''
  scan_lines(text) do |line, _lineno, in_code|
    stripped << (in_code ? "\n" : line)
  end
  strip_inline_code(stripped)
end

.strip_emphasis(line) ⇒ Array(String, Array<Integer>)

強調記法を外した文字列と、元の行への添字表を返す。

なぜ要るのか。 校正の辞書は「読者が見る文字列」に当てなければならない。 生の行に当てると、語の途中に入った強調で両方向に壊れる—— 結**合し**直した の直前が * なのでガード (?<![一-龥])合し を すり抜けて誤検出になり、だ**円** は語が割れて当たらなくなる。 仕様: inline-emphasis-word-split-spec.md

Parameters:

  • line (String)

    1 行(コードは protect_code で退避済みであること)

Returns:

  • (Array(String, Array<Integer>))

    記法を外した文字列と、 plain が元の行の何文字目だったかを表す添字の配列



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

def strip_emphasis(line)
  plain = +''
  map = []
  pos = 0
  while (matched = EMPHASIS_PAIR.match(line, pos))
    head = matched.begin(0)
    plain << line[pos...head]
    map.concat((pos...head).to_a)
    inner_group = matched[2] ? 2 : 4
    inner_start = matched.begin(inner_group)
    inner, inner_map = strip_emphasis(matched[inner_group])
    plain << inner
    map.concat(inner_map.map { it + inner_start })
    pos = matched.end(0)
  end
  plain << line[pos..].to_s
  map.concat((pos...line.size).to_a)
  [plain, map]
end

.strip_inline_code(text) ⇒ Object

インラインコード ... を空白に置き換える。



82
# File 'lib/vivlio_starter/cli/masking.rb', line 82

def strip_inline_code(text) = text.gsub(/`[^`\n]+`/, ' ')