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 =
--- 数式のしるし ------------------------------------------------------ 関数名は前後が英数字だと拾わない(
expressionのexpは関数ではない。 これも仕様 §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
-
.align_row(row) ⇒ Object
最初の関係演算子の前に
&を置く(alignedの揃え位置)。. -
.display_body(body) ⇒ Object
複数行は
\begin{aligned}で関係演算子の位置を揃える。 1 行ずつ独立した$$にすると、X = …に続く= …の導出が揃わない。 著者がすでに\\や\begin{…}を書いているときは、その行組みをそのまま渡す。. -
.display_math(block) ⇒ Object
フェンスブロックがディスプレイ数式なら
$$…$$を返す。違えば nil。. -
.math?(text) ⇒ Boolean
1 つの綴りが数式か。.
-
.stash_block(block, stash) ⇒ Object
数式でないフェンスをプレースホルダへ退避する。.
-
.transform(content) ⇒ String
本文中のバッククォート数式を
$…$/$$…$$へ起こす。. -
.transform_inline_spans(text) ⇒ Object
インラインコードスパンのうち数式のものを
$…$にする。 二重バッククォート(…)は対象外——記法を説明するために使われる形なので触らない。.
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 つの綴りが数式か。
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
本文中のバッククォート数式を $…$ / $$…$$ へ起こす。
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 |