Module: VivlioStarter::CLI::Build::MathTextRenderer
- Defined in:
- lib/vivlio_starter/cli/build/math_text_renderer.rb
Overview
LaTeX 単純サブセット → HTML テキスト変換器(Kindle 数式テキスト化)
Constant Summary collapse
- SYMBOLS =
名前付き記号コマンド(\times 等)→ Unicode。§3.1 の記号行。
{ 'times' => '×', 'cdot' => '⋅', 'pm' => '±', 'mp' => '∓', 'approx' => '≈', 'neq' => '≠', 'leq' => '≤', 'geq' => '≥', 'le' => '≤', 'ge' => '≥', 'sim' => '∼', 'propto' => '∝', 'infty' => '∞', 'll' => '≪', 'gg' => '≫', 'langle' => '⟨', 'rangle' => '⟩', 'ldots' => '…', 'cdots' => '…', 'dots' => '…', # 矢印と集合。`\to` が無いだけで `\lim_{n \to \infty}` が式ごと拒否され、 # SVG のまま Kindle へ残って横に潰れていた(2026-08-20 の実機で発覚)。 'to' => '→', 'rightarrow' => '→', 'leftarrow' => '←', 'mapsto' => '↦', 'Rightarrow' => '⇒', 'Leftarrow' => '⇐', 'Leftrightarrow' => '⇔', 'in' => '∈', 'notin' => '∉', 'subset' => '⊂', 'supset' => '⊃', 'cup' => '∪', 'cap' => '∩', 'emptyset' => '∅', 'partial' => '∂', 'nabla' => '∇', 'circ' => '∘', 'perp' => '⊥', 'forall' => '∀', 'exists' => '∃', 'equiv' => '≡', # 大型演算子。直後の `_{…}^{…}` は既存の上下付き処理がそのまま拾うので、 # `\sum_{i=1}^{n}` は `∑` + `<sub>i=1</sub><sup>n</sup>` になる。 # SVG 画像より読みやすく、かつ本文のフォントサイズに追従する。 'sum' => '∑', 'prod' => '∏', 'int' => '∫', 'oint' => '∮' }.freeze
- GREEK =
ギリシャ文字コマンド → Unicode(立体=イタリックにしない・§3.1)。
{ 'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ', 'delta' => 'δ', 'epsilon' => 'ε', 'varepsilon' => 'ε', 'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ', 'vartheta' => 'ϑ', 'iota' => 'ι', 'kappa' => 'κ', 'lambda' => 'λ', 'mu' => 'μ', 'nu' => 'ν', 'xi' => 'ξ', 'omicron' => 'ο', 'pi' => 'π', 'varpi' => 'ϖ', 'rho' => 'ρ', 'varrho' => 'ϱ', 'sigma' => 'σ', 'varsigma' => 'ς', 'tau' => 'τ', 'upsilon' => 'υ', 'phi' => 'φ', 'varphi' => 'ϕ', 'chi' => 'χ', 'psi' => 'ψ', 'omega' => 'ω', 'Gamma' => 'Γ', 'Delta' => 'Δ', 'Theta' => 'Θ', 'Lambda' => 'Λ', 'Xi' => 'Ξ', 'Pi' => 'Π', 'Sigma' => 'Σ', 'Upsilon' => 'Υ', 'Phi' => 'Φ', 'Psi' => 'Ψ', 'Omega' => 'Ω' }.freeze
- OPERATORS =
そのまま通す演算子・区切り(§3.1)。< > & は出力時にエスケープする。
'は導関数のプライム(f'(x)・f''(0))。数式で普通に出る。 %r{[+\-=/<>()\[\]|,.:;!']}- PASSTHROUGH =
Unicode で直接書かれた記号・ギリシャ文字・日本語をそのまま通す。
なぜ後から足したか: この変換器は 2026-07-19 に、LaTeX で書かれた原稿だけを見て 作られた(当時の原稿の数式は全件
\times\piの形だった)。素の表記を組む機能が 入ってからは×やθが直接届くようになり、受理集合が実態に追いつかなくなった。 上の SYMBOLS / GREEK は「コマンド名 → Unicode」の対応表なので、Unicode をそのまま 受けて出すのは同じ字を返すだけで済む。 これを足すとa^(p-1) mod pやsin²θ + cos²θが真の上付き付きの HTMLになり、 原文テキストへ落とさずに済む(Kindle での見栄えが一段良くなる)。 日本語はぁ-ゖァ-ヺの範囲だけでは足りない——長音符ー(U+30FC)が外にあり、 「ハッシュ値 mod サーバー台数」が丸ごと拒否されていた。文字プロパティで書く。 /[×÷±∓⋅・·≈≒≡≠≤≥≦≧∼∝∞…°−→∈]|[α-ωΑ-Ω]|[\p{Hiragana}\p{Katakana}\p{Han}ー々〆]/- FUNCTIONS =
TeX に用意されている演算子名(38 種)。立体(イタリックにしない)で出す。 変換器(PlainMathTranspiler::FUNCTIONS)と同じ集合にしておく——あちらが
\supを出すのにこちらが知らないと、式ごと拒否されて Kindle が SVG へ落ちる。 %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
- BIG_OPERATOR_WITH_LIMITS =
大型演算子と
limは、上下限のあとに被演算子が続くと詰まって読みにくい。 数式の空白は TeX 流に落としているので(consume_atom)、ここで細空白を補う。 %r{((?:[∑∏∫∮]|\blim)(?:<su[bp]>.*?</su[bp]>)*)(?=[^\s])}m
Class Method Summary collapse
-
.consume_atom(scanner, upright:, allow_script:, allow_frac:) ⇒ Object
1 アトムを消費して HTML 片を返す(空白は '' か ' ')。未知トークンは nil。.
-
.consume_brace_group(scanner, upright:, allow_script:, allow_frac:) ⇒ Object
'{ … }' を必須で読み、中身の run を返す(透過)。閉じ '}' が無ければ nil。.
-
.consume_command(scanner, upright:, allow_frac:) ⇒ Object
\command を消費する。\ は消費済みで、続く名前/記号を読む。.
-
.consume_group_or_token(scanner, upright:, allow_script:, allow_frac:) ⇒ Object
上/下付き・\frac の引数('…' グループ or 単一アトム)。先行空白は読み飛ばす。.
-
.consume_run(scanner, upright:, allow_script:, allow_frac:, stop_at_brace:) ⇒ Object
トークン列を消費して HTML を組む。stop_at_brace 時はトップレベルの '}' で停止(消費しない)。 1 つでも解釈不能なトークンがあれば nil(=式全体を拒否)。.
-
.dispatch_named_command(name, scanner, allow_frac:) ⇒ Object
英字コマンドの振り分け(\text/\mathrm・\frac・空白語・記号/ギリシャ表)。未知は nil。.
-
.escape_html(str) ⇒ Object
出力の HTML 予約文字をエスケープする(< > & のみ。" は属性化しないので不要)。.
-
.parenthesize(html) ⇒ Object
複合式なら括弧で囲む。判定は描画済み HTML から上下付きの中身を除いたうえで、 加減算や除算が残るかを見る(
10<sup>-34</sup>の-は指数の符号であって 項の区切りではない)。生成するタグは // だけなので、 タグ自体に+ - /が現れることはない。. -
.render(latex) ⇒ String?
LaTeX(デリミタ除去済み)を HTML テキストへ変換する。.
- .space_after_big_operators(html) ⇒ Object
-
.wrap_script(scanner, tag, upright:, allow_script:, allow_frac:) ⇒ Object
上/下付き(/)。2 段入れ子は許さない(引数を allow_script: false で読む)。.
Class Method Details
.consume_atom(scanner, upright:, allow_script:, allow_frac:) ⇒ Object
1 アトムを消費して HTML 片を返す(空白は '' か ' ')。未知トークンは nil。
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 135 def consume_atom(scanner, upright:, allow_script:, allow_frac:) if scanner.scan(/\s+/) upright ? ' ' : '' # 数式空白は無視、\text 等の立体テキスト内は保持 elsif (letters = scanner.scan(/[A-Za-z]+/)) upright ? escape_html(letters) : "<i>#{escape_html(letters)}</i>" elsif (digits = scanner.scan(/[0-9]+/)) digits elsif scanner.scan(/\^/) wrap_script(scanner, 'sup', upright:, allow_script:, allow_frac:) elsif scanner.scan(/_/) wrap_script(scanner, 'sub', upright:, allow_script:, allow_frac:) elsif scanner.check(/\{/) consume_brace_group(scanner, upright:, allow_script:, allow_frac:) # 透過グループ elsif scanner.scan(/\\/) consume_command(scanner, upright:, allow_frac:) elsif (op = scanner.scan(OPERATORS)) escape_html(op) elsif (raw = scanner.scan(PASSTHROUGH)) raw # 記号・ギリシャ・日本語はそのままの字で出す(立体) end end |
.consume_brace_group(scanner, upright:, allow_script:, allow_frac:) ⇒ Object
'{ … }' を必須で読み、中身の run を返す(透過)。閉じ '}' が無ければ nil。
238 239 240 241 242 243 244 245 246 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 238 def consume_brace_group(scanner, upright:, allow_script:, allow_frac:) scanner.skip(/\s+/) return nil unless scanner.scan(/\{/) inner = consume_run(scanner, upright:, allow_script:, allow_frac:, stop_at_brace: true) return nil if inner.nil? || !scanner.scan(/\}/) inner end |
.consume_command(scanner, upright:, allow_frac:) ⇒ Object
\command を消費する。\ は消費済みで、続く名前/記号を読む。
166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 166 def consume_command(scanner, upright:, allow_frac:) if (name = scanner.scan(/[A-Za-z]+/)) dispatch_named_command(name, scanner, allow_frac:) elsif scanner.scan(/,/) # \, 細空白 " " elsif scanner.scan(/[;:]/) # \; \: 通常空白 ' ' elsif (esc = scanner.scan(/[%&#_{}$]/)) # \% \& \# \_ \{ \} \$ リテラル escape_html(esc) end # \\ やその他の記号は nil(拒否) end |
.consume_group_or_token(scanner, upright:, allow_script:, allow_frac:) ⇒ Object
上/下付き・\frac の引数('…' グループ or 単一アトム)。先行空白は読み飛ばす。
228 229 230 231 232 233 234 235 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 228 def consume_group_or_token(scanner, upright:, allow_script:, allow_frac:) scanner.skip(/\s+/) if scanner.check(/\{/) consume_brace_group(scanner, upright:, allow_script:, allow_frac:) else consume_atom(scanner, upright:, allow_script:, allow_frac:) end end |
.consume_run(scanner, upright:, allow_script:, allow_frac:, stop_at_brace:) ⇒ Object
トークン列を消費して HTML を組む。stop_at_brace 時はトップレベルの '}' で停止(消費しない)。 1 つでも解釈不能なトークンがあれば nil(=式全体を拒否)。
121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 121 def consume_run(scanner, upright:, allow_script:, allow_frac:, stop_at_brace:) buf = +'' until scanner.eos? break if stop_at_brace && scanner.check(/\}/) piece = consume_atom(scanner, upright:, allow_script:, allow_frac:) return nil if piece.nil? buf << piece end buf end |
.dispatch_named_command(name, scanner, allow_frac:) ⇒ Object
英字コマンドの振り分け(\text/\mathrm・\frac・空白語・記号/ギリシャ表)。未知は nil。
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 180 def dispatch_named_command(name, scanner, allow_frac:) case name when 'text', 'mathrm' consume_brace_group(scanner, upright: true, allow_script: false, allow_frac: false) when 'frac' return nil unless allow_frac numer = consume_brace_group(scanner, upright: false, allow_script: true, allow_frac: false) denom = numer && consume_brace_group(scanner, upright: false, allow_script: true, allow_frac: false) # **括弧が要る。** `\frac{a+b}{2}` を `a+b/2` と書くと `a + (b/2)` に読める # ——意味が変わる。`\frac{1}{2}` のような単項なら付けない。 denom && "#{parenthesize(numer)}/#{parenthesize(denom)}" when 'hat' # `\hat{p}` → `p̂`(結合アクセント U+0302)。統計の推定値でよく出る。 # HTML のテキストなので結合文字がそのまま乗る base = consume_brace_group(scanner, upright: false, allow_script: false, allow_frac: false) base && "#{base}\u0302" when 'sqrt' # 根号は横線を引けないので `√…` にする。中身が複合式なら括弧で範囲を示す # ——`√a+b` は「√a に b を足す」に読めてしまう。 body = consume_brace_group(scanner, upright: false, allow_script: true, allow_frac: false) body && "√#{parenthesize(body)}" when 'quad', 'qquad' ' ' when 'bmod' ' mod ' when 'pmod' inner = consume_brace_group(scanner, upright: true, allow_script: false, allow_frac: false) inner && " (mod #{inner})" when *FUNCTIONS name # 立体で出す(素のままだと 𝑠𝑖𝑛 と 1 文字ずつ斜体になる) else SYMBOLS[name] || GREEK[name] end end |
.escape_html(str) ⇒ Object
出力の HTML 予約文字をエスケープする(< > & のみ。" は属性化しないので不要)。
249 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 249 def escape_html(str) = str.gsub('&', '&').gsub('<', '<').gsub('>', '>') |
.parenthesize(html) ⇒ Object
複合式なら括弧で囲む。判定は描画済み HTML から上下付きの中身を除いたうえで、
加減算や除算が残るかを見る(10<sup>-34</sup> の - は指数の符号であって
項の区切りではない)。生成するタグは // だけなので、
タグ自体に + - / が現れることはない。
220 221 222 223 224 225 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 220 def parenthesize(html) # 上下付きは中身ごと落とす(指数の符号を項の区切りと読まないため)。 # 残りのタグも落とす——`</i>` の `/` を除算と読まないため。 = html.gsub(%r{<su[bp]>.*?</su[bp]>}m, '').gsub(/<[^>]+>/, '') .match?(%r{[+\-−/]}) ? "(#{html})" : html end |
.render(latex) ⇒ String?
LaTeX(デリミタ除去済み)を HTML テキストへ変換する。
100 101 102 103 104 105 106 107 108 109 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 100 def render(latex) src = latex.to_s return nil if src.strip.empty? scanner = StringScanner.new(src) html = consume_run(scanner, upright: false, allow_script: true, allow_frac: true, stop_at_brace: false) return nil if html.nil? || !scanner.eos? # 未消費が残る=解釈できないトークンがあった space_after_big_operators(html) end |
.space_after_big_operators(html) ⇒ Object
115 116 117 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 115 def space_after_big_operators(html) html.gsub(BIG_OPERATOR_WITH_LIMITS) { "#{::Regexp.last_match(1)}\u2009" } end |
.wrap_script(scanner, tag, upright:, allow_script:, allow_frac:) ⇒ Object
上/下付き(/)。2 段入れ子は許さない(引数を allow_script: false で読む)。
158 159 160 161 162 163 |
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 158 def wrap_script(scanner, tag, upright:, allow_script:, allow_frac:) return nil unless allow_script arg = consume_group_or_token(scanner, upright:, allow_script: false, allow_frac:) arg && "<#{tag}>#{arg}</#{tag}>" end |