Typst-equivalent of Unicodeit

For LaTeX, there is the website https://www.unicodeit.net/, which converts LaTeX math expressions into easily-copy-pastable plaintext. This is tremendously useful when corresponding with people about mathy stuff via email or plaintext chat.

Does anyone know of an eqiuvalent for typst syntax? For example, one would input {x^2 | x in RR} and get out “{x² | x ∈ ℝ}”. Google turns up nothing useful, except for a mac-app that compiles typst locally to PDF (Typstit: Drag Typst formulas into Keynote and other apps).

If this does not exist yet, I would consider building this, likely by forking Unicodeit (their license is permissive) and swapping out their symbol table with one that I somehow generate from the typst compiler. (Alternatively, I could try using typst’s HTML export and converting that, but that seems more fragile, cursed, and seems to be more work).

3 Likes

There is also the open issue #2041 Plaintext export, which would make this near-trivial to solve.

Been trying to code a plaintext exporter the past couple days, but unsurprisingly, complex layouting within plaintext is hard.

I’m now aware of Lynchpin: WIP terminal export for typst which (from the examples) looks way more competent and complete than my code, and should easily be able to be used to implement a website once published.

1 Like

Actually, I think lynchpin isn’t tailored for this specific use case. It focuses strictly on the terminal grid, achieving complex layouts by positioning elements within character cells. A dedicated package for single-line Unicode export is still highly worth developing, as it could benefit from a much simpler architectural model.
Are you reusing typst’s rust crates? Maybe we can share some ideas.

So my project’s scope was set way too high, instead of just having a simple “inline maths to plaintext” converter (even using regexes like unicodeit would have done most things fine), I did want to essentially implement plaintext as an export target. That also meant block level maths, for which I had the same ideas that you had: Several lines per block, exponents span two lines, large delimiters use the fancy unicode characters across several lines.

That was most of what I had worked on so far, and which in retrospect seems much more difficult to pull off than simple inline-maths export. So I think I’ll scratch my project and start over, reusing whatever I can (the project had a comfortable setup for integration tests at least :smile:), as your project is far more advanced and robust.

As to reusing rust-crates: I did want to reuse rust crates instead of forking them. I hooked into typst-eval::eval, though typst-realize may have been better. I couldn’t call on typst-layout, because that would give me a finished layout with floating-point coordinates and everything, which was unsuitable for plaintext. Of course, forking would have access to the inner working of typst-layout and dispense with that.

If I do the simple inline-maths exporter, typst-eval may be sufficiently strong.

I do wonder if it would not be better to try and implement an exporter for mathml, and eventually connect it up to typst’s html export.

I feel like (with some manual effort of defining how to translate everything this could be done from within typst:

unicodeit.typ
#let tostring(m) = {
  let tosuperscript(str) = {
    let map = (
      "0": "⁰",
      "1": "¹",
      "2": "²",
      "3": "³",
      "4": "⁴",
      "5": "⁵",
      "6": "⁶",
      "7": "⁷",
      "8": "⁸",
      "9": "⁹",
      i: "ⁱ",
      n: "ⁿ",
      "+": "⁺",
      "-": "⁻",
      "=": "⁼",
      "(": "⁽",
      ")": "⁾",
      " ": " ",
    )
    let res = ""
    for c in str.clusters() {
      if not c in map { return "^(" + str + ")" }
      res = res + map.at(c)
    }
    res
  }
  let tosubscript(str) = {
    let map = (
      "0": "₀",
      "1": "₁",
      "2": "₂",
      "3": "₃",
      "4": "₄",
      "5": "₅",
      "6": "₆",
      "7": "₇",
      "8": "₈",
      "9": "₉",
      "+": "₊",
      "-": "₋",
      "=": "₌",
      "(": "₍",
      ")": "₎",
      " ": "",
    )
    let res = ""
    for c in str.clusters() {
      if not c in map { return "_(" + str + ")" }
      res = res + map.at(c)
    }
    res
  }

  let inner(arr) = {
    arr = if type(arr) == array { arr } else { (arr,) }
    arr
      .map(c => {
        if c.has("text") {
          c.text
        } else if c == [ ] {
          " "
        } else if c.has("children") {
          inner(c.children)
        } else if c.has("body") {
          if c.body.has("children") {
            inner(c.body.children)
          } else {
            inner(c.body)
          }
        } else if c.func() == math.attach and c.fields().keys() == ("base", "t") {
          inner(c.base) + tosuperscript(inner(c.t))
        } else if c.func() == math.attach and c.fields().keys() == ("base", "b") {
          inner(c.base) + tosubscript(inner(c.b))
        } else if c.func() == math.frac {
          tosuperscript(inner(c.num)) + "⁄" + tosubscript(inner(c.denom))
        } else if c.func() == math.root {
          if not c.has("index") {
            "√" + inner(c.radicand)
          } else if c.index == [3] {
            "∛" + inner(c.radicand)
          } else if c.index == [4] {
            "∜" + inner(c.radicand)
          } else {
            tosuperscript(inner(c.index)) + "√" + inner(c.radicand)
          }
        } else {
          repr(c)
        }
      })
      .join()
  }

  if m.body.has("children") {
    inner(m.body.children)
  } else if m.body.has("text") {
    inner(m.body)
  } else {
    repr(m.body)
  }
}

#metadata(tostring(eval("$" + sys.inputs.at("m", default: "\"no math provided\"") + "$")))<stringified>
$ typst query unicodeit.typ --input m="f(x) := x^2 + phi Gamma_3 sqrt(5^n) root(3,2) root(+,2) + 2/3" "<stringified>" --field value --one
"f(x) ≔ x² + φ Γ₃ √5ⁿ ∛2 ⁺√2 + ²⁄₃"

Of course, there are many more functions that should be handled, for fractions one could use the precomposed where availiable, etc.

3 Likes