Module: VivlioStarter::CLI::PreProcessCommands::MathSpanDetector

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

Overview

バッククォートの中の数式を $…$ / $$…$$ へ起こす判定器

Constant Summary collapse

CODE_MARKERS =

--- コードのしるし(1 つでもあれば数式にしない) --------------------- \.[A-Za-z_] は小数点を避けるため(0.99 をメソッド呼び出しと誤認すると 数式を 15 件落とす。仕様 §6.3 で実際に踏んだ)。

/
  \.[A-Za-z_] | :: | => | \|\| | &&  | \#\{ | @\w
  | \*\*                                        # Ruby のべき乗・Markdown の太字
  | \[\w*\]                                     # 添字アクセス
  | \{\s*\|                                     # ブロック引数
  | [;{}]\s*\z
  | \b(?:puts|print|def|end|do|nil|true|false|class|module|require|return
      |each|map|select|reject|new|attr_\w+|elsif|then|yield|lambda|proc)\b
/x
MATH_FUNCTIONS =

--- 数式のしるし ------------------------------------------------------ 関数名は前後が英数字だと拾わない(expressionexp は関数ではない。 これも仕様 §6.3 で踏んだ)。

%w[arcsin arccos arctan sinh cosh tanh sin cos tan sec csc cot
log ln exp det gcd lim].freeze
STRONG_MARKERS =

強いしるし——これがあれば、日本語を含んでいても式と見てよい。 変数を日本語で書いた式(平文^e mod n)は、^mod のような 構造を持つしるしがあって初めて式と分かる。

/
  [²³¹⁰⁴-⁹⁺⁻⁼⁽⁾ⁿⁱˣ]                            # Unicode 上付き
  | [₀-₉₊₋₌₍₎ₙₖᵢⱼ]                               # Unicode 下付き
  | [√Σ∑∏∫≡≒≈≠≦≧≤≥] | [α-ωΑ-Ω]
  | (?<![\\[:alnum:]])[[:alnum:])\]]+\s*\^        # べき乗(`圧力^2` のように底が日本語でも拾う)
  | (?<=[[:alnum:]))])\s*\bmod\b\s*[[:alnum:]((]  # 両側に被演算子のある剰余
  | (?<![\\[:alnum:]])(?:#{MATH_FUNCTIONS.join('|')})(?![[:alnum:]])\s*[(θ]
/x
WEAK_MARKERS =

弱いしるし——日本語と一緒だと式か地の文か決まらない。 行計 × 列計 ÷ 総計 は式にも読めるが、数式として組んで得るものが無く、 それを機械が黙って決めてよい判断でもない(§3.4)。

/[×÷±∓・·°−∞]/
MATH_MARKERS =
Regexp.union(STRONG_MARKERS, WEAK_MARKERS)
JAPANESE =

日本語( は数式の中で使われるので除く)。

/[ぁ-ゖァ-ヺ一-鿿]/
OUTPUT_MARKERS =

実行結果の貼り付け。ディスプレイ候補から弾く(仕様 §6.2)。 を一律に弾いてはならない——外接: C(n) = 2n・tanθ は日本語ラベル付きの 本物の数式で、同じ字を使う。弾く根拠は「見出し行がある」「矢印で結果を書いている」に置く。

/【.*】|→|⇒/
MAX_DISPLAY_LINES =

ディスプレイ候補の上限行数。これを超えるものは実行結果の貼り付けとみなす。

8
RELATION =

揃えるための関係演算子(複数行を \beginaligned で組むときの & の位置)。

/(?<![<>=!])[=≡≈≒](?![=])/
FENCE_OPENER =

数式であることを著者が明示したフェンス(```math / ~~~math)。

/\A(?:`{3,}|~{3,})[ \t]*/
EXPLICIT_LANG =
/#{FENCE_OPENER.source}math[ \t]*\z/i
AUTHORED_ROWS =

著者がすでに TeX の行組みを書いている印。\begin{aligned}\\ があれば そのまま渡す(こちらで aligned を被せると二重になる)。

/\\\\|\\begin\{/

Class Method Summary collapse

Class Method Details

.align_row(row) ⇒ Object

最初の関係演算子の前に & を置く(aligned の揃え位置)。



187
# File 'lib/vivlio_starter/cli/pre_process/math_span_detector.rb', line 187

def align_row(row) = row.sub(RELATION) { "&#{::Regexp.last_match(0)}" }

.display_body(body) ⇒ Object

複数行は \begin{aligned} で関係演算子の位置を揃える。 1 行ずつ独立した $$ にすると、X = … に続く = … の導出が揃わない。 著者がすでに \\\begin{…} を書いているときは、その行組みをそのまま渡す。



178
179
180
181
182
183
184
# File 'lib/vivlio_starter/cli/pre_process/math_span_detector.rb', line 178

def display_body(body)
  return body.first.strip if body.size == 1
  return body.map(&:strip).join("\n") if body.any? { it.match?(AUTHORED_ROWS) }

  rows = body.map { align_row(it.strip) }
  "\\begin{aligned}\n#{rows.join(" \\\\\n")}\n\\end{aligned}"
end

.display_math(block) ⇒ Object

フェンスブロックがディスプレイ数式なら $$…$$ を返す。違えば nil。

そちらには行数・実行結果らしさ・関係演算子・`math?` の 4 条件が掛かる。


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/vivlio_starter/cli/pre_process/math_span_detector.rb', line 158

def display_math(block)
  lines = block.lines.map(&:chomp)
  opener = lines.first.to_s.strip
  explicit = opener.match?(EXPLICIT_LANG)
  return nil unless explicit || opener.match?(/#{FENCE_OPENER.source}\z/) # 他の言語付きは常にコード

  body = lines[1..-2].to_a.map(&:rstrip).reject { it.strip.empty? }
  return nil if body.empty?
  return "\n$$\n#{display_body(body)}\n$$\n" if explicit

  return nil if body.size > MAX_DISPLAY_LINES
  return nil if body.any? { it.match?(OUTPUT_MARKERS) }
  return nil unless body.all? { it.match?(RELATION) && math?(it) }

  "\n$$\n#{display_body(body)}\n$$\n"
end

.math?(text) ⇒ Boolean

1 つの綴りが数式か。

Parameters:

  • text (String)

    コードスパン / フェンス 1 行の中身

Returns:

  • (Boolean)


114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/vivlio_starter/cli/pre_process/math_span_detector.rb', line 114

def math?(text)
  body = text.to_s.strip
  return false if body.include?('$')
  # 1 文字だけの綴りは**記号そのものの説明**であって式ではない
  # (本書 32-metrics.md の「`・` で連結し」で実際に踏んだ)。
  # 実測でこの規則が落とすのはコーパス全体で `λ` 1 種だけ。2 文字以上は
  # `√n` `Σx` `eˣ` `n³` `2²` のように本物の式が並ぶので、境界はここが妥当。
  return false if body.chars.size < 2
  return false if body.match?(CODE_MARKERS)
  return false unless body.match?(MATH_MARKERS)

  # 日本語を含むときは**強いしるしがあるときだけ**式と見る。
  # `平文^e mod n` は変数名が日本語なだけの式なので組む。
  # `行計 × 列計 ÷ 総計` は `×` しか手がかりが無く、式か地の文か決められない。
  body.match?(JAPANESE) ? body.match?(STRONG_MARKERS) : true
end

.stash_block(block, stash) ⇒ Object

数式でないフェンスをプレースホルダへ退避する。



190
191
192
193
194
# File 'lib/vivlio_starter/cli/pre_process/math_span_detector.rb', line 190

def stash_block(block, stash)
  placeholder = "\u0000VS_FENCE_#{stash.size}\u0000"
  stash[placeholder] = block
  placeholder
end

.transform(content) ⇒ String

本文中のバッククォート数式を $…$ / $$…$$ へ起こす。

Parameters:

  • content (String)

    章の Markdown 本文

Returns:

  • (String)

    置換後の本文



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/vivlio_starter/cli/pre_process/math_span_detector.rb', line 134

def transform(content)
  return content if content.nil? || content.empty?

  # --- Phase: フェンスを先に処理する(中のコードスパンを拾わないため) ---
  # 数式でないフェンスはプレースホルダへ退避し、インライン走査から隠す。
  stash = {}
  text = Masking.replace_top_level_fences(content) do |block, _lineno|
    display_math(block) || stash_block(block, stash)
  end

  # --- Phase: インラインコードスパンを起こす ---
  text = transform_inline_spans(text)

  # --- Phase: 退避したフェンスを戻す ---
  stash.each { |placeholder, original| text = text.sub(placeholder) { original } }
  text
end

.transform_inline_spans(text) ⇒ Object

インラインコードスパンのうち数式のものを $…$ にする。 二重バッククォート()は対象外——記法を説明するために使われる形なので触らない。



198
199
200
201
202
203
# File 'lib/vivlio_starter/cli/pre_process/math_span_detector.rb', line 198

def transform_inline_spans(text)
  text.gsub(/(?<!`)`([^`\n]+)`(?!`)/) do
    body = ::Regexp.last_match(1)
    math?(body) ? "$#{body.strip}$" : ::Regexp.last_match(0)
  end
end