Module: RubyRich::Markdown::TerminalConverter::LatexConverter

Defined in:
lib/ruby_rich/markdown.rb

Overview

—- LaTeX to Unicode converter —- Translates common LaTeX math commands to Unicode characters for terminal display. Handles Greek letters, big operators, frac, sqrt, super/subscript, cases, and ~150 common symbols.

Constant Summary collapse

SYMBOLS =

— big lookup table ———————————— Format: “\command” => “unicode_char”

{
  # Greek lowercase
  'alpha' => 'α', 'beta' => 'β', 'gamma' => 'γ',
  'delta' => 'δ', 'epsilon' => 'ε', 'varepsilon' => 'ɛ',
  'zeta' => 'ζ', 'eta' => 'η', 'theta' => 'θ',
  'vartheta' => 'ϑ', 'iota' => 'ι', 'kappa' => 'κ',
  'lambda' => 'λ', 'mu' => 'μ', 'nu' => 'ν',
  'xi' => 'ξ', 'pi' => 'π', 'varpi' => 'ϖ',
  'rho' => 'ρ', 'varrho' => 'ϱ', 'sigma' => 'σ',
  'varsigma' => 'ς', 'tau' => 'τ', 'upsilon' => 'υ',
  'phi' => 'φ', 'varphi' => 'ϕ', 'chi' => 'χ',
  'psi' => 'ψ', 'omega' => 'ω',
  # Greek uppercase
  'Gamma' => 'Γ', 'Delta' => 'Δ', 'Theta' => 'Θ',
  'Lambda' => 'Λ', 'Xi' => 'Ξ', 'Pi' => 'Π',
  'Sigma' => 'Σ', 'Upsilon' => 'Υ', 'Phi' => 'Φ',
  'Psi' => 'Ψ', 'Omega' => 'Ω',
  # Relations
  'leq' => '', 'geq' => '', 'neq' => '',
  'equiv' => '', 'approx' => '', 'sim' => '',
  'simeq' => '', 'propto' => '', 'll' => '',
  'gg' => '', 'doteq' => '', 'prec' => '',
  'succ' => '', 'preceq' => '', 'succeq' => '',
  'subset' => '', 'supset' => '', 'subseteq' => '',
  'supseteq' => '', 'in' => '', 'ni' => '',
  'notin' => '', 'perp' => '', 'parallel' => '',
  # Binary operators
  'times' => '×', 'div' => '÷', 'cdot' => '·',
  'pm' => '±', 'mp' => '', 'oplus' => '',
  'ominus' => '', 'otimes' => '', 'oslash' => '',
  'odot' => '', 'circ' => '', 'bullet' => '',
  'cap' => '', 'cup' => '', 'setminus' => '',
  'land' => '', 'lor' => '', 'wedge' => '',
  'vee' => '', 'star' => '',
  # Arrows
  'to' => '', 'rightarrow' => '', 'Rightarrow' => '',
  'leftarrow' => '', 'Leftarrow' => '',
  'leftrightarrow' => '', 'Leftrightarrow' => '',
  'mapsto' => '', 'longmapsto' => '',
  'uparrow' => '', 'downarrow' => '',
  'longrightarrow' => '', 'Longrightarrow' => '',
  # Big operators
  'sum' => '', 'prod' => '', 'coprod' => '',
  'int' => '', 'iint' => '', 'iiint' => '',
  'oint' => '', 'bigcup' => '', 'bigcap' => '',
  'bigvee' => '', 'bigwedge' => '',
  # Misc symbols
  'infty' => '', 'partial' => '', 'nabla' => '',
  'forall' => '', 'exists' => '', 'nexists' => '',
  'emptyset' => '', 'varnothing' => '',
  'Re' => '', 'Im' => '', 'aleph' => '',
  'ell' => '', 'hbar' => '', 'wp' => '',
  'angle' => '', 'triangle' => '', 'triangledown' => '',
  'square' => '', 'Box' => '', 'diamond' => '',
  'clubsuit' => '', 'diamondsuit' => '',
  'heartsuit' => '', 'spadesuit' => '',
  'ldots' => '', 'cdots' => '', 'vdots' => '',
  'ddots' => '', 'dots' => '',
  'cong' => '', 'models' => '', 'mid' => '',
  'nmid' => '', 'therefore' => '', 'because' => '',
  'neg' => '¬', 'lnot' => '¬', 'top' => '', 'bot' => '',
  'degree' => '°', 'prime' => '', 'dag' => '',
  'ddag' => '', 'S' => '§', 'P' => '',
  'pound' => '£', 'euro' => '', 'yen' => '¥',
  'copyright' => '©', 'circledR' => '®',
  # Delimiters – strip LaTeX wrapper
  'left' => '', 'right' => '', 'bigl' => '', 'bigr' => '',
  'Bigl' => '', 'Bigr' => '', 'biggl' => '', 'biggr' => '',
  # Arrows special
  'gets' => '',
  # Text sub/sup scripts
  'text' => '',
}.freeze
TEXT_LIKE =

Commands whose argument should be preserved verbatim (e.g. textabc)

%w[text textrm textsf texttt textbf textit].freeze
SUPERSCRIPTS =
{
  '0' => '', '1' => '¹', '2' => '²', '3' => '³', '4' => '',
  '5' => '', '6' => '', '7' => '', '8' => '', '9' => '',
  '+' => '', '-' => '', '=' => '', '(' => '', ')' => '',
  'i' => '', 'n' => '',
}.freeze
SUBSCRIPTS =
{
  '0' => '', '1' => '', '2' => '', '3' => '', '4' => '',
  '5' => '', '6' => '', '7' => '', '8' => '', '9' => '',
  '+' => '', '-' => '', '=' => '', '(' => '', ')' => '',
  'a' => '', 'e' => '', 'i' => '', 'j' => '',
  'n' => '', 'x' => '',
}.freeze

Class Method Summary collapse

Class Method Details

.convert(formula) ⇒ Object



736
737
738
739
740
741
742
743
744
745
746
747
# File 'lib/ruby_rich/markdown.rb', line 736

def self.convert(formula)
  return formula if formula.nil? || formula.strip.empty?

  result = formula.dup
  result = process_frac(result)
  result = process_sqrt(result)
  result = process_cases(result)
  result = process_scripts(result)
  result = replace_symbols(result)
  result = strip_delim_spacing(result)
  result
end

.process_cases(text) ⇒ Object

begincases … endcases → ⎧ … ⎨ … ⎩ …



771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
# File 'lib/ruby_rich/markdown.rb', line 771

def self.process_cases(text)
  text.gsub(/\\begin\{cases\}(.*?)\\end\{cases\}/m) do
    body = Regexp.last_match(1).strip
    lines = body.split('\\\\').map(&:strip).reject(&:empty?)
    return '{}' if lines.empty?
    out = +""
    lines.each_with_index do |line, i|
      leader = case i
               when 0 then ''
               when lines.length - 1 then ''
               else ''
               end
      out << "#{leader} #{line.gsub('&', '')}\n"
    end
    out.strip
  end
end

.process_frac(text) ⇒ Object

fracnumden → (num)/(den) or num/den when single-char



750
751
752
753
754
755
756
757
758
# File 'lib/ruby_rich/markdown.rb', line 750

def self.process_frac(text)
  text.gsub(/\\frac\s*\{([^{}]*(?:\{[^}]*\}[^{}]*)*)\}\s*\{([^{}]*(?:\{[^}]*\}[^{}]*)*)\}/) do
    num = Regexp.last_match(1)
    den = Regexp.last_match(2)
    num_wrap = num.length > 1 ? "(#{num})" : num
    den_wrap = den.length > 1 ? "(#{den})" : den
    "#{num_wrap}/#{den_wrap}"
  end
end

.process_scripts(text) ⇒ Object

^x / _x → Unicode super/subscript



790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
# File 'lib/ruby_rich/markdown.rb', line 790

def self.process_scripts(text)
  # ^{...}
  text = text.gsub(/\^\{([^}]+)\}/) {
    inner = Regexp.last_match(1)
    inner.include?('\\') ? "^\{#{inner}\}" : script_chars(inner, SUPERSCRIPTS)
  }
  # _{...}
  text = text.gsub(/_\{([^}]+)\}/) {
    inner = Regexp.last_match(1)
    inner.include?('\\') ? "_\{#{inner}\}" : script_chars(inner, SUBSCRIPTS)
  }
  # ^x  (single non-whitespace char, not \ or {)
  text = text.gsub(/\^([^\s\\{])/) { SUPERSCRIPTS[Regexp.last_match(1)] || "^#{Regexp.last_match(1)}" }
  # _x  (single non-whitespace char, not \ or {)
  text = text.gsub(/_([^\s\\{])/) { SUBSCRIPTS[Regexp.last_match(1)] || "_#{Regexp.last_match(1)}" }
  text
end

.process_sqrt(text) ⇒ Object

sqrtx → √(x) sqrtx → ⁿ√(x)



761
762
763
764
765
766
767
768
# File 'lib/ruby_rich/markdown.rb', line 761

def self.process_sqrt(text)
  text.gsub(/\\sqrt(?:\[([^\]]*)\])?\s*\{([^{}]*(?:\{[^}]*\}[^{}]*)*)\}/) do
    degree = Regexp.last_match(1)
    radicand = Regexp.last_match(2)
    prefix = degree ? script_chars(degree, SUPERSCRIPTS) : ''
    "#{prefix}√(#{radicand})"
  end
end

.replace_symbols(text) ⇒ Object

Replace command tokens with Unicode equivalents.



813
814
815
816
817
818
819
820
# File 'lib/ruby_rich/markdown.rb', line 813

def self.replace_symbols(text)
  # Handle \text{…} first – keep content, remove wrapper
  text = text.gsub(/\\(text\w*)\s*\{(.*?)\}/) { Regexp.last_match(2) }
  # Replace all other \commands
  text.gsub(/\\([a-zA-Z]+)/) { |m|
    SYMBOLS[Regexp.last_match(1)] || m
  }
end

.script_chars(str, map) ⇒ Object



808
809
810
# File 'lib/ruby_rich/markdown.rb', line 808

def self.script_chars(str, map)
  str.each_char.map { |c| map[c] || c }.join
end

.strip_delim_spacing(text) ⇒ Object

Remove stray spaces inserted by left / right.



823
824
825
826
827
828
# File 'lib/ruby_rich/markdown.rb', line 823

def self.strip_delim_spacing(text)
  text.gsub(/\(\s+/, '(').gsub(/\s+\)/, ')')
      .gsub(/\[\s+/, '[').gsub(/\s+\]/, ']')
      .gsub(/\{\s+/, '{').gsub(/\s+\}/, '}')
      .gsub(/\\s+/, ' ')
end