Skip to content

Instantly share code, notes, and snippets.

@bebeal
Last active March 16, 2026 06:35
Show Gist options
  • Select an option

  • Save bebeal/a91fe018723cb64808f381a706198550 to your computer and use it in GitHub Desktop.

Select an option

Save bebeal/a91fe018723cb64808f381a706198550 to your computer and use it in GitHub Desktop.
Patched matext (LaTeX → Unicode terminal renderer) — adds matrix environments, inline Unicode sub/superscripts, missing punctuation (!), clean inline √ rendering, and non-breaking space centering fix for fraction alignment.

LaTeX Rendering in the Terminal with matext

Render LaTeX math as multiline Unicode text directly in the terminal. No images, no browser — pure Unicode.

Install

sudo pacman -S nim
nimble install matext -y

Binary lands at ~/.nimble/bin/matext. Add to PATH if needed.

Usage

# Pass as argument (careful: zsh eats ! in args, use printf|pipe for factorials)
matext '\frac{a}{b}'

# Pipe via stdin (safest)
printf '%s' '\sum_{n=1}^{\infty} \frac{1}{n!}' | matext

# Force single line
matext -1 '\frac{a}{b}'

Sample equations

m=~/.nimble/bin/matext
printf '%s' 'x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}' | $m
printf '%s' 'f(x) = \sum_{n=0}^{\infty} \frac{f^{(n)}(a)}{n!}(x-a)^n' | $m
printf '%s' 'x_{n+1} = r x_n (1 - x_n)' | $m
printf '%s' 'H(X) = -\sum_{i=1}^{n} p(x_i) \log p(x_i)' | $m
printf '%s' '\begin{pmatrix} a & b \\ c & d \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} = \begin{pmatrix} ax + by \\ cx + dy \end{pmatrix}' | $m
printf '%s' '\det\begin{vmatrix} a & b \\ c & d \end{vmatrix} = ad - bc' | $m
printf '%s' '\frac{1}{1 + \frac{1}{1 + \frac{1}{1 + x}}}' | $m
printf '%s' '\rho \left(\frac{\partial \vec{v}}{\partial t} + \vec{v} \cdot \nabla \vec{v}\right) = -\nabla p + \mu \nabla^2 \vec{v}' | $m
printf '%s' '\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} dx' | $m
printf '%s' '\zeta(s) = \sum_{n=1}^{\infty} \frac{1}{n^s} = \prod_{p} \frac{1}{1 - p^{-s}}' | $m
printf '%s' '\mathbb{N} \subset \mathbb{Z} \subset \mathbb{Q} \subset \mathbb{R} \subset \mathbb{C}' | $m
matext-showcase

Patches applied

The stock matext (2022.11.9) has gaps. Five patches fix them:

1. Missing punctuation characters (!, ;, ?)

! isn't in the parser's character tables, so n! (factorial) causes a parse error.

File: matextpkg/lookup.nim

# Before:
const punctuation = textRects(trfPunctuation, {
  ",": ",",
  ":": ":",
})

# After:
const punctuation = textRects(trfPunctuation, {
  ",": ",",
  ":": ":",
  "!": "!",
  ";": ";",
  "?": "?",
})

2. Matrix environments (\begin{pmatrix}...\end{pmatrix})

Not implemented at all. Added support for pmatrix, bmatrix, Bmatrix, vmatrix, Vmatrix, matrix, smallmatrix, and cases.

File: matextpkg/render.nim

The approach: parse the raw body text between \begin{env} and \end{env} using createParser from honeycomb (handles brace nesting), split by \\ (rows) and & (columns), render each cell independently by calling the expression parser recursively, then assemble into a grid with proper alignment and wrap with big delimiters (bigDelimiter function already existed).

A parseMatrixEnv proc takes env name + delimiter pair and returns a parser, then |-chains all env types together.

3. Inline Unicode subscripts/superscripts for single characters

Stock behavior: subscripts always render multiline (letter on one line, subscript below-right on next line). This looks bad for simple cases like x_i where Unicode has a perfectly good .

File: matextpkg/render.nim

The subscript/superscript lookup tables already had Unicode mappings (i, n, 2², etc.) but only used them in --one-line mode. Two changes:

  1. Added deItalicize function — the parser renders Latin letters as math italic (i𝑖, U+1D456) but the lookup tables use ASCII runes. deItalicize maps math italic codepoints (U+1D434–U+1D467) back to ASCII before table lookup.

  2. In the sub/superscript rendering path: before building multiline layout, try translateIfPossible on both sub and super. If all characters have inline Unicode equivalents and the base is single-line, emit inline (𝑥ᵢ²) instead of stacking.

Big operators (\sum, \prod, etc.) still stack their sub/superscripts vertically — this only affects normal atoms.

4. Square root rendering

Stock used multiline ╲╱ + _ overbar which breaks with inline superscripts (the _ also renders as dashed in some fonts). Changed to clean inline √(expr) for single-line content.

File: matextpkg/render.nim

5. Fraction centering with non-breaking spaces

Stock uses regular spaces (U+0020) for centering numerators/denominators over fraction lines. Some terminal renderers (including Claude Code) strip leading whitespace from lines, causing numerators to appear left-aligned instead of centered.

File: matextpkg/textrect.nim

Changed the default padding character in the center function from ' ' (U+0020) to "\u00A0" (NO-BREAK SPACE, U+00A0). This preserves centering in environments that strip regular leading spaces.

# Before:
func center(s: string, width: Natural, padding = ' '.Rune): string =

# After:
func center(s: string, width: Natural, padding = "\u00A0".runeAt(0)): string =

Rebuild after patching

cd ~/.nimble/pkgs2/matext-*/
nim c -d:release -o:~/.nimble/bin/matext matext.nim
chmod +x ~/.nimble/bin/matext

Shell note

zsh history expansion eats ! in arguments. Either:

  • Pipe with printf '%s' '...' | matext
  • Or set +H to disable history expansion
import ./textrect
import std/sequtils
import std/strformat
import std/tables
import std/unicode
type
Font* = enum
fNone
fItalic
fBold
fScript
fFraktur
fDoubleStruck
LetterType = enum
ltLatin
ltGreek
func textRects(flag: TextRectFlag, table: openArray[(string, string)]): seq[(string, TextRect)] =
table.mapIt((it[0], it[1].toTextRect(flag = flag)))
func rune*(s: string): Rune =
s.runeAt(0)
func `<=`(a, b: Rune): bool {.borrow.}
const bigOperators = textRects(trfBigOperator, {
"\\sum": "∑", "\\prod": "∏", "\\bigotimes": "⨂", "\\bigvee": "⋁",
"\\int": "∫", "\\coprod": "∐", "\\bigoplus": "⨁", "\\bigwedge": "⋀",
"\\iint": "∬", "\\intop": "∫", "\\bigodot": "⨀", "\\bigcap": "⋂",
"\\iiint": "∭", "\\smallint": "∫", "\\biguplus": "⨄", "\\bigcup": "⋃",
"\\oint": "∮", "\\oiint": "∯", "\\oiiint": "∰", "\\bigsqcup": "⨆",
})
const binaryOperators = textRects(trfOperator, {
"+": "+", "-": "−", "*": "∗", "/": "/",
"\\cdot": "⋅", "\\gtrdot": "⋗",
"\\cdotp": "⋅", "\\intercal": "⊺",
"\\centerdot": "⋅", "\\land": "∧", "\\rhd": "⊳",
"\\circ": "∘", "\\leftthreetimes": "⋋", "\\rightthreetimes": "⋌",
"\\amalg": "⨿", "\\circledast": "⊛", "\\ldotp": ".", "\\rtimes": "⋊",
"\\And": "&", "\\circledcirc": "⊚", "\\lor": "∨", "\\setminus": "∖",
"\\ast": "∗", "\\circleddash": "⊝", "\\lessdot": "⋖", "\\smallsetminus": "∖",
"\\barwedge": "⊼", "\\Cup": "⋓", "\\lhd": "⊲", "\\sqcap": "⊓",
"\\bigcirc": "◯", "\\cup": "∪", "\\ltimes": "⋉", "\\sqcup": "⊔",
"\\mod": "bmod", "\\curlyvee": "⋎", "\\times": "×",
"\\boxdot": "⊡", "\\curlywedge": "⋏", "\\mp": "∓", "\\unlhd": "⊴",
"\\boxminus": "⊟", "\\div": "÷", "\\odot": "⊙", "\\unrhd": "⊵",
"\\boxplus": "⊞", "\\divideontimes": "⋇", "\\ominus": "⊖", "\\uplus": "⊎",
"\\boxtimes": "⊠", "\\dotplus": "∔", "\\oplus": "⊕", "\\vee": "∨",
"\\bullet": "∙", "\\doublebarwedge": "⩞", "\\otimes": "⊗", "\\veebar": "⊻",
"\\Cap": "⋒", "\\doublecap": "⋒", "\\oslash": "⊘", "\\wedge": "∧",
"\\cap": "∩", "\\doublecup": "⋓", "\\pm": "±", "\\plusmn": "±", "\\wr": "≀",
"\\forall": "∀", "\\complement": "∁", "\\therefore": "∴", "\\emptyset": "∅",
"\\exists": "∃", "\\subset": "⊂", "\\because": "∵", "\\empty": "∅",
"\\exist": "∃", "\\supset": "⊃", "\\mapsto": "↦", "\\varnothing": "∅",
"\\nexists": "∄", "\\mid": "∣", "\\to": "→", "\\implies": "⟹",
"\\in": "∈", "\\land": "∧", "\\gets": "←", "\\impliedby": "⟸",
"\\isin": "∈", "\\lor": "∨", "\\leftrightarrow": "↔", "\\iff": "⟺",
"\\notin": "∉", "\\ni": "∋", "\\notni": "∌", "\\neg": "¬", "\\lnot": "¬",
"\\circlearrowleft": "↺", "\\leftharpoonup": "↼", "\\rArr": "⇒",
"\\circlearrowright": "↻", "\\leftleftarrows": "⇇", "\\rarr": "→",
"\\curvearrowleft": "↶", "\\leftrightarrow": "↔", "\\restriction": "↾",
"\\curvearrowright": "↷", "\\Leftrightarrow": "⇔", "\\rightarrow": "→",
"\\Darr": "⇓", "\\leftrightarrows": "⇆", "\\Rightarrow": "⇒",
"\\dArr": "⇓", "\\leftrightharpoons": "⇋", "\\rightarrowtail": "↣",
"\\darr": "↓", "\\leftrightsquigarrow": "↭", "\\rightharpoondown": "⇁",
"\\dashleftarrow": "⇠", "\\Lleftarrow": "⇚", "\\rightharpoonup": "⇀",
"\\dashrightarrow": "⇢", "\\longleftarrow": "⟵", "\\rightleftarrows": "⇄",
"\\downarrow": "↓", "\\Longleftarrow": "⟸", "\\rightleftharpoons": "⇌",
"\\Downarrow": "⇓", "\\longleftrightarrow": "⟷", "\\rightrightarrows": "⇉",
"\\downdownarrows": "⇊", "\\Longleftrightarrow": "⟺", "\\rightsquigarrow": "⇝",
"\\downharpoonleft": "⇃", "\\longmapsto": "⟼", "\\Rrightarrow": "⇛",
"\\downharpoonright": "⇂", "\\longrightarrow": "⟶", "\\Rsh": "↱",
"\\gets": "←", "\\Longrightarrow": "⟹", "\\searrow": "↘",
"\\Harr": "⇔", "\\looparrowleft": "↫", "\\swarrow": "↙",
"\\hArr": "⇔", "\\looparrowright": "↬", "\\to": "→",
"\\harr": "↔", "\\Lrarr": "⇔", "\\twoheadleftarrow": "↞",
"\\hookleftarrow": "↩", "\\lrArr": "⇔", "\\twoheadrightarrow": "↠",
"\\hookrightarrow": "↪", "\\lrarr": "↔", "\\Uarr": "⇑",
"\\Lsh": "↰", "\\uArr": "⇑",
"\\mapsto": "↦", "\\uarr": "↑",
"\\nearrow": "↗", "\\uparrow": "↑",
"\\Larr": "⇐", "\\nleftarrow": "↚", "\\Uparrow": "⇑",
"\\lArr": "⇐", "\\nLeftarrow": "⇍", "\\updownarrow": "↕",
"\\larr": "←", "\\nleftrightarrow": "↮", "\\Updownarrow": "⇕",
"\\leadsto": "⇝", "\\nLeftrightarrow": "⇎", "\\upharpoonleft": "↿",
"\\leftarrow": "←", "\\nrightarrow": "↛", "\\upharpoonright": "↾",
"\\Leftarrow": "⇐", "\\nRightarrow": "⇏", "\\upuparrows": "⇈",
"\\leftarrowtail": "↢", "\\nwarrow": "↖",
"\\leftharpoondown": "↽", "\\Rarr": "⇒",
"=": "=", "\\doteqdot": "≑", "\\lessapprox": "⪅", "\\smile": "⌣",
"<": "<", "\\eqcirc": "≖", "\\lesseqgtr": "⋚", "\\sqsubset": "⊏",
"\\eqcolon": ">>> >, ∹", "\\minuscolon": "∹", ">": ">", "\\lesseqqgtr": "⪋", "\\sqsubseteq": "⊑",
"\\Eqcolon": "::: :, −∷", "\\minuscoloncolon": "::: :, −∷", "\\lessgtr": "≶", "\\sqsupset": "⊐",
"\\eqqcolon": "≕", "\\equalscolon": "≕", "\\approx": "≈", "\\lesssim": "≲", "\\sqsupseteq": "⊒",
"\\Eqqcolon": "=∷", "\equalscoloncolon": "=∷", "\\approxcolon": "≈:", "\\ll": "≪", "\\Subset": "⋐",
"\approxcoloncolon": "≈∷", "\\eqsim": "≂", "\\lll": "⋘", "\\subset": "⊂", "\\sub": "⊂",
"\\approxeq": "≊", "\\eqslantgtr": "⪖", "\\llless": "⋘", "\\subseteq": "⊆", "\\sube": "⊆",
"\\asymp": "≍", "\\eqslantless": "⪕", "\\lt": "<", "\\subseteqq": "⫅",
"\\backepsilon": "∍", "\\equiv": "≡", "\\mid": "∣", "\\succ": "≻",
"\\backsim": "∽", "\\fallingdotseq": "≒", "\\models": "⊨", "\\succapprox": "⪸",
"\\backsimeq": "⋍", "\\frown": "⌢", "\\multimap": "⊸", "\\succcurlyeq": "≽",
"\\between": "≬", "\\ge": "≥", "\\origof": "⊶", "\\succeq": "⪰",
"\\bowtie": "⋈", "\\geq": "≥", "\\owns": "∋", "\\succsim": "≿",
"\\bumpeq": "≏", "\\geqq": "≧", "\\parallel": "∥", "\\Supset": "⋑",
"\\Bumpeq": "≎", "\\geqslant": "⩾", "\\perp": "⊥", "\\supset": "⊃",
"\\circeq": "≗", "\\gg": "≫", "\\pitchfork": "⋔", "\\supseteq": "⊇", "\\supe": "⊇",
"\\colonapprox": ":≈", "\\ggg": "⋙", "\\prec": "≺", "\\supseteqq": "⫆",
"\\Colonapprox": "∷≈", "\coloncolonapprox": "∷≈", "\\gggtr": "⋙", "\\precapprox": "⪷", "\\thickapprox": "≈",
"\\coloneq": ":−", "\\colonminus": ":−", "\\gt": ">", "\\preccurlyeq": "≼", "\\thicksim": "∼",
"\\Coloneq": "∷−", "\coloncolonminus": "∷−", "\\gtrapprox": "⪆", "\\preceq": "⪯", "\\trianglelefteq": "⊴",
"\\coloneqq": "≔", "\colonequals": "≔", "\\gtreqless": "⋛", "\\precsim": "≾", "\\triangleq": "≜",
"\\Coloneqq": "∷=", "\coloncolonequals": "∷=", "\\gtreqqless": "⪌", "\\propto": "∝", "\\trianglerighteq": "⊵",
"\\colonsim": ":∼", "\\gtrless": "≷", "\\risingdotseq": "≓", "\\varpropto": "∝",
"\\Colonsim": "∷∼", "\coloncolonsim": "∷∼", "\\gtrsim": "≳", "\\shortmid": "∣", "\\vartriangle": "△",
"\\cong": "≅", "\\imageof": "⊷", "\\shortparallel": "∥", "\\vartriangleleft": "⊲",
"\\curlyeqprec": "⋞", "\\in": "∈", "\\isin": "∈", "\\sim": "∼", "\\vartriangleright": "⊳",
"\\curlyeqsucc": "⋟", "\\Join": "⋈", "\\simcolon": "∼:", "\\vcentcolon": ":", "\\ratio": ":",
"\\dashv": "⊣", "\\le": "≤", "\\simcoloncolon": "∼∷", "\\vdash": "⊢",
"\\dblcolon": "∷", "\coloncolon": "∷", "\\leq": "≤", "\\simeq": "≃", "\\vDash": "⊨",
"\\doteq": "≐", "\\leqq": "≦", "\\smallfrown": "⌢", "\\Vdash": "⊩",
"\\Doteq": "≑", "\\leqslant": "⩽", "\\smallsmile": "⌣", "\\Vvdash": "⊪",
"\\gnapprox": "⪊", "\\ngeqslant": "≱", "\\nsubseteq": "⊈", "\\precneqq": "⪵",
"\\gneq": "⪈", "\\ngtr": "≯", "\\nsubseteqq": "⊈", "\\precnsim": "⋨",
"\\gneqq": "≩", "\\nleq": "≰", "\\nsucc": "⊁", "\\subsetneq": "⊊",
"\\gnsim": "⋧", "\\nleqq": "≰", "\\nsucceq": "⋡", "\\subsetneqq": "⫋",
"\\gvertneqq": "≩", "\\nleqslant": "≰", "\\nsupseteq": "⊉", "\\succnapprox": "⪺",
"\\lnapprox": "⪉", "\\nless": "≮", "\\nsupseteqq": "⊉", "\\succneqq": "⪶",
"\\lneq": "⪇", "\\nmid": "∤", "\\ntriangleleft": "⋪", "\\succnsim": "⋩",
"\\lneqq": "≨", "\\notin": "∉", "\\ntrianglelefteq": "⋬", "\\supsetneq": "⊋",
"\\lnsim": "⋦", "\\notni": "∌", "\\ntriangleright": "⋫", "\\supsetneqq": "⫌",
"\\lvertneqq": "≨", "\\nparallel": "∦", "\\ntrianglerighteq": "⋭", "\\varsubsetneq": "⊊",
"\\ncong": "≆", "\\nprec": "⊀", "\\nvdash": "⊬", "\\varsubsetneqq": "⫋",
"\\ne": "≠", "\\npreceq": "⋠", "\\nvDash": "⊭", "\\varsupsetneq": "⊋",
"\\neq": "≠", "\\nshortmid": "∤", "\\nVDash": "⊯", "\\varsupsetneqq": "⫌",
"\\ngeq": "≱", "\\nshortparallel": "∦", "\\nVdash": "⊮",
"\\ngeqq": "≱", "\\nsim": "≁", "\\precnapprox": "⪹",
})
const delimiters* = textRects(trfNone, {
"(": "(",
"\\lparen": "(",
")": ")",
"\\rparen": ")",
"[": "[",
"\\lbrack": "[",
"]": "]",
"\\rbrack": "]",
"\\{": "{",
"\\lbrace": "{",
"\\}": "}",
"\\rbrace": "}",
"⟨": "⟨",
"\\langle": "⟨",
"\\lang": "⟨",
"⟩": "⟩",
"\\rangle": "⟩",
"\\rang": "⟩",
"|": "∣",
"\\vert": "∣",
"\\|": "∥",
"\\Vert": "∥",
"⌈": "⌈",
"\\lceil": "⌈",
"⌉": "⌉",
"\\rceil": "⌉",
"⌊": "⌊",
"\\lfloor": "⌊",
"⌋": "⌋",
"\\rfloor": "⌋",
"⟦": "⟦",
"\\llbracket": "⟦",
"⟧": "⟧",
"\\rrbracket": "⟧",
})
const extensibleArrowParts* = {
"\\xleftarrow": ("←", "←", "─", "─"),
"\\xLeftarrow": ("⇐", "⇐", "═", "═"),
"\\xleftrightarrow": ("↔", "←", "─", "→"),
"\\xhookleftarrow": ("↩", "←", "─", "╯"),
"\\xtwoheadleftarrow": ("↞", "↞", "─", "─"),
"\\xleftharpoonup": ("↼", "↼", "─", "─"),
"\\xleftharpoondown": ("↽", "↽", "─", "─"),
"\\xleftrightharpoons": ("⇋", "⥪", "═", "⥭"),
"\\xtofrom": ("⇄", "←", "═", "→"),
"\\xlongequal": ("=", "═", "═", "═"),
"\\xrightarrow": ("→", "─", "─", "→"),
"\\xRightarrow": ("⇒", "═", "═", "⇒"),
"\\xLeftrightarrow": ("⇔", "⇐", "═", "⇒"),
"\\xhookrightarrow": ("↪", "╰", "─", "→"),
"\\xtwoheadrightarrow": ("↠", "─", "─", "↠"),
"\\xrightharpoonup": ("⇀", "─", "─", "⇀"),
"\\xrightharpoondown": ("⇁", "─", "─", "⇁"),
"\\xrightleftharpoons": ("⇌", "⥫", "═", "⥬"),
"\\xmapsto": ("↦", "├", "─", "→"),
}
const fontsByName* = {
"\\mathrm": fNone,
"\\mathit": fItalic,
"\\mathbf": fBold,
"\\mathcal": fScript,
"\\mathfrak": fFraktur,
"\\mathbb": fDoubleStruck,
}
const fontStarts* = [
ltLatin: [
fItalic: 119860,
fBold: 119808,
fScript: 119964,
fFraktur: 120068,
fDoubleStruck: 120120,
],
ltGreek: [
fItalic: 120546,
fBold: 120488,
fScript: 0,
fFraktur: 0,
fDoubleStruck: 0,
],
]
const fontExceptions* = [
fItalic: @{rune"h": "ℎ"},
fBold: @[],
fScript: @{
rune"B": "ℬ",
rune"E": "ℰ",
rune"F": "ℱ",
rune"H": "ℋ",
rune"I": "ℐ",
rune"L": "ℒ",
rune"M": "ℳ",
rune"R": "ℛ",
rune"e": "ℯ",
rune"g": "ℊ",
rune"o": "ℴ",
},
fFraktur: @{
rune"C": "ℭ",
rune"H": "ℌ",
rune"I": "ℑ",
rune"R": "ℜ",
rune"Z": "ℨ",
},
fDoubleStruck: @{
rune"C": "ℂ",
rune"H": "ℍ",
rune"N": "ℕ",
rune"P": "ℙ",
rune"Q": "ℚ",
rune"R": "ℝ",
rune"Z": "ℤ",
},
]
func inFont*(letter: Rune, font: Font): string =
if font == fNone:
return $letter
for (lhs, rhs) in fontExceptions[font]:
if letter == lhs:
return rhs
let (typ, shift) = case letter
of rune"A" .. rune"Z": (ltLatin, 65)
of rune"a" .. rune"z": (ltLatin, 71)
of rune"Α" .. rune"Ω": (ltGreek, 913)
of rune"α" .. rune"ω": (ltGreek, 919)
else: raise newException(ValueError, &"Invalid letter: {letter}")
let fontStart = fontStarts[typ][font]
if fontStart == 0:
raise newException(ValueError, &"Letter {letter} can't be rendered in font {font}")
return $Rune(fontStart + letter.ord - shift)
# TODO: make Greek letters italic
const letters = textRects(trfAlnum, {
"\\Alpha": "A", "\\Beta": "B", "\\Gamma": "Γ", "\\Delta": "Δ",
"\\Epsilon": "E", "\\Zeta": "Z", "\\Eta": "H", "\\Theta": "Θ",
"\\Iota": "I", "\\Kappa": "K", "\\Lambda": "Λ", "\\Mu": "M",
"\\Nu": "N", "\\Xi": "Ξ", "\\Omicron": "O", "\\Pi": "Π",
"\\Rho": "P", "\\Sigma": "Σ", "\\Tau": "T", "\\Upsilon": "Υ",
"\\Phi": "Φ", "\\Chi": "X", "\\Psi": "Ψ", "\\Omega": "Ω",
"\\varGamma": "Γ", "\\varDelta": "Δ", "\\varTheta": "Θ", "\\varLambda": "Λ",
"\\varXi": "Ξ", "\\varPi": "Π", "\\varSigma": "Σ", "\\varUpsilon": "Υ",
"\\varPhi": "Φ", "\\varPsi": "Ψ", "\\varOmega": "Ω",
"\\alpha": "α", "\\beta": "β", "\\gamma": "γ", "\\delta": "δ",
"\\epsilon": "ϵ", "\\zeta": "ζ", "\\eta": "η", "\\theta": "θ",
"\\iota": "ι", "\\kappa": "κ", "\\lambda": "λ", "\\mu": "μ",
"\\nu": "ν", "\\xi": "ξ", "\\omicron": "ο", "\\pi": "π",
"\\rho": "ρ", "\\sigma": "σ", "\\tau": "τ", "\\upsilon": "υ",
"\\phi": "ϕ", "\\chi": "χ", "\\psi": "ψ", "\\omega": "ω",
"\\varepsilon": "ε", "\\varkappa": "ϰ", "\\vartheta": "ϑ", "\\thetasym": "ϑ",
"\\varpi": "ϖ", "\\varrho": "ϱ", "\\varsigma": "ς", "\\varphi": "φ",
"\\digamma": "ϝ",
"\\imath": "", "\\nabla": "∇", "\\Im": "ℑ", "\\Reals": "ℝ", "\\OE": "Œ",
"\\jmath": "ȷ", "\\partial": "∂", "\\image": "ℑ", "\\wp": "℘", "\\o": "ø",
"\\aleph": "ℵ", "\\Game": "⅁", "\\Bbbk": "k", "\\weierp": "℘", "\\O": "Ø",
"\\alef": "ℵ", "\\Finv": "Ⅎ", "\\N": "ℕ", "\\Z": "ℤ", "\\ss": "ß",
"\\alefsym": "ℵ", "\\cnums": "ℂ", "\\natnums": "ℕ", "\\aa": "å", "\\i": "ı",
"\\beth": "ℶ", "\\Complex": "ℂ", "\\R": "ℝ", "\\AA": "Å", "\\j": "ȷ",
"\\gimel": "ℷ", "\\ell": "ℓ", "\\Re": "ℜ", "\\ae": "æ",
"\\daleth": "ℸ", "\\hbar": "ℏ", "\\real": "ℜ", "\\AE": "Æ",
"\\eth": "ð", "\\hslash": "ℏ", "\\reals": "ℝ", "\\oe": "œ",
})
const punctuation = textRects(trfPunctuation, {
",": ",",
":": ":",
"!": "!",
";": ";",
"?": "?",
})
const simpleDiacritics* = {
"\\acute": (combining: "\u0301", low: "ˏ"),
"\\bar": (combining: "\u0304", low: "_"),
"\\breve": (combining: "\u0306", low: "⏑"),
"\\check": (combining: "\u030C", low: "ˇ"),
"\\dot": (combining: "\u0307", low: "."),
"\\ddot": (combining: "\u0308", low: "¨"),
"\\grave": (combining: "\u0300", low: "ˎ"),
"\\hat": (combining: "\u0302", low: "ꞈ"),
"\\not": (combining: "\u0338", low: "/"),
"\\tilde": (combining: "\u0303", low: "˷"),
"\\vec": (combining: "\u20D7", low: "→")
}
const superscripts* = {
rune"0": "⁰",
rune"1": "¹",
rune"2": "²",
rune"3": "³",
rune"4": "⁴",
rune"5": "⁵",
rune"6": "⁶",
rune"7": "⁷",
rune"8": "⁸",
rune"9": "⁹",
rune"+": "⁺",
rune"−": "⁻",
rune"=": "⁼",
rune"(": "⁽",
rune")": "⁾",
rune"i": "ⁱ",
rune"n": "ⁿ",
rune" ": "",
}.toTable
const subscripts* = {
rune"0": "₀",
rune"1": "₁",
rune"2": "₂",
rune"3": "₃",
rune"4": "₄",
rune"5": "₅",
rune"6": "₆",
rune"7": "₇",
rune"8": "₈",
rune"9": "₉",
rune"+": "₊",
rune"-": "₋",
rune"=": "₌",
rune"(": "₍",
rune")": "₎",
rune"a": "ₐ",
rune"e": "ₑ",
rune"o": "ₒ",
rune"x": "ₓ",
rune"h": "ₕ",
rune"k": "ₖ",
rune"l": "ₗ",
rune"m": "ₘ",
rune"n": "ₙ",
rune"p": "ₚ",
rune"s": "ₛ",
rune"t": "ₜ",
rune"i": "ᵢ",
rune"r": "ᵣ",
rune"j": "ⱼ",
rune"u": "ᵤ",
rune"β": "ᵦ",
rune"v": "ᵥ",
rune"χ": "ᵪ",
rune"γ": "ᵧ",
rune"ρ": "ᵨ",
rune"φ": "ᵩ",
rune" ": "",
}.toTable
const symbols = textRects(trfNone, {
"\\dots": "…", "\\KaTeX": "K T X\n A E ",
"\\%": "%", "\\cdots": "⋯", "\\LaTeX": "L T X\n A E ",
"\\#": "#", "\\ddots": "⋱", "\\TeX": "T X\n E ",
"\\&": "&", "\\ldots": "…", "\\nabla": "∇",
"\\_": "_", "\\vdots": "⋮", "\\infty": "∞",
"\\textunderscore": "_", "\\dotsb": "⋯", "\\infin": "∞",
"\\--": "–", "\\dotsc": "…", "\\checkmark": "✓",
"\\textendash": "–", "\\dotsi": "⋯", "\\dag": "†",
"\\---": "—", "\\dotsm": "⋯", "\\dagger": "†",
"\\textemdash": "—", "\\dotso": "…", "\\textdagger": "†",
"\\textasciitilde": "~", "\\sdot": "⋅", "\\ddag": "‡",
"\\textasciicircum": "^", "\\mathellipsis": "…", "\\ddagger": "‡",
"`": "‘", "\\textellipsis": "…", "\\textdaggerdbl": "‡",
"\\textquoteleft": "‘", "\\Box": "□", "\\Dagger": "‡",
"\\lq": "‘", "\\square": "□", "\\angle": "∠",
"\\textquoteright": "’", "\\blacksquare": "■", "\\measuredangle": "∡",
"\\rq": "′", "\\triangle": "△", "\\sphericalangle": "∢",
"\\textquotedblleft": "“", "\\triangledown": "▽", "\\top": "⊤",
"\"": "\"", "\\triangleleft": "◃", "\\bot": "⊥",
"\\textquotedblright": "”", "\\triangleright": "▹", "\\$": "$",
"\\colon": ":", "\\bigtriangledown": "▽", "\\textdollar": "$",
"\\backprime": "‵", "\\bigtriangleup": "△", "\\pounds": "£",
"\\prime": "′", "\\blacktriangle": "▲", "\\mathsterling": "£",
"\\textless": "<", "\\blacktriangledown": "▼", "\\textsterling": "£",
"\\textgreater": ">", "\\blacktriangleleft": "◀", "\\yen": "¥",
"\\textbar": "|", "\\blacktriangleright": "▶", "\\surd": "√",
"\\textbardbl": "∥", "\\diamond": "⋄", "\\degree": "°",
"\\textbraceleft": "{", "\\Diamond": "◊", "\\textdegree": "°",
"\\textbraceright": "}", "\\lozenge": "◊", "\\mho": "℧",
"\\textbackslash": "\\", "\\blacklozenge": "⧫", "\\diagdown": "╲",
"\\P": "¶", "\\star": "⋆", "\\diagup": "╱",
"\\S": "§", "\\bigstar": "★", "\\flat": "♭",
"\\sect": "§", "\\clubsuit": "♣", "\\natural": "♮",
"\\copyright": "©", "\\clubs": "♣", "\\sharp": "♯",
"\\circledR": "®", "\\diamondsuit": "♢", "\\heartsuit": "♡",
"\\textregistered": "®", "\\diamonds": "♢", "\\hearts": "♡",
"\\circledS": "Ⓢ", "\\spadesuit": "♠", "\\spades": "♠",
"\\maltese": "✠", "\\minuso": "⦵",
})
const textOperators = textRects(trfWord, {
"\\arcsin": "arcsin", "\\cosec": "cosec", "\\deg": "deg", "\\sec": "sec",
"\\arccos": "arccos", "\\cosh": "cosh", "\\dim": "dim", "\\sin": "sin",
"\\arctan": "arctan", "\\cot": "cot", "\\exp": "exp", "\\sinh": "sinh",
"\\arctg": "arctg", "\\cotg": "cotg", "\\hom": "hom", "\\sh": "sh",
"\\arcctg": "arcctg", "\\coth": "coth", "\\ker": "ker", "\\tan": "tan",
"\\arg": "arg", "\\csc": "csc", "\\lg": "lg", "\\tanh": "tanh",
"\\ch": "ch", "\\ctg": "ctg", "\\ln": "ln", "\\tg": "tg",
"\\cos": "cos", "\\cth": "cth", "\\log": "log", "\\th": "th",
"\\argmax": "arg max", "\\injlim": "inj lim", "\\min": "min",
"\\argmin": "arg min", "\\lim": "lim", "\\plim": "plim",
"\\det": "det", "\\liminf": "lim inf", "\\Pr": "Pr",
"\\gcd": "gcd", "\\limsup": "lim sup", "\\projlim": "proj lim",
"\\inf": "inf", "\\max": "max", "\\sup": "sup",
})
func isCommand(key: string): bool =
key[0] == '\\' and (key[1] in 'a'..'z' or key[1] in 'A'..'Z')
const commands* = block:
var commands = initTable[string, TextRect]()
proc extractCommands(table: openArray[(string, TextRect)]) =
for (key, val) in table:
if key.isCommand:
commands[key[1..^1]] = val
extractCommands bigOperators
extractCommands binaryOperators
extractCommands delimiters
extractCommands letters
extractCommands punctuation
extractCommands symbols
extractCommands textOperators
commands
const nonCommands* = block:
var nonCommands = newSeq[(string, TextRect)]()
proc extractNonCommands(table: openArray[(string, TextRect)]) =
for (key, val) in table:
if not key.isCommand:
nonCommands.add((key, val))
extractNonCommands bigOperators
extractNonCommands binaryOperators
extractNonCommands delimiters
extractNonCommands letters
extractNonCommands punctuation
extractNonCommands symbols
extractNonCommands textOperators
nonCommands
import ./lookup
import ./textrect
import honeycomb
import std/options
import std/sequtils
import std/strformat
import std/strutils
import std/sugar
import std/tables
import std/unicode
proc render*(latex: string, oneLine = false): string =
func bigDelimiter(delimiter: string, height, baseline: Natural): TextRect =
const delimiterParts = {
"(": ("⎛", "⎜", "⎝"),
")": ("⎞", "⎟", "⎠"),
"[": ("⎡", "⎢", "⎣"),
"]": ("⎤", "⎥", "⎦"),
"∣": ("│", "│", "│"),
"∥": ("║", "║", "║"),
"⌈": ("⎡", "⎢", "⎢"),
"⌉": ("⎤", "⎥", "⎥"),
"⌊": ("⎢", "⎢", "⎣"),
"⌋": ("⎥", "⎥", "⎦"),
"⟦": ("╓", "║", "╙"),
"⟧": ("╖", "║", "╜"),
}.toTable
result.rows = newSeq[string](height)
result.width = 1
result.baseline = baseline
case delimiter
of "{":
if height == 2:
result.rows[0] = "⎰"
result.rows[1] = "⎱"
else:
result.rows[0] = "⎧"
for i in 1 ..< height - 1:
result.rows[i] = "⎪"
result.rows[height div 2] = "⎨"
result.rows[^1] = "⎩"
of "}":
if height == 2:
result.rows[0] = "⎱"
result.rows[1] = "⎰"
else:
result.rows[0] = "⎫"
for i in 1 ..< height - 1:
result.rows[i] = "⎪"
result.rows[height div 2] = "⎬"
result.rows[^1] = "⎭"
of "⟨":
result.width = (height + 1) div 2
let widthDec = result.width - 1
if height mod 2 == 1:
result.rows[height div 2] = "⟨" & " ".repeat(height div 2)
for i in 0..<height div 2:
result.rows[i] = " ".repeat(widthDec - i) & "╱" & " ".repeat(i)
result.rows[height - 1 - i] = " ".repeat(widthDec - i) & "╲" & " ".repeat(i)
of "⟩":
result.width = (height + 1) div 2
let widthDec = result.width - 1
if height mod 2 == 1:
result.rows[height div 2] = " ".repeat(height div 2) & "⟩"
for i in 0..<height div 2:
result.rows[i] = " ".repeat(i) & "╲" & " ".repeat(widthDec - i)
result.rows[height - 1 - i] = " ".repeat(i) & "╱" & " ".repeat(widthDec - i)
else:
let (top, mid, bottom) = delimiterParts[delimiter]
result.rows[0] = top
for i in 1 ..< height - 1:
result.rows[i] = mid
result.rows[^1] = bottom
let ws = whitespace.many
var atom = fwdcl[TextRect]()
let expr = atom.many.map(atoms => atoms.join)
let alpha = c('A'..'Z') | c('a'..'z')
let digit = c('0'..'9').map(ch => ($ch).toTextRectOneLine(0, trfAlnum))
let latinLetter = alpha.map(letter => letter.Rune.inFont(fItalic).toTextRectOneLine(0, trfAlnum)) |
fontsByName.map(pair => (
let (name, font) = pair
((s(name) >> ws >> c('{') >> ws >> alpha << ws << c('}')) |
(s(name) >> whitespace.atLeast(1) >> alpha))
.map(letter => letter.Rune.inFont(font).toTextRectOneLine(0, trfAlnum))
)).foldl(a | b)
let delimiter =
delimiters.map(entry => (
let (key, val) = entry
if key[0] == '\\':
(s(key) << !letter).result(val)
else:
s(key).result(val))
).foldr(a | b)
let command = (c('\\') >> letter.atLeast(1)).map(chars => chars.join).validate(name => name in commands, "a command").map(name => commands[name])
let nonCommand =
nonCommands.map(entry => (
let (key, val) = entry
s(key).result(val))
).foldr(a | b)
let simpleDiacritic = simpleDiacritics.map(entry => (
let (key, val) = entry
(s(key) >> !letter >> atom).map(rect => (
var rect = rect
if rect.width == 1 and rect.height == 1:
rect.rows[0] &= val.combining
rect
elif oneLine:
fmt"{val.combining} ({rect.row})".toTextRectOneLine
else:
stack(val.low.toTextRectOneLine, rect, 1 + rect.baseline, saCenter)
))
)).foldr(a | b)
let frac = (s"\frac" | s"\tfrac" | s"\dfrac" | s"\cfrac") >> !letter >> (atom & atom).map(fraction => (
let numerator = fraction[0]
let denominator = fraction[1]
if oneLine:
fmt"{numerator.rowAsAtom} / {denominator.rowAsAtom}".toTextRectOneLine
else:
let width = max(numerator.width, denominator.width)
var fractionLine = "─".repeat(width)
var flag = trfFraction
if (numerator.flag == trfFraction and numerator.width == width) or
(denominator.flag == trfFraction and denominator.width == width):
fractionLine = "╶" & fractionLine & "╴"
flag = trfNone
stack(numerator, fractionLine.toTextRectOneLine, denominator, numerator.height, saCenter).withFlag(flag)
))
let binom = (s"\binom" | s"\tbinom" | s"\dbinom" | s"\cbinom") >> !letter >> (atom & atom).map(nk => (
let n = nk[0]
let k = nk[1]
if oneLine:
fmt"C({n.row}, {k.row})".toTextRectOneLine
else:
let inside = stack(n, k, n.height, saCenter)
join(
bigDelimiter("(", inside.height, inside.baseline),
inside,
bigDelimiter(")", inside.height, inside.baseline),
)
))
let sqrt = s"\sqrt" >> !letter >> atom.map(arg => (
("√" & arg.rowAsAtom).toTextRectOneLine(arg.baseline)
))
let boxed = s"\boxed" >> !letter >> atom.map(arg => (
if oneLine:
fmt"[{arg.row}]".toTextRectOneLine
else:
let horizontal = "─".repeat(arg.width).toTextRectOneLine
let sandwich = stack(horizontal, arg, horizontal, arg.baseline + 1, saLeft)
var left: TextRect
left.rows = newSeq[string](sandwich.height)
left.width = 1
left.baseline = sandwich.baseline
for i in 1 ..< sandwich.height - 1:
left.rows[i] = "│"
var right = left
left.rows[0] = "┌"
left.rows[^1] = "└"
right.rows[0] = "┐"
right.rows[^1] = "┘"
join(left, sandwich, right)
))
let extensibleArrow = extensibleArrowParts.map(entry => (
let (key, val) = entry
let (one, left, middle, right) = val
(s(key) >> !letter >> atom).map(rect => (
var rect = rect
(if oneLine:
fmt"{one}[{rect.row}]".toTextRectOneLine
elif rect.width <= 1:
stack(rect, one.toTextRectOneLine, rect.height, saCenter)
else:
var arrow = left.toTextRectOneLine
for _ in 0 ..< rect.width - 2:
arrow &= middle.toTextRectOneLine
arrow &= right.toTextRectOneLine
stack(rect, arrow, rect.height, saCenter)
).withFlag(trfOperator)
))
)).foldr(a | b)
let leftright = (s"\left" >> ws >> delimiter & (ws >> expr) & (s"\right" >> ws >> delimiter)).map(things => (
let inside = things[1]
var left = things[0]
var right = things[2]
if inside.height > 1:
left = left.row.bigDelimiter(inside.height, inside.baseline)
right = right.row.bigDelimiter(inside.height, inside.baseline)
join(left, inside, right)
))
# Extract raw body text between \begin{env} and \end{env}, handling brace nesting
let matrixBody = createParser(string):
var depth = 0
var i = 0
var body = ""
while i < input.len:
if i + 4 < input.len and input[i..i+3] == "\\end":
break
if input[i] == '{':
depth.inc
elif input[i] == '}':
depth.dec
body.add input[i]
i.inc
succeed(input, body, input[i..^1])
# Parse a matrix environment, render cells, build grid with delimiters
proc parseMatrixEnv(envName, leftDelim, rightDelim: string, completeExprParser: Parser[TextRect]): Parser[TextRect] =
(s("\\begin{" & envName & "}") >> ws >> matrixBody << s("\\end{" & envName & "}")).map(
proc(body: string): TextRect =
# Split body into rows by \\, then cells by &
let rowStrs = body.split("\\\\")
var rows: seq[seq[TextRect]]
for rowStr in rowStrs:
let cellStrs = rowStr.split("&")
var cells: seq[TextRect]
for cellStr in cellStrs:
let trimmed = strutils.strip(cellStr)
if trimmed.len > 0:
let parsed = completeExprParser.parse(trimmed)
if parsed.kind == success:
cells.add parsed.value
else:
cells.add trimmed.toTextRectOneLine
else:
cells.add " ".toTextRectOneLine
rows.add cells
# Build grid
var numCols = 0
for row in rows:
if row.len > numCols:
numCols = row.len
if numCols == 0:
return " ".toTextRectOneLine
var colWidths = newSeq[int](numCols)
var rowHeights = newSeq[int](rows.len)
var rowBaselines = newSeq[int](rows.len)
for r, row in rows:
for ci, cell in row:
if cell.width > colWidths[ci]:
colWidths[ci] = cell.width
if cell.height > rowHeights[r]:
rowHeights[r] = cell.height
if cell.baseline > rowBaselines[r]:
rowBaselines[r] = cell.baseline
if rowHeights[r] == 0:
rowHeights[r] = 1
var inside: TextRect
inside.rows = newSeq[string](0)
inside.width = 0
for r, row in rows:
for h in 0 ..< rowHeights[r]:
var line = ""
for ci in 0 ..< numCols:
if ci > 0:
line &= " "
var cell: TextRect
if ci < row.len and not row[ci].isEmpty:
cell = row[ci]
else:
cell = " ".toTextRectOneLine
if cell.height < rowHeights[r]:
cell = cell.extendUp(rowBaselines[r] - cell.baseline)
if cell.height < rowHeights[r]:
cell = cell.extendDown(rowHeights[r] - cell.height)
let pad = colWidths[ci] - cell.width
let leftPad = pad div 2
let rightPad = pad - leftPad
line &= " ".repeat(leftPad) & cell.rows[h] & " ".repeat(rightPad)
inside.rows.add line
inside.width = inside.rows[0].runeLen
inside.baseline = inside.rows.len div 2
for i in 0 ..< inside.rows.len:
let rlen = inside.rows[i].runeLen
if rlen < inside.width:
inside.rows[i] &= " ".repeat(inside.width - rlen)
if leftDelim == "" and rightDelim == "":
inside
elif rightDelim == "":
join(bigDelimiter(leftDelim, inside.height, inside.baseline), inside)
else:
join(
bigDelimiter(leftDelim, inside.height, inside.baseline),
inside,
bigDelimiter(rightDelim, inside.height, inside.baseline),
)
)
let matrixEnv =
parseMatrixEnv("pmatrix", "(", ")", expr) |
parseMatrixEnv("bmatrix", "[", "]", expr) |
parseMatrixEnv("Bmatrix", "{", "}", expr) |
parseMatrixEnv("vmatrix", "∣", "∣", expr) |
parseMatrixEnv("Vmatrix", "∥", "∥", expr) |
parseMatrixEnv("matrix", "", "", expr) |
parseMatrixEnv("smallmatrix", "(", ")", expr) |
parseMatrixEnv("cases", "{", "", expr)
let bracedExpr = c('{') >> expr << c('}')
let atom1 = (
bracedExpr |
leftright |
matrixEnv |
digit |
latinLetter |
command |
nonCommand |
simpleDiacritic |
frac |
binom |
sqrt |
boxed |
extensibleArrow
) << ws
func deItalicize(r: Rune): Rune =
let o = r.ord
if o >= 119886 and o <= 119911: Rune(o - 119886 + ord('a')) # math italic a-z
elif o >= 119860 and o <= 119885: Rune(o - 119860 + ord('A')) # math italic A-Z
else: r
func translateIfPossible(str: string, table: Table[Rune, string]): Option[string] =
let runes = str.runes.toSeq
let deItalicized = runes.mapIt(it.deItalicize)
if deItalicized.allIt(it in table):
return some(deItalicized.mapIt(table[it]).join)
if runes.allIt(it in table):
return some(runes.mapIt(table[it]).join)
let superscript = (c('^') >> atom1).map(sup => sup.withFlag(trfSup)) |
c('\'').atLeast(1).map(primes => "′".repeat(primes.len).toTextRectOneLine.withFlag(trfSup))
let subscript = (c('_') >> atom1).map(sub => sub.withFlag(trfSub))
atom.become ws >> (atom1 & ((superscript & subscript.optional) | (subscript & superscript.optional)).optional).map(operands => (
var base = operands[0]
let flag = base.flag
base.flag = trfNone
(case operands.len
of 1:
base
of 3:
let (sup, sub) =
if operands[1].flag == trfSup:
(operands[1], operands[2])
else:
(operands[2], operands[1])
if oneLine:
var str = base.row
if not sub.isEmpty:
str.add sub.row.translateIfPossible(subscripts).get("_" & sub.rowAsAtom)
if not sup.isEmpty:
str.add sup.row.translateIfPossible(superscripts).get("^" & sup.rowAsAtom)
str.toTextRectOneLine
elif flag in {trfBigOperator, trfWord}:
stack(sup, base, sub, base.baseline + sup.height, saCenter)
else:
# Use inline Unicode sub/superscripts when possible (single-line base and translatable)
let supInline = if sup.isEmpty: some("") else: sup.row.translateIfPossible(superscripts)
let subInline = if sub.isEmpty: some("") else: sub.row.translateIfPossible(subscripts)
if base.height == 1 and supInline.isSome and subInline.isSome:
(base.row & supInline.get & subInline.get).toTextRectOneLine(0, base.flag)
else:
base & stack(sup.extendDown(base.height), sub, base.baseline + sup.height, saLeft)
else:
TextRect.default
).withFlag(flag)
))
let completeExpr = expr << eof
let parsed = completeExpr.parse(latex)
if parsed.kind == success:
$parsed.value
else:
let (lnNum, colNum) = parsed.lineInfo
let showing = " " & latex.splitLines[lnNum - 1] & "\n" & ' '.repeat(colNum + 3) & "^"
raise newException(ValueError, &"Parse error at line {lnNum}, column {colNum}\n{showing}")
from std/strutils import join, repeat, split
import std/math
import std/sequtils
import std/sugar
import std/unicode
type
TextRectFlag* = enum
trfNone
trfAlnum
trfOperator
trfBigOperator
trfWord
trfFraction
trfSub
trfSup
trfPunctuation
TextRect* = object
rows*: seq[string]
baseline*: int
width*: Natural
flag*: TextRectFlag
StackAlignment* = enum
saCenter
saLeft
saRight
using rect: TextRect
func `$`*(rect): string =
rect.rows.join("\n")
func row*(rect): string =
rect.rows[0]
func rowAsAtom*(rect): string =
if rect.rows[0].runeLen == 1:
rect.rows[0]
else:
"(" & rect.rows[0] & ")"
func height*(rect): Natural =
rect.rows.len
func isEmpty*(rect): bool =
rect.height == 0
func toTextRect*(s: string, baseline: int = 0, flag = trfNone): TextRect =
if s == "":
result.flag = flag
return
result.rows = s.split("\n")
result.width = result.rows[0].runeLen
for row in result.rows:
if row.runeLen != result.width:
raise newException(ValueError, "All rows of a TextRect must be the same width")
result.baseline = baseline
result.flag = flag
func toTextRectOneLine*(s: string, baseline: int = 0, flag = trfNone): TextRect =
if s == "":
result.flag = flag
return
result.rows = @[s]
result.width = s.runeLen
result.baseline = baseline
result.flag = flag
func extendUp*(rect; num: Natural): TextRect =
result.rows = sequtils.repeat(' '.repeat(rect.width), num) & rect.rows
result.baseline = rect.baseline + num
result.width = rect.width
func extendDown*(rect; num: Natural): TextRect =
result.rows = rect.rows & sequtils.repeat(' '.repeat(rect.width), num)
result.baseline = rect.baseline
result.width = rect.width
func extendLeft(rect: var TextRect) =
for row in rect.rows.mitems:
row = " " & row
rect.width.inc
func extendRight(rect: var TextRect) =
for row in rect.rows.mitems:
row &= " "
rect.width.inc
func join*(rects: varargs[TextRect]): TextRect =
var rects = rects.toSeq
rects.keepItIf(not it.isEmpty)
if rects == @[]:
return
if rects.len == 1:
return rects[0]
for i, rect in rects.mpairs:
case rect.flag
of trfPunctuation:
if i != rects.high:
rect.extendRight
of trfOperator, trfBigOperator:
if i != rects.high:
rect.extendRight
if i != rects.low and rects[i - 1].flag notin {trfOperator, trfBigOperator, trfPunctuation}:
rect.extendLeft
of trfWord:
if i != rects.high and rects[i + 1].flag in {trfAlnum, trfWord}:
rect.extendRight
if i != rects.low and rects[i - 1].flag in {trfAlnum}:
rect.extendLeft
else:
discard
let maxBaseline = rects.mapIt(it.baseline).max
rects.applyIt(it.extendUp(maxBaseline - it.baseline))
let maxHeight = rects.mapIt(it.height).max
rects.applyIt(it.extendDown(maxHeight - it.height))
result.rows = newSeq[string](rects[0].height)
for i, row in result.rows.mpairs:
row = rects.mapIt(it.rows[i]).join
result.baseline = rects[0].baseline
result.width = rects.mapIt((it.width)).sum
func `&`*(left, right: TextRect): TextRect =
join(left, right)
func `&=`*(left: var TextRect, right: TextRect) =
left = join(left, right)
func center(s: string, width: Natural, padding = "\u00A0".runeAt(0)): string =
let sLen = s.runeLen
if sLen >= width:
s
else:
let diff = width - sLen
let left = diff div 2
let right = diff - left
padding.repeat(left) & s & padding.repeat(right)
const alignFuncs = [
saCenter: (s: string, width: int) => s.center(width),
saLeft: (s: string, width: int) => s.alignLeft(width),
saRight: (s: string, width: int) => s.align(width),
]
proc stack*(rects: varargs[TextRect], baseline: int, alignment: StackAlignment): TextRect =
let width = max(rects.mapIt(it.width))
let alignFunc = alignFuncs[alignment]
for rect in rects:
for row in rect.rows:
result.rows.add alignFunc(row, width)
result.baseline = baseline
result.width = width
func withFlag*(rect; flag: TextRectFlag): TextRect =
result = rect
result.flag = flag
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment