7shi n [F#] MathML to TeX

Created by 7shi n last modified
// This file is licensed under the CC0.

// [Optional]
// If you want to use MimeTeX, please download MimeTeX.dll from:
// http://www.shitalshah.com/dev/eq2img_all.zip

#r "System"
#r "System.Drawing"
#r "System.Windows.Forms"
#r "System.Xml"

open System
open System.IO
open System.Text
open System.Runtime.InteropServices
open System.Drawing
open System.Drawing.Imaging
open System.Windows.Forms
open System.Xml

// MathML to TeX translator

let eachNode (xr:XmlReader) f =
    let rec loop i =
        if xr.Read() && xr.NodeType <> XmlNodeType.EndElement && f i then
            loop <| i + 1
    if not <| xr.IsEmptyElement then loop 0

let attr (xr:XmlReader) (n:string) d =
    let a = xr.GetAttribute n
    if a <> null then a else d

let group (s:string) =
    if s.Length = 1 then s else "{" + s.Trim() + "}"

let fenced s = match s with | "" -> "." | "{" | "}" | "[" | "]" -> @"\" + s | s -> s

let convch = function
| '⋯'|'…' -> @"\cdots"  | '⋮' -> @"\vdots"      | '⋱' -> @"\ddots"      | '⋅' -> @"\cdot"
| '∙' -> @"\bullet"     | '×' -> @"\times"      | '÷' -> @"\div"        | '⊈' -> @"\not\subseteq"
| '≠' -> @"\neq"        | '≡' -> @"\equiv"      | '≢' -> @"\not\equiv"  | '⊉' -> @"\not\supseteq"
| '±' -> @"\pm"         | '∓' -> @"\mp"         | '∞' -> @"\infty"      | '∝' -> @"\propto"
| '~' -> @"\sim"        | '≃' -> @"\simeq"      | '≅' -> @"\cong"       | '≈' -> @"\approx"
| '≤' -> @"\leq"        | '≥' -> @"\geq"        | '≪' -> @"\ll"         | '≫' -> @"\gg"
| '¬' -> @"\neg"        | '∅' -> @"\emptyset"   | '∪' -> @"\cup"        | '∩' -> @"\cap"
| '⊂' -> @"\subset"     | '⊃' -> @"\supset"     | '⊆' -> @"\subseteq"   | '⊇' -> @"\supseteq"
| '∀' -> @"\forall"     | '∃' -> @"\exists"     | '∈' -> @"\in"         | '∋' -> @"\ni"
| '∧' -> @"\wedge"      | '∨' -> @"\vee"        | '⊕' -> @"\oplus"      | '⊗' -> @"\otimes"
| '⇑' -> @"\Uparrow"    | '⇓' -> @"\Downarrow"  | '⇐' -> @"\Leftarrow"  | '⇒' -> @"\Rightarrow"
| '∂' -> @"\partial"    | '∆' -> @"\triangle"   | '∇' -> @"\nabla"
| '←' -> @"\leftarrow"  | '→' -> @"\rightarrow" | '↑' -> @"\uparrow"    | '↓' -> @"\downarrow"
| '∉' -> @"\notin"      | '∌' -> @"\not\ni"     | '↕' -> @"\updownarrow"| '↔' -> @"\leftrightarrow"
| '⊄' -> @"\not\subset" | '⊅' -> @"\not\supset" | '⇕' -> @"\Updownarrow"| '⇔' -> @"\Leftrightarrow"
| 'ℏ' -> @"\hbar"       | '↦' -> @"\mapsto"     | '†' -> @"\dagger"     | '‡' -> @"\ddagger"
| '∑' -> @"\sum"        | '∏' -> @"\prod"       | '∫' -> @"\int"        | '∮' -> @"\oint"
| '⋃' -> @"\bigcup"     | '⋂' -> @"\bigcap"     | '⋁' -> @"\bigvee"     | '⋀' -> @"\bigwedge"
| 'ℵ' -> @"\aleph"      | 'ℶ' -> @"\beth"       | 'ℷ' -> @"\gimel"      | 'ℸ' -> @"\daleth"
| 'Α' -> "A"            | 'Β' -> "B"            | 'Γ' -> @"\Gamma"      | 'Δ' -> @"\Delta"
| 'Ε' -> "E"            | 'Ζ' -> "Z"            | 'Η' -> "H"            | 'Θ' -> @"\Theta"
| 'Ι' -> "I"            | 'Κ' -> "K"            | 'Λ' -> @"\Lambda"     | 'Μ' -> "M"
| 'Ν' -> "N"            | 'Ξ' -> @"\Xi"         | 'Ο' -> "O"            | 'Π' -> @"\Pi"
| 'Ρ' -> "P"            | 'Σ' -> @"\Sigma"      | 'Τ' -> "T"            | 'Υ' -> @"\Upsilon"
| 'Φ' -> @"\Phi"        | 'Χ' -> "X"            | 'Ψ' -> @"\Psi"        | 'Ω' -> @"\Omega"
| 'α' -> @"\alpha"      | 'β' -> @"\beta"       | 'γ' -> @"\gamma"      | 'δ' -> @"\delta"
| 'ε' -> @"\varepsilon" | 'ϵ' -> @"\epsilon"    | 'ζ' -> @"\zeta"       | 'η' -> @"\eta"
| 'θ' -> @"\theta"      | 'ϑ' -> @"\vartheta"   | 'ι' -> @"\iota"       | 'κ' -> @"kappa"
| 'λ' -> @"\lambda"     | 'μ' -> @"\mu"         | 'ν' -> @"\nu"         | 'ξ' -> @"\xi"
| 'ο' -> "o"            | 'π' -> @"\pi"         | 'ϖ' -> @"\varpi"      | 'ρ' -> @"\rho"
| 'ϱ' -> @"\varrho"     | 'σ' -> @"\sigma"      | 'ς' -> @"\varsigma"   | 'τ' -> @"\tau"
| 'υ' -> @"\upsilon"    | 'φ' -> @"\varphi"     | 'ϕ' -> @"\phi"        | 'χ' -> @"\chi"
| 'ψ' -> @"\psi"        | 'ω' -> @"\omega"
| ch  -> ""

let conv expr =
    let sb = new StringBuilder()
    for ch in expr do
        let s = convch ch
        if s = "" then
            sb.Append ch |> ignore
        else
            sb.Append s |> ignore
            if s.[0] = '\\' then sb.Append ' ' |> ignore
    sb.ToString()

let mutable mathJax = false

let rec math max (xr:XmlReader) (tw:TextWriter) =
    let str max =
        use sw = new StringWriter()
        math max xr sw
        sw.ToString()
    let strt max = (str max).Trim()
    let writegrp max = str max |> group |> tw.Write
    let writebase (sep:string) max =
        let s = strt 1
        if mathJax then group s else
            match s with
            | @"\int"  -> @"{\Bigint}"
            | @"\oint" -> @"{\Bigoint}"
            | @"\sum"  -> @"\Bigsum"
            | @"\prod" -> @"\Bigprod"
            | _ when s.StartsWith @"\big" -> @"\B" + s.[2..]
            | _ -> group s
        |> tw.Write
        tw.Write sep
        writegrp max
    let rec mml = function
    | "mml:math"
    | "mml:mrow"
    | "mml:mtd"
    | "mml:maligngroup"
    | "mml:mn" (* number *) ->
        math 0 xr tw
    | "mml:mo" (* operator *) ->
        match str 0 with | "\u2061" -> () | s -> tw.Write s
    | "mml:mi" (* variable *) ->
        let v = xr.GetAttribute("mathvariant")
        let s = str 0
        match s with
        | "sin" | "cos" | "tan" | "arcsin" | "arccos" | "arctan"
        | "sinh" | "cosh" | "tanh" | "exp" | "log" | "ln" ->
            @"\" + s
        | _ ->
            match v with
            | "fraktur" ->
                match s with
                | "R" -> @"\Re"
                | "I" -> @"\Im"
                | _   -> @"\mathfrak{" + s + "}"
            | "script" -> @"\script{" + s + "}"
            | "double-struck" -> @"\mathbb{" + s + "}"
            | "bold" | "bold-italic" -> @"\bf{" + s + "}"
            | _ -> s
        |> tw.Write
    | "mml:mfrac" ->
        let s1 = strt 1
        fprintf tw @"\frac{%s}{%s}" s1 (strt 0)
    | "mml:msqrt" ->
        fprintf tw @"\sqrt{%s}" (strt 0)
    | "mml:mroot" ->
        let s1 = strt 1
        fprintf tw @"\sqrt[%s]{%s}" (strt 0) s1
    | "mml:msub" ->
        writebase "_" 0
    | "mml:msup" ->
        writebase "^" 0
    | "mml:msubsup"
    | "mml:munderover" ->
        writebase "_" 1; tw.Write "^"; writegrp 0
    | "mml:munder" ->
        let s1 = strt 1
        let s2 = str 0 |> group
        match s1 with
        | "lim" | "max" | "min" -> fprintf tw @"\%s_%s" s1 s2
        | _ when s1.StartsWith(@"\underbrace{") ->
            fprintf tw "%s_%s" s1 (group s2)
        | _ ->
            match s2 with
            | "⏟" -> fprintf tw @"\underbrace{%s}" s1
            | _ -> printfn "? mml:munder %s_%s" (group s1) s2
                   fprintf tw "%s_%s" (group s1) s2
    | "mml:mover" ->
        let s1 = strt 1
        let s2 = strt 0
        match s2 with
        | "˙" -> fprintf tw @"\dot{%s}" s1
        | "¨" -> fprintf tw @"\ddot{%s}" s1
        | "¯" -> fprintf tw @"\bar{%s}" s1
        | "^" -> fprintf tw @"\hat{%s}" s1
        | @"\sim" -> fprintf tw @"\tilde{%s}" s1
        | @"\rightarrow" -> fprintf tw @"\vec{%s}" s1
        | _ -> printfn "? mml:mover '%s'" s2
               fprintf tw "%s^%s" (group s1) (group s2)
    | "mml:mfenced" ->
        let op, cl = attr xr "open" "(", attr xr "close" ")"
        fprintf tw @"\left%s"  (fenced op)
        math 0 xr tw
        fprintf tw @"\right%s" (fenced cl)
    | "mml:mtable" ->
        fprintf tw @"\begin{matrix}"
        eachNode xr <| fun i ->
            if i > 0 then fprintf tw @"\\"
            mml xr.Name
            true
        fprintf tw @"\end{matrix}"
    | "mml:mtr" ->
        eachNode xr <| fun i ->
            if i > 0 then fprintf tw "&"
            mml xr.Name
            true
    | t ->
        printfn "? <%s>" t
        math 0 xr tw
    eachNode xr <| fun i ->
        if xr.NodeType = XmlNodeType.Element then
            mml xr.Name else tw.Write (conv xr.Value)
        i + 1 <> max

let mml2tex mml =
    if String.IsNullOrEmpty mml then "" else
    use xr = new XmlTextReader(new StringReader(mml))
    use sw = new StringWriter()
    math 0 xr sw
    sw.ToString()

// GUI

let CF_ENHMETAFILE = 14
[<DllImport("user32")>] extern bool OpenClipboard(nativeint hWndNewOwner)
[<DllImport("user32")>] extern bool IsClipboardFormatAvailable(int wFormat)
[<DllImport("user32")>] extern nativeint GetClipboardData(int wFormat)
[<DllImport("user32")>] extern int CloseClipboard()
[<DllImport("gdi32" )>] extern int GetEnhMetaFileBits(nativeint hemf, int cbBuffer, byte[] lpbBuffer)
let getClipboardEMF() =
    if not <| OpenClipboard(nativeint 0) then null else
    try try
            if not <| IsClipboardFormatAvailable CF_ENHMETAFILE then null else
            let hemf = GetClipboardData CF_ENHMETAFILE
            let buf = Array.zeroCreate<byte>(GetEnhMetaFileBits(hemf, 0, null))
            GetEnhMetaFileBits(hemf, buf.Length, buf) |> ignore
            use ms = new MemoryStream(buf)
            new Metafile(ms)
        with _ -> null
    finally
        CloseClipboard() |> ignore

[<DllImport("MimeTeX")>] extern int CreateGifFromEq(string expr, string fileName)
let createImageFromEq expr =
    if String.IsNullOrEmpty expr then null else
    let temp = Path.GetTempFileName()
    try try
            if CreateGifFromEq(expr, temp) <> 0 then null else
            use ms = new MemoryStream(File.ReadAllBytes temp)
            Image.FromStream ms
        with e -> null
    finally
        File.Delete temp

[<EntryPoint; STAThread>] do
let w = new Form(Text = "MathML to TeX")
let pn1 = new Panel(Dock = DockStyle.Top)
let b = new Button(Text = "Convert from Clipboard", Dock = DockStyle.Fill)
let cb = new CheckBox(Text = "MathJax", AutoSize = true, Dock = DockStyle.Right)
let sp1 = new SplitContainer(Dock = DockStyle.Fill,
                             Orientation = Orientation.Horizontal)
let sp2 = new SplitContainer(Dock = DockStyle.Fill,
                             Orientation = Orientation.Horizontal)
let sp3 = new SplitContainer(Dock = DockStyle.Fill,
                             Orientation = Orientation.Vertical)
let t1 = new TextBox(Multiline = true,
                     ScrollBars = ScrollBars.Both,
                     Dock = DockStyle.Fill)
let t2 = new TextBox(Multiline = true,
                     ScrollBars = ScrollBars.Both,
                     Dock = DockStyle.Fill)
let pn2 = new Panel(BorderStyle = BorderStyle.Fixed3D, Dock = DockStyle.Fill)
let pn3 = new Panel(BorderStyle = BorderStyle.Fixed3D, Dock = DockStyle.Fill)
let lb1 = new Label(Text = "Office", TextAlign = ContentAlignment.MiddleCenter,
                    BorderStyle = BorderStyle.Fixed3D, Dock = DockStyle.Bottom)
let lb2 = new Label(Text = "MimeTeX", TextAlign = ContentAlignment.MiddleCenter,
                    BorderStyle = BorderStyle.Fixed3D, Dock = DockStyle.Bottom)
sp3.Panel1.Controls.AddRange [| pn2; lb1 |]
sp3.Panel2.Controls.AddRange [| pn3; lb2 |]
sp2.Panel1.Controls.Add t2
sp2.Panel2.Controls.Add sp3
sp1.Panel1.Controls.Add t1
sp1.Panel2.Controls.Add sp2
pn1.Controls.AddRange [| b; cb |]
w.Controls.AddRange [| sp1; pn1 |]
pn1.Height <- lb1.Height
sp2.SplitterDistance <- 40
sp3.SplitterDistance <- w.ClientSize.Width / 2

pn2.Resize.Add <| fun _ -> pn2.Invalidate()
pn3.Resize.Add <| fun _ -> pn3.Invalidate()

let img1 : Image ref = ref null
let img1zoom = ref 6
pn2.Paint.Add <| fun e ->
    if !img1 = null then () else
    let r = pn2.ClientSize
    let w, h = (!img1).Width / !img1zoom, (!img1).Height / !img1zoom
    e.Graphics.DrawImage(!img1, (r.Width - w) / 2, (r.Height - h) / 2, w, h)

let img2 : Image ref = ref null
pn3.Paint.Add <| fun e ->
    if !img2 = null then () else
    let r = pn3.ClientSize
    let w, h = (!img2).Width, (!img2).Height
    e.Graphics.DrawImage(!img2, (r.Width - w) / 2, (r.Height - h) / 2, w, h)

let selectAllHandler = new KeyEventHandler(fun tb e ->
    if e.KeyData = (Keys.Control ||| Keys.A) then
        (tb :?> TextBox).SelectAll()
        e.Handled <- true)
t1.KeyDown.AddHandler selectAllHandler
t2.KeyDown.AddHandler selectAllHandler

let convTeX (_:EventArgs) =
    mathJax <- cb.Checked
    t2.Text <- mml2tex t1.Text
cb.Click.Add convTeX
t1.TextChanged.Add convTeX

t2.TextChanged.Add <| fun _ ->
    if !img2 <> null then (!img2).Dispose()
    img2 := createImageFromEq t2.Text
    pn3.Invalidate()

b.Click.Add <| fun _ ->
    let ms = Clipboard.GetData("MathML") :?> MemoryStream
    if ms = null then () else
    use sr = new StreamReader(ms)
    let header = sr.ReadLine()
    img1zoom := if header.Contains "UTF-16" then 6 else 12
    let mml = sr.ReadToEnd()
    t1.Text <- mml
    if !img1 <> null then (!img1).Dispose()
    img1 := getClipboardEMF() :> Image
    pn2.Invalidate()

Application.EnableVisualStyles()
Application.Run w

Comments (0)