Module: VivlioStarter::CLI::PreProcessCommands::PlainMathTranspiler
- Defined in:
- lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb
Overview
素の表記 → TeX 変換器(plain-math-notation-spec.md §5)
Constant Summary collapse
- SUPERSCRIPT =
Unicode 上付き文字 → 通常文字。
¹²³だけ Latin-1 に居て他と離れている。 { '⁰' => '0', '¹' => '1', '²' => '2', '³' => '3', '⁴' => '4', '⁵' => '5', '⁶' => '6', '⁷' => '7', '⁸' => '8', '⁹' => '9', '⁺' => '+', '⁻' => '-', '⁼' => '=', '⁽' => '(', '⁾' => ')', 'ⁿ' => 'n', 'ⁱ' => 'i', 'ˣ' => 'x' # ˣ は U+02E3(`eˣ` で実在) }.freeze
- SUBSCRIPT =
Unicode 下付き文字 → 通常文字。
Hₙの ₙ は U+2099。 { '₀' => '0', '₁' => '1', '₂' => '2', '₃' => '3', '₄' => '4', '₅' => '5', '₆' => '6', '₇' => '7', '₈' => '8', '₉' => '9', '₊' => '+', '₋' => '-', '₌' => '=', '₍' => '(', '₎' => ')', 'ₙ' => 'n', 'ₖ' => 'k', 'ᵢ' => 'i', 'ⱼ' => 'j' }.freeze
- SUPERSCRIPT_CHARS =
Regexp.union(SUPERSCRIPT.keys).freeze
- SUBSCRIPT_CHARS =
Regexp.union(SUBSCRIPT.keys).freeze
- SUPERSCRIPT_RUN =
/#{SUPERSCRIPT_CHARS}+/o- SUBSCRIPT_RUN =
/#{SUBSCRIPT_CHARS}+/o- FUNCTIONS =
TeX に用意されている演算子名(38 種)。長いものから並べる (
arctanをarc+tanに、liminfをlim+infに割らないため。 正規表現の交替は先頭一致優先)。MathJax で全種が通ることを実測済み。 %w[ varinjlim varliminf varlimsup varprojlim arccos arcsin arctan liminf limsup injlim projlim sinh cosh tanh coth sin cos tan sec csc cot arg deg det dim exp gcd hom inf ker lim log max min sup Pr lg ln ].freeze
- FUNCTION_NAMES =
関数名の綴り。前後が英数字・バックスラッシュのときは拾わない (
\sinは変換済み、expressionのexpは関数ではない)。 /(?<![\\\p{Alnum}])(?:#{FUNCTIONS.join('|')})(?![A-Za-z0-9])/o- PLAIN_MARKERS =
素の表記のしるし(ゲート)。仕様 §5.1。
既に正しい TeX を 1 文字も変えないための保険である。
\sqrt{}に√は無く、^{2}に²は無く、f^{(n)}は^の次が{であって(ではない。^_の扱いに注意:a^2は TeX としてそのまま正しいので、しるしにしない。 壊れるのは引数が 2 文字以上のとき(a^560はa^5の後ろに60が並ぶ)と、 丸括弧を引数にしたとき(a^(p-1))だけなので、その形だけを拾う。 Regexp.union( SUPERSCRIPT_CHARS, SUBSCRIPT_CHARS, /[√Σ・]/, # 根号・総和(U+03A3)・中黒(U+30FB) /[\u{FF01}-\u{FF5E}\u{3000}]/, # 全角英数字・全角記号・全角空白 /[A-Za-z]̂/, # 合成アクセント(p̂) /[\^_]\(/, # 丸括弧を引数にした上下付き /[\^_]\w{2,}/, # 引数が 2 文字以上の上下付き /[\^_][-−+]/, # 符号つきの上下付き(`2^-52` は `2^{-}` に 52 が続く誤り) /<=|>=|!=/, /(?<!\\)[%#&]/, # TeX の特殊文字(素のままだと消える・エラーになる) /[〜~]/, /(?<!\\)\bmod\b/, /∫\s*\[/, # 上下限つきの積分 /(?<!\\)\blim\s*\(/, /(?<!\\)\b(?:#{FUNCTIONS.join('|')})\b/o ).freeze
- BASE_TAIL =
上下付きの「基底」になれる文字で終わっているか。
x²の x や(a+b)²の ) は基底になるが、行頭や+の直後は基底が無い。 /[\p{Alnum}\p{Greek})\]}]\z/- TEX_SPECIAL =
TeX が特別な意味を与える字。素で書かれると黙って壊れるので必ず退避する。
50% + 1→50% から後ろがコメント扱いで消えるa#ba&bTeX エラー(merror)になる 既に\%と書いてある場合は command() が先に食うので、ここへは来ない。 /[%#&$]/- WAVE_DASH =
波ダッシュ。半角
~にしてはならない——TeX の~は改行しない空白で、 範囲を表すg^1〜g^(p-1)がg^1 g^(p-1)に化ける。 /[〜~]/
Class Method Summary collapse
-
.atom(scanner, out) ⇒ Object
1 トークンを消費して TeX 片を返す。out は「直前に何を出したか」の判定にだけ使う。.
-
.balanced(scanner, pair = ['(', ')']) ⇒ Object
対応する括弧まで読み、中身を再帰変換して返す(括弧自体は含めない)。 閉じ括弧が無ければ位置を戻して nil——壊れた入力で式全体を諦めないため。.
-
.big_operator(scanner) ⇒ Object
Σ(U+03A3)は斜体の変数として描かれてしまうので\sumへ。Σ(k=0 to ∞)のような著者が書いた上下限の記法もここで拾う。∑(U+2211)は既に総和記号なので触らない。. -
.bounds(scanner, name) ⇒ Object
(k=0 to ∞)→_{k=0}^{∞}。toが無ければ上下限なしの演算子だけを返す。. -
.command(scanner) ⇒ Object
\commandは中身を見ずにそのまま通す(既存 TeX を壊さない)。. -
.escape_tex(scanner, char) ⇒ Object
TeX の特殊文字なら退避する(
50%→50\%、全角の%も同じ)。. -
.group(scanner) ⇒ Object
{…}は透過。中は再帰する。. -
.integral_bounds(scanner) ⇒ Object
∫[0, π/2]→\int_{0}^{π/2}。著者が考えた記法なので TeX 側に対応が無い。. -
.limit(scanner) ⇒ Object
lim(n→∞)→\lim_{n→∞}。→はそのままで正しく描かれる。. -
.macro(scanner, name) ⇒ Object
マクロの直後が英字・ギリシャ文字だと
\tanθのように 1 つの命令として読まれるので、 そのときだけ空白を足す。\sin(x)や\sin^{2}θには足さない——余計な空きが出るうえ、 上付き・下付きの基底が空白で隠れてしまう。. -
.modulo(scanner) ⇒ Object
(mod n)→\pmod{n}、単独のmod→\bmod。modを素のまま渡すと𝑚𝑜𝑑と斜体 3 文字で並ぶ。. -
.named_function(scanner) ⇒ Object
sin(x)→\sin(x)、tanθ→\tan θ(ギリシャ文字自体は素通し)。 素のままだと𝑡𝑎𝑛𝜃と 1 文字ずつ斜体になる。log₂の下付きは続く script() が拾う——そのためにここで空白を足してはならない (空白があると基底が無いと判定され、₂ が変換されずに残る)。. -
.plain?(src) ⇒ Boolean
素の表記のしるしを含むか(ゲート)。.
-
.radical(scanner) ⇒ Object
√は直後の 1 トークンだけに掛かる。全体に掛けたければ√(…)と括る。√x+1が\sqrt{x}+1になるのはこの規則による(仕様 §5.2)。. -
.run(scanner, stop: nil) ⇒ Object
トークン列を消費して TeX を組む。
stopを見つけたら消費せずに戻る。. -
.script(scanner, out) ⇒ Object
上付き・下付き。Unicode の上下付き文字と、
^/_の両方をここで正規化する。. -
.script_argument(scanner) ⇒ Object
^/_の引数。(…)は外して中身を、{…}はそのまま、 それ以外は 1 かたまりを取る。. -
.symbol(scanner) ⇒ Object
1 文字の置き換え。全角は半角へ、合成アクセントは
\hat{}へ、・(U+30FB) は\cdotへ(消えるのはこちらで、·(U+00B7) は無事)。. -
.to_tex(src, force: false) ⇒ String
素の表記を TeX へ起こす。しるしが無ければ src をそのまま返す。.
-
.unicode_script(raw, table, mark, out) ⇒ Object
連続する Unicode 上下付き文字を 1 つの
^{…}/_{…}にまとめる。 基底が無いとき(式頭の²x)は変換せず原文のまま残す——基底の無い msup になり、 著者の意図と無関係な組版になるため。.
Class Method Details
.atom(scanner, out) ⇒ Object
1 トークンを消費して TeX 片を返す。out は「直前に何を出したか」の判定にだけ使う。
143 144 145 146 147 148 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 143 def atom(scanner, out) command(scanner) || group(scanner) || script(scanner, out) || radical(scanner) || big_operator(scanner) || limit(scanner) || modulo(scanner) || named_function(scanner) || symbol(scanner) || scanner.getch # 解釈できない字は原文のまま通す end |
.balanced(scanner, pair = ['(', ')']) ⇒ Object
対応する括弧まで読み、中身を再帰変換して返す(括弧自体は含めない)。 閉じ括弧が無ければ位置を戻して nil——壊れた入力で式全体を諦めないため。
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 328 def balanced(scanner, pair = ['(', ')']) open, close = pair saved = scanner.pos return nil unless scanner.scan(/#{Regexp.escape(open)}/) out = +'' depth = 1 until scanner.eos? if scanner.check(/#{Regexp.escape(open)}/) depth += 1 elsif scanner.check(/#{Regexp.escape(close)}/) depth -= 1 break if depth.zero? end out << atom(scanner, out) end unless scanner.scan(/#{Regexp.escape(close)}/) scanner.pos = saved return nil end out end |
.big_operator(scanner) ⇒ Object
Σ(U+03A3)は斜体の変数として描かれてしまうので \sum へ。
Σ(k=0 to ∞) のような著者が書いた上下限の記法もここで拾う。
∑(U+2211)は既に総和記号なので触らない。
225 226 227 228 229 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 225 def big_operator(scanner) if scanner.scan(/Σ/) then bounds(scanner, '\\sum') elsif scanner.scan(/∫/) then integral_bounds(scanner) end end |
.bounds(scanner, name) ⇒ Object
(k=0 to ∞) → _{k=0}^{∞}。to が無ければ上下限なしの演算子だけを返す。
232 233 234 235 236 237 238 239 240 241 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 232 def bounds(scanner, name) saved = scanner.pos scanner.skip(/\s*/) if scanner.check(/\(/) && (inner = balanced(scanner)) && (parts = inner.split(/\s+to\s+/, 2)).size == 2 return "#{name}_{#{parts[0].strip}}^{#{parts[1].strip}}" end scanner.pos = saved macro(scanner, name) end |
.command(scanner) ⇒ Object
\command は中身を見ずにそのまま通す(既存 TeX を壊さない)。
151 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 151 def command(scanner) = scanner.scan(/\\(?:[A-Za-z]+|.)/m) |
.escape_tex(scanner, char) ⇒ Object
TeX の特殊文字なら退避する(50% → 50\%、全角の % も同じ)。
ただし \begin{aligned} のような環境の中では、& は桁揃えの記号として、
# はマクロ引数として正しい——環境を使う式は LaTeX を知っている著者が
書いたものなので、そこは触らない(本書 96-sample.md の九九の表で実際に踏んだ)。
% だけは例外で常に退避する。素のままだと後ろが丸ごとコメントとして消えるため。
319 320 321 322 323 324 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 319 def escape_tex(scanner, char) return char unless char.match?(TEX_SPECIAL) return char if char != '%' && scanner.string.include?('\\begin{') "\\#{char}" end |
.group(scanner) ⇒ Object
{…} は透過。中は再帰する。
154 155 156 157 158 159 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 154 def group(scanner) return nil unless scanner.scan(/\{/) inner = run(scanner, stop: /\}/) "{#{inner}#{scanner.scan(/\}/) || ''}" end |
.integral_bounds(scanner) ⇒ Object
∫[0, π/2] → \int_{0}^{π/2}。著者が考えた記法なので TeX 側に対応が無い。
244 245 246 247 248 249 250 251 252 253 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 244 def integral_bounds(scanner) saved = scanner.pos scanner.skip(/\s*/) if scanner.check(/\[/) && (inner = balanced(scanner, ['[', ']'])) && (parts = inner.split(',', 2)).size == 2 return "\\int_{#{parts[0].strip}}^{#{parts[1].strip}}" end scanner.pos = saved macro(scanner, '\\int') end |
.limit(scanner) ⇒ Object
lim(n→∞) → \lim_{n→∞}。→ はそのままで正しく描かれる。
256 257 258 259 260 261 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 256 def limit(scanner) return nil unless scanner.scan(/(?<!\\)\blim(?=\s*\()/) scanner.skip(/\s*/) "\\lim_{#{balanced(scanner).to_s.strip}}" end |
.macro(scanner, name) ⇒ Object
マクロの直後が英字・ギリシャ文字だと \tanθ のように 1 つの命令として読まれるので、
そのときだけ空白を足す。\sin(x) や \sin^{2}θ には足さない——余計な空きが出るうえ、
上付き・下付きの基底が空白で隠れてしまう。
287 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 287 def macro(scanner, name) = scanner.check(/[[:alpha:]]/) ? "#{name} " : name |
.modulo(scanner) ⇒ Object
(mod n) → \pmod{n}、単独の mod → \bmod。
mod を素のまま渡すと 𝑚𝑜𝑑 と斜体 3 文字で並ぶ。
265 266 267 268 269 270 271 272 273 274 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 265 def modulo(scanner) saved = scanner.pos if scanner.scan(/\(\s*mod\b/) inner = run(scanner, stop: /\)/) return "\\pmod{#{inner.strip}}" if scanner.scan(/\)/) scanner.pos = saved end scanner.scan(/(?<!\\)\bmod\b/) ? macro(scanner, '\\bmod') : nil end |
.named_function(scanner) ⇒ Object
sin(x) → \sin(x)、tanθ → \tan θ(ギリシャ文字自体は素通し)。
素のままだと 𝑡𝑎𝑛𝜃 と 1 文字ずつ斜体になる。
log₂ の下付きは続く script() が拾う——そのためにここで空白を足してはならない
(空白があると基底が無いと判定され、₂ が変換されずに残る)。
280 281 282 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 280 def named_function(scanner) (name = scanner.scan(FUNCTION_NAMES)) && macro(scanner, "\\#{name}") end |
.plain?(src) ⇒ Boolean
素の表記のしるしを含むか(ゲート)。
119 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 119 def plain?(src) = !src.nil? && src.match?(PLAIN_MARKERS) |
.radical(scanner) ⇒ Object
√ は直後の 1 トークンだけに掛かる。全体に掛けたければ √(…) と括る。
√x+1 が \sqrt{x}+1 になるのはこの規則による(仕様 §5.2)。
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 202 def radical(scanner) saved = scanner.pos return nil unless scanner.scan(/√/) scanner.skip(/[ \t]+/) body = if scanner.check(/\(/) balanced(scanner) elsif scanner.scan(/\{/) run(scanner, stop: /\}/).tap { scanner.scan(/\}/) } else # 数は丸ごと 1 トークン。1 文字ずつ取ると `√163` が `\sqrt{1}63` になる scanner.scan(/\d+(?:\.\d+)?|[[:alpha:]]/) end return "\\sqrt{#{body.strip}}" if body && !body.strip.empty? # 閉じ括弧が無い `√(x` や裸の `√`。原文のまま通し、式全体を諦めない scanner.pos = saved scanner.getch end |
.run(scanner, stop: nil) ⇒ Object
トークン列を消費して TeX を組む。stop を見つけたら消費せずに戻る。
136 137 138 139 140 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 136 def run(scanner, stop: nil) out = +'' out << atom(scanner, out) until scanner.eos? || (stop && scanner.check(stop)) out end |
.script(scanner, out) ⇒ Object
上付き・下付き。Unicode の上下付き文字と、^/_ の両方をここで正規化する。
162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 162 def script(scanner, out) if (raw = scanner.scan(SUPERSCRIPT_RUN)) return unicode_script(raw, SUPERSCRIPT, '^', out) end if (raw = scanner.scan(SUBSCRIPT_RUN)) return unicode_script(raw, SUBSCRIPT, '_', out) end return nil unless (mark = scanner.scan(/[\^_]/)) argument = script_argument(scanner) # 引数を読めなければ `^` をそのまま通す(`2^(` を `2^{}(` にしない) argument.nil? || argument.empty? ? mark : "#{mark}{#{argument}}" end |
.script_argument(scanner) ⇒ Object
^ / _ の引数。(…) は外して中身を、{…} はそのまま、
それ以外は 1 かたまりを取る。
数字と英字は混ぜて取らない。 H_2O は LaTeX では「H の添字 2」に続く「O」で、
混ぜて取ると H_{2O} になり O まで添字へ落ちる(実測: 本書の早見表で
H₂O が H₂ₒ と出た)。一方 a^560 は 560 全体が指数でなければ
a^5 の後ろに 60 が並んでしまう。数字の連なり/英字の連なりを
それぞれ 1 かたまりと見れば、どちらも正しくなる。
193 194 195 196 197 198 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 193 def script_argument(scanner) return balanced(scanner) if scanner.check(/\(/) return run(scanner, stop: /\}/).tap { scanner.scan(/\}/) } if scanner.scan(/\{/) scanner.scan(/[-−+]?(?:\d+(?:\.\d+)?|[\p{Alpha}]+)/) || '' end |
.symbol(scanner) ⇒ Object
1 文字の置き換え。全角は半角へ、合成アクセントは \hat{} へ、
・(U+30FB) は \cdot へ(消えるのはこちらで、·(U+00B7) は無事)。
300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 300 def symbol(scanner) if (accented = scanner.scan(/[A-Za-z]̂/)) then "\\hat{#{accented[0]}}" elsif scanner.scan(/・/) then macro(scanner, '\\cdot') elsif scanner.scan(WAVE_DASH) then macro(scanner, '\\sim') elsif scanner.scan(/<=/) then macro(scanner, '\\leq') elsif scanner.scan(/>=/) then macro(scanner, '\\geq') elsif scanner.scan(/!=/) then macro(scanner, '\\neq') elsif (special = scanner.scan(TEX_SPECIAL)) then escape_tex(scanner, special) elsif (wide = scanner.scan(/[\u{FF01}-\u{FF5D}]/)) then escape_tex(scanner, (wide.ord - 0xFEE0).chr(Encoding::UTF_8)) elsif scanner.scan(/ /) then ' ' end end |
.to_tex(src, force: false) ⇒ String
素の表記を TeX へ起こす。しるしが無ければ src をそのまま返す。
125 126 127 128 129 130 131 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 125 def to_tex(src, force: false) text = src.to_s return src if text.empty? return src unless force || plain?(text) run(StringScanner.new(text)) end |
.unicode_script(raw, table, mark, out) ⇒ Object
連続する Unicode 上下付き文字を 1 つの ^{…} / _{…} にまとめる。
基底が無いとき(式頭の ²x)は変換せず原文のまま残す——基底の無い msup になり、
著者の意図と無関係な組版になるため。
179 180 181 182 183 |
# File 'lib/vivlio_starter/cli/pre_process/plain_math_transpiler.rb', line 179 def unicode_script(raw, table, mark, out) return raw unless out.match?(BASE_TAIL) "#{mark}{#{raw.each_char.map { table[it] }.join}}" end |