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' => ''
}.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)。< > & は出力時にエスケープする。

%r{[+\-=/<>()\[\]|,.:;!]}

Class Method Summary collapse

Class Method Details

.consume_atom(scanner, upright:, allow_script:, allow_frac:) ⇒ Object

1 アトムを消費して HTML 片を返す(空白は '' か ' ')。未知トークンは nil。



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 90

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)
  end
end

.consume_brace_group(scanner, upright:, allow_script:, allow_frac:) ⇒ Object

'{ … }' を必須で読み、中身の run を返す(透過)。閉じ '}' が無ければ nil。



161
162
163
164
165
166
167
168
169
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 161

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 を消費する。\ は消費済みで、続く名前/記号を読む。



119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 119

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 単一アトム)。先行空白は読み飛ばす。



151
152
153
154
155
156
157
158
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 151

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(=式全体を拒否)。



76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 76

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。



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 133

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)
    denom && "#{numer}/#{denom}"
  when 'quad', 'qquad'
    ' '
  else
    SYMBOLS[name] || GREEK[name]
  end
end

.escape_html(str) ⇒ Object

出力の HTML 予約文字をエスケープする(< > & のみ。" は属性化しないので不要)。



172
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 172

def escape_html(str) = str.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')

.render(latex) ⇒ String?

LaTeX(デリミタ除去済み)を HTML テキストへ変換する。

Parameters:

  • latex (String)

    $…$ / (…) を剥いだ LaTeX 本文

Returns:

  • (String, nil)

    HTML 文字列(変換可能時)/ nil(サブセット外・空)



63
64
65
66
67
68
69
70
71
72
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 63

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? # 未消費が残る=解釈できないトークンがあった

  html
end

.wrap_script(scanner, tag, upright:, allow_script:, allow_frac:) ⇒ Object

上/下付き(/)。2 段入れ子は許さない(引数を allow_script: false で読む)。



111
112
113
114
115
116
# File 'lib/vivlio_starter/cli/build/math_text_renderer.rb', line 111

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