feat: referentiel projets dynamique + validation nommage by design
Durcit la convention de nommage des projets (dérive constatée : 'Sliding Automation', 'code_versioning'... au lieu des formes canoniques). - trilium_api.py : projets_canoniques() lit le référentiel = valeurs du label projet sur les notes de type=projet (source unique, pas de constante en dur). Note-projet CodeVersioning créée (manquait). - mcp_server.py : _valider_projet() branché dans les 6 tools de création (add_decision/history/backlog, new_conversation, create_entite, add_skill). Refuse un projet non canonique (suggestion si faute) ou inconnu (renvoi au processus de création de projet). Ne verrouille pas si référentiel illisible. - lint_audit.py : VAL-nommage aligné sur le référentiel (attrape casse, espace ET snake_case ; l'ancien 'contient un espace' ratait code_versioning). - Données : 79 notes ré-étiquetées vers les 3 formes canoniques. Quality by design : l'erreur de nommage devient impossible à l'écriture, le Lint n'est plus que le filet de sécurité.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from importlib import metadata
|
||||
|
||||
__version__ = metadata.version("latex2mathml")
|
||||
@@ -0,0 +1,507 @@
|
||||
from collections import OrderedDict, defaultdict
|
||||
from typing import Optional
|
||||
|
||||
OPENING_BRACE = "{"
|
||||
CLOSING_BRACE = "}"
|
||||
BRACES = "{}"
|
||||
|
||||
OPENING_BRACKET = "["
|
||||
CLOSING_BRACKET = "]"
|
||||
BRACKETS = "[]"
|
||||
|
||||
OPENING_PARENTHESIS = "("
|
||||
CLOSING_PARENTHESIS = ")"
|
||||
PARENTHESES = "()"
|
||||
|
||||
SUBSUP = "_^"
|
||||
SUBSCRIPT = "_"
|
||||
SUPERSCRIPT = "^"
|
||||
APOSTROPHE = "'"
|
||||
PRIME = r"\prime"
|
||||
DPRIME = r"\dprime"
|
||||
|
||||
LEFT = r"\left"
|
||||
MIDDLE = r"\middle"
|
||||
RIGHT = r"\right"
|
||||
|
||||
ABOVE = r"\above"
|
||||
ABOVEWITHDELIMS = r"\abovewithdelims"
|
||||
ATOP = r"\atop"
|
||||
ATOPWITHDELIMS = r"\atopwithdelims"
|
||||
BINOM = r"\binom"
|
||||
BRACE = r"\brace"
|
||||
BRACK = r"\brack"
|
||||
CFRAC = r"\cfrac"
|
||||
CHOOSE = r"\choose"
|
||||
DBINOM = r"\dbinom"
|
||||
DFRAC = r"\dfrac"
|
||||
FRAC = r"\frac"
|
||||
GENFRAC = r"\genfrac"
|
||||
OVER = r"\over"
|
||||
TBINOM = r"\tbinom"
|
||||
TFRAC = r"\tfrac"
|
||||
|
||||
ROOT = r"\root"
|
||||
SQRT = r"\sqrt"
|
||||
|
||||
OVERSET = r"\overset"
|
||||
UNDERSET = r"\underset"
|
||||
|
||||
ACUTE = r"\acute"
|
||||
BAR = r"\bar"
|
||||
BREVE = r"\breve"
|
||||
CHECK = r"\check"
|
||||
DOT = r"\dot"
|
||||
DDOT = r"\ddot"
|
||||
DDDOT = r"\dddot"
|
||||
DDDDOT = r"\ddddot"
|
||||
GRAVE = r"\grave"
|
||||
HAT = r"\hat"
|
||||
MATHRING = r"\mathring"
|
||||
OVERBRACE = r"\overbrace"
|
||||
OVERLEFTARROW = r"\overleftarrow"
|
||||
OVERLEFTRIGHTARROW = r"\overleftrightarrow"
|
||||
OVERLINE = r"\overline"
|
||||
OVERPAREN = r"\overparen"
|
||||
OVERRIGHTARROW = r"\overrightarrow"
|
||||
TILDE = r"\tilde"
|
||||
UNDERBRACE = r"\underbrace"
|
||||
UNDERLEFTARROW = r"\underleftarrow"
|
||||
UNDERLINE = r"\underline"
|
||||
UNDERPAREN = r"\underparen"
|
||||
UNDERRIGHTARROW = r"\underrightarrow"
|
||||
UNDERLEFTRIGHTARROW = r"\underleftrightarrow"
|
||||
VEC = r"\vec"
|
||||
WIDEHAT = r"\widehat"
|
||||
WIDETILDE = r"\widetilde"
|
||||
XLEFTARROW = r"\xleftarrow"
|
||||
XRIGHTARROW = r"\xrightarrow"
|
||||
|
||||
HREF = r"\href"
|
||||
TEXT = r"\text"
|
||||
TEXTBF = r"\textbf"
|
||||
TEXTIT = r"\textit"
|
||||
TEXTRM = r"\textrm"
|
||||
TEXTSF = r"\textsf"
|
||||
TEXTTT = r"\texttt"
|
||||
|
||||
BEGIN = r"\begin"
|
||||
END = r"\end"
|
||||
|
||||
LIMITS = r"\limits"
|
||||
INTEGRAL = r"\int"
|
||||
SUMMATION = r"\sum"
|
||||
PRODUCT = r"\prod"
|
||||
LIMIT = (r"\lim", r"\sup", r"\inf", r"\max", r"\min")
|
||||
|
||||
OPERATORNAME = r"\operatorname"
|
||||
|
||||
LBRACE = r"\{"
|
||||
|
||||
FUNCTIONS = (
|
||||
r"\arccos",
|
||||
r"\arcsin",
|
||||
r"\arctan",
|
||||
r"\cos",
|
||||
r"\cosh",
|
||||
r"\cot",
|
||||
r"\coth",
|
||||
r"\csc",
|
||||
r"\deg",
|
||||
r"\dim",
|
||||
r"\exp",
|
||||
r"\hom",
|
||||
r"\ker",
|
||||
r"\ln",
|
||||
r"\lg",
|
||||
r"\log",
|
||||
r"\sec",
|
||||
r"\sin",
|
||||
r"\sinh",
|
||||
r"\tan",
|
||||
r"\tanh",
|
||||
)
|
||||
DETERMINANT = r"\det"
|
||||
GCD = r"\gcd"
|
||||
INTOP = r"\intop"
|
||||
INJLIM = r"\injlim"
|
||||
LIMINF = r"\liminf"
|
||||
LIMSUP = r"\limsup"
|
||||
PR = r"\Pr"
|
||||
PROJLIM = r"\projlim"
|
||||
MOD = r"\mod"
|
||||
PMOD = r"\pmod"
|
||||
BMOD = r"\bmod"
|
||||
|
||||
HDASHLINE = r"\hdashline"
|
||||
HLINE = r"\hline"
|
||||
HFIL = r"\hfil"
|
||||
|
||||
CASES = r"\cases"
|
||||
DISPLAYLINES = r"\displaylines"
|
||||
SMALLMATRIX = r"\smallmatrix"
|
||||
SUBSTACK = r"\substack"
|
||||
SPLIT = r"\split"
|
||||
ALIGN = r"\align*"
|
||||
MATRICES = (
|
||||
r"\matrix",
|
||||
r"\matrix*",
|
||||
r"\pmatrix",
|
||||
r"\pmatrix*",
|
||||
r"\bmatrix",
|
||||
r"\bmatrix*",
|
||||
r"\Bmatrix",
|
||||
r"\Bmatrix*",
|
||||
r"\vmatrix",
|
||||
r"\vmatrix*",
|
||||
r"\Vmatrix",
|
||||
r"\Vmatrix*",
|
||||
r"\array",
|
||||
SUBSTACK,
|
||||
CASES,
|
||||
DISPLAYLINES,
|
||||
SMALLMATRIX,
|
||||
SPLIT,
|
||||
ALIGN,
|
||||
)
|
||||
|
||||
BACKSLASH = "\\"
|
||||
CARRIAGE_RETURN = r"\cr"
|
||||
|
||||
COLON = r"\:"
|
||||
COMMA = r"\,"
|
||||
DOUBLEBACKSLASH = r"\\"
|
||||
ENSPACE = r"\enspace"
|
||||
EXCLAMATION = r"\!"
|
||||
GREATER_THAN = r"\>"
|
||||
HSKIP = r"\hskip"
|
||||
HSPACE = r"\hspace"
|
||||
KERN = r"\kern"
|
||||
MKERN = r"\mkern"
|
||||
MSKIP = r"\mskip"
|
||||
MSPACE = r"\mspace"
|
||||
NEGTHINSPACE = r"\negthinspace"
|
||||
NEGMEDSPACE = r"\negmedspace"
|
||||
NEGTHICKSPACE = r"\negthickspace"
|
||||
NOBREAKSPACE = r"\nobreakspace"
|
||||
SPACE = r"\space"
|
||||
THINSPACE = r"\thinspace"
|
||||
QQUAD = r"\qquad"
|
||||
QUAD = r"\quad"
|
||||
SEMICOLON = r"\;"
|
||||
|
||||
BLACKBOARD_BOLD = r"\Bbb"
|
||||
BOLD_SYMBOL = r"\boldsymbol"
|
||||
MIT = r"\mit"
|
||||
OLDSTYLE = r"\oldstyle"
|
||||
SCR = r"\scr"
|
||||
TT = r"\tt"
|
||||
|
||||
MATH = r"\math"
|
||||
MATHBB = r"\mathbb"
|
||||
MATHBF = r"\mathbf"
|
||||
MATHCAL = r"\mathcal"
|
||||
MATHFRAK = r"\mathfrak"
|
||||
MATHIT = r"\mathit"
|
||||
MATHRM = r"\mathrm"
|
||||
MATHSCR = r"\mathscr"
|
||||
MATHSF = r"\mathsf"
|
||||
MATHTT = r"\mathtt"
|
||||
|
||||
BOXED = r"\boxed"
|
||||
FBOX = r"\fbox"
|
||||
HBOX = r"\hbox"
|
||||
MBOX = r"\mbox"
|
||||
|
||||
COLOR = r"\color"
|
||||
DISPLAYSTYLE = r"\displaystyle"
|
||||
TEXTSTYLE = r"\textstyle"
|
||||
SCRIPTSTYLE = r"\scriptstyle"
|
||||
SCRIPTSCRIPTSTYLE = r"\scriptscriptstyle"
|
||||
STYLE = r"\style"
|
||||
|
||||
HPHANTOM = r"\hphantom"
|
||||
PHANTOM = r"\phantom"
|
||||
VPHANTOM = r"\vphantom"
|
||||
|
||||
IDOTSINT = r"\idotsint"
|
||||
LATEX = r"\LaTeX"
|
||||
TEX = r"\TeX"
|
||||
|
||||
SIDESET = r"\sideset"
|
||||
|
||||
SKEW = r"\skew"
|
||||
NOT = r"\not"
|
||||
|
||||
|
||||
def font_factory(default: Optional[str], replacement: dict[str, Optional[str]]) -> defaultdict[str, Optional[str]]:
|
||||
fonts = defaultdict(lambda: default, replacement)
|
||||
return fonts
|
||||
|
||||
|
||||
LOCAL_FONTS: dict[str, defaultdict[str, Optional[str]]] = {
|
||||
BLACKBOARD_BOLD: font_factory("double-struck", {"fence": None}),
|
||||
BOLD_SYMBOL: font_factory("bold", {"mi": "bold-italic", "mtext": None}),
|
||||
MATHBB: font_factory("double-struck", {"fence": None}),
|
||||
MATHBF: font_factory("bold", {"fence": None}),
|
||||
MATHCAL: font_factory("script", {"fence": None}),
|
||||
MATHFRAK: font_factory("fraktur", {"fence": None}),
|
||||
MATHIT: font_factory("italic", {"fence": None}),
|
||||
MATHRM: font_factory(None, {"mi": "normal"}),
|
||||
MATHSCR: font_factory("script", {"fence": None}),
|
||||
MATHSF: font_factory(None, {"mi": "sans-serif"}),
|
||||
MATHTT: font_factory("monospace", {"fence": None}),
|
||||
MIT: font_factory("italic", {"fence": None, "mi": None}),
|
||||
OLDSTYLE: font_factory("normal", {"fence": None}),
|
||||
SCR: font_factory("script", {"fence": None}),
|
||||
TT: font_factory("monospace", {"fence": None}),
|
||||
}
|
||||
|
||||
OLD_STYLE_FONTS: dict[str, defaultdict[str, Optional[str]]] = {
|
||||
r"\rm": font_factory(None, {"mi": "normal"}),
|
||||
r"\bf": font_factory(None, {"mi": "bold"}),
|
||||
r"\it": font_factory(None, {"mi": "italic"}),
|
||||
r"\sf": font_factory(None, {"mi": "sans-serif"}),
|
||||
r"\tt": font_factory(None, {"mi": "monospace"}),
|
||||
}
|
||||
|
||||
GLOBAL_FONTS = {
|
||||
**OLD_STYLE_FONTS,
|
||||
r"\cal": font_factory("script", {"fence": None}),
|
||||
r"\frak": font_factory("fraktur", {"fence": None}),
|
||||
}
|
||||
|
||||
COMMANDS_WITH_ONE_PARAMETER = (
|
||||
ACUTE,
|
||||
BAR,
|
||||
BLACKBOARD_BOLD,
|
||||
BOLD_SYMBOL,
|
||||
BOXED,
|
||||
BREVE,
|
||||
CHECK,
|
||||
DOT,
|
||||
DDOT,
|
||||
DDDOT,
|
||||
DDDDOT,
|
||||
GRAVE,
|
||||
HAT,
|
||||
HPHANTOM,
|
||||
MATHRING,
|
||||
MIT,
|
||||
MOD,
|
||||
OLDSTYLE,
|
||||
OVERBRACE,
|
||||
OVERLEFTARROW,
|
||||
OVERLEFTRIGHTARROW,
|
||||
OVERLINE,
|
||||
OVERPAREN,
|
||||
OVERRIGHTARROW,
|
||||
PHANTOM,
|
||||
PMOD,
|
||||
SCR,
|
||||
TILDE,
|
||||
TT,
|
||||
UNDERBRACE,
|
||||
UNDERLEFTARROW,
|
||||
UNDERLINE,
|
||||
UNDERPAREN,
|
||||
UNDERRIGHTARROW,
|
||||
UNDERLEFTRIGHTARROW,
|
||||
VEC,
|
||||
VPHANTOM,
|
||||
WIDEHAT,
|
||||
WIDETILDE,
|
||||
)
|
||||
COMMANDS_WITH_TWO_PARAMETERS = (
|
||||
BINOM,
|
||||
CFRAC,
|
||||
DBINOM,
|
||||
DFRAC,
|
||||
FRAC,
|
||||
OVERSET,
|
||||
TBINOM,
|
||||
TFRAC,
|
||||
UNDERSET,
|
||||
)
|
||||
|
||||
BIG: dict[str, tuple[str, dict]] = {
|
||||
# command: (mathml_equivalent, attributes)
|
||||
r"\Bigg": ("mo", OrderedDict([("minsize", "2.470em"), ("maxsize", "2.470em")])),
|
||||
r"\bigg": ("mo", OrderedDict([("minsize", "2.047em"), ("maxsize", "2.047em")])),
|
||||
r"\Big": ("mo", OrderedDict([("minsize", "1.623em"), ("maxsize", "1.623em")])),
|
||||
r"\big": ("mo", OrderedDict([("minsize", "1.2em"), ("maxsize", "1.2em")])),
|
||||
}
|
||||
|
||||
BIG_OPEN_CLOSE = {
|
||||
command + postfix: (tag, OrderedDict([("stretchy", "true"), ("fence", "true"), *attrib.items()]))
|
||||
for command, (tag, attrib) in BIG.items()
|
||||
for postfix in "lmr"
|
||||
}
|
||||
|
||||
MSTYLE_SIZES: dict[str, tuple[str, dict]] = {
|
||||
# command: (mathml_equivalent, attributes)
|
||||
r"\Huge": ("mstyle", {"mathsize": "2.49em"}),
|
||||
r"\huge": ("mstyle", {"mathsize": "2.07em"}),
|
||||
r"\LARGE": ("mstyle", {"mathsize": "1.73em"}),
|
||||
r"\Large": ("mstyle", {"mathsize": "1.44em"}),
|
||||
r"\large": ("mstyle", {"mathsize": "1.2em"}),
|
||||
r"\normalsize": ("mstyle", {"mathsize": "1em"}),
|
||||
r"\scriptsize": ("mstyle", {"mathsize": "0.7em"}),
|
||||
r"\small": ("mstyle", {"mathsize": "0.85em"}),
|
||||
r"\tiny": ("mstyle", {"mathsize": "0.5em"}),
|
||||
r"\Tiny": ("mstyle", {"mathsize": "0.6em"}),
|
||||
}
|
||||
|
||||
STYLES: dict[str, tuple[str, dict]] = {
|
||||
DISPLAYSTYLE: ("mstyle", {"displaystyle": "true", "scriptlevel": "0"}),
|
||||
TEXTSTYLE: ("mstyle", {"displaystyle": "false", "scriptlevel": "0"}),
|
||||
SCRIPTSTYLE: ("mstyle", {"displaystyle": "false", "scriptlevel": "1"}),
|
||||
SCRIPTSCRIPTSTYLE: ("mstyle", {"displaystyle": "false", "scriptlevel": "2"}),
|
||||
}
|
||||
|
||||
CONVERSION_MAP: dict[str, tuple[str, dict]] = {
|
||||
# command: (mathml_equivalent, attributes)
|
||||
# tables
|
||||
**{matrix: ("mtable", {}) for matrix in MATRICES},
|
||||
DISPLAYLINES: ("mtable", {"rowspacing": "0.5em", "columnspacing": "1em", "displaystyle": "true"}),
|
||||
SMALLMATRIX: ("mtable", {"rowspacing": "0.1em", "columnspacing": "0.2778em"}),
|
||||
SPLIT: (
|
||||
"mtable",
|
||||
{"displaystyle": "true", "columnspacing": "0em", "rowspacing": "3pt"},
|
||||
),
|
||||
ALIGN: (
|
||||
"mtable",
|
||||
{"displaystyle": "true", "rowspacing": "3pt"},
|
||||
),
|
||||
# subscripts/superscripts
|
||||
SUBSCRIPT: ("msub", {}),
|
||||
SUPERSCRIPT: ("msup", {}),
|
||||
SUBSUP: ("msubsup", {}),
|
||||
# fractions
|
||||
BINOM: ("mfrac", {"linethickness": "0"}),
|
||||
CFRAC: ("mfrac", {}),
|
||||
DBINOM: ("mfrac", {"linethickness": "0"}),
|
||||
DFRAC: ("mfrac", {}),
|
||||
FRAC: ("mfrac", {}),
|
||||
GENFRAC: ("mfrac", {}),
|
||||
TBINOM: ("mfrac", {"linethickness": "0"}),
|
||||
TFRAC: ("mfrac", {}),
|
||||
# over/under
|
||||
ACUTE: ("mover", {}),
|
||||
BAR: ("mover", {}),
|
||||
BREVE: ("mover", {}),
|
||||
CHECK: ("mover", {}),
|
||||
DOT: ("mover", {}),
|
||||
DDOT: ("mover", {}),
|
||||
DDDOT: ("mover", {}),
|
||||
DDDDOT: ("mover", {}),
|
||||
GRAVE: ("mover", {}),
|
||||
HAT: ("mover", {}),
|
||||
LIMITS: ("munderover", {}),
|
||||
MATHRING: ("mover", {}),
|
||||
OVERBRACE: ("mover", {}),
|
||||
OVERLEFTARROW: ("mover", {}),
|
||||
OVERLEFTRIGHTARROW: ("mover", {}),
|
||||
OVERLINE: ("mover", {}),
|
||||
OVERPAREN: ("mover", {}),
|
||||
OVERRIGHTARROW: ("mover", {}),
|
||||
TILDE: ("mover", {}),
|
||||
OVERSET: ("mover", {}),
|
||||
UNDERBRACE: ("munder", {}),
|
||||
UNDERLEFTARROW: ("munder", {}),
|
||||
UNDERLINE: ("munder", {}),
|
||||
UNDERPAREN: ("munder", {}),
|
||||
UNDERRIGHTARROW: ("munder", {}),
|
||||
UNDERLEFTRIGHTARROW: ("munder", {}),
|
||||
UNDERSET: ("munder", {}),
|
||||
VEC: ("mover", {}),
|
||||
WIDEHAT: ("mover", {}),
|
||||
WIDETILDE: ("mover", {}),
|
||||
# spaces
|
||||
COLON: ("mspace", {"width": "0.222em"}),
|
||||
COMMA: ("mspace", {"width": "0.167em"}),
|
||||
DOUBLEBACKSLASH: ("mspace", {"linebreak": "newline"}),
|
||||
ENSPACE: ("mspace", {"width": "0.5em"}),
|
||||
EXCLAMATION: ("mspace", {"width": "negativethinmathspace"}),
|
||||
GREATER_THAN: ("mspace", {"width": "0.222em"}),
|
||||
HSKIP: ("mspace", {}),
|
||||
HSPACE: ("mspace", {}),
|
||||
KERN: ("mspace", {}),
|
||||
MKERN: ("mspace", {}),
|
||||
MSKIP: ("mspace", {}),
|
||||
MSPACE: ("mspace", {}),
|
||||
NEGTHINSPACE: ("mspace", {"width": "negativethinmathspace"}),
|
||||
NEGMEDSPACE: ("mspace", {"width": "negativemediummathspace"}),
|
||||
NEGTHICKSPACE: ("mspace", {"width": "negativethickmathspace"}),
|
||||
THINSPACE: ("mspace", {"width": "thinmathspace"}),
|
||||
QQUAD: ("mspace", {"width": "2em"}),
|
||||
QUAD: ("mspace", {"width": "1em"}),
|
||||
SEMICOLON: ("mspace", {"width": "0.278em"}),
|
||||
# enclose
|
||||
BOXED: ("menclose", {"notation": "box"}),
|
||||
FBOX: ("menclose", {"notation": "box"}),
|
||||
# operators
|
||||
**BIG,
|
||||
**BIG_OPEN_CLOSE,
|
||||
**MSTYLE_SIZES,
|
||||
**{limit: ("mo", {}) for limit in LIMIT},
|
||||
LEFT: ("mo", OrderedDict([("stretchy", "true"), ("fence", "true"), ("form", "prefix")])),
|
||||
MIDDLE: ("mo", OrderedDict([("stretchy", "true"), ("fence", "true"), ("lspace", "0.05em"), ("rspace", "0.05em")])),
|
||||
RIGHT: ("mo", OrderedDict([("stretchy", "true"), ("fence", "true"), ("form", "postfix")])),
|
||||
# styles
|
||||
COLOR: ("mstyle", {}),
|
||||
**STYLES,
|
||||
# others
|
||||
SQRT: ("msqrt", {}),
|
||||
ROOT: ("mroot", {}),
|
||||
HREF: ("mtext", {}),
|
||||
TEXT: ("mtext", {}),
|
||||
TEXTBF: ("mtext", {"mathvariant": "bold"}),
|
||||
TEXTIT: ("mtext", {"mathvariant": "italic"}),
|
||||
TEXTRM: ("mtext", {}),
|
||||
TEXTSF: ("mtext", {"mathvariant": "sans-serif"}),
|
||||
TEXTTT: ("mtext", {"mathvariant": "monospace"}),
|
||||
HBOX: ("mtext", {}),
|
||||
MBOX: ("mtext", {}),
|
||||
HPHANTOM: ("mphantom", {}),
|
||||
PHANTOM: ("mphantom", {}),
|
||||
VPHANTOM: ("mphantom", {}),
|
||||
SIDESET: ("mrow", {}),
|
||||
SKEW: ("mrow", {}),
|
||||
MOD: ("mi", {}),
|
||||
PMOD: ("mi", {}),
|
||||
BMOD: ("mo", {}),
|
||||
XLEFTARROW: ("mover", {}),
|
||||
XRIGHTARROW: ("mover", {}),
|
||||
}
|
||||
|
||||
|
||||
DIACRITICS: dict[str, tuple[str, dict[str, str]]] = {
|
||||
ACUTE: ("´", {}),
|
||||
BAR: ("¯", {"stretchy": "true"}),
|
||||
BREVE: ("˘", {}),
|
||||
CHECK: ("ˇ", {}),
|
||||
DOT: ("˙", {}),
|
||||
DDOT: ("¨", {}),
|
||||
DDDOT: ("⃛", {}),
|
||||
DDDDOT: ("⃜", {}),
|
||||
GRAVE: ("`", {}),
|
||||
HAT: ("^", {"stretchy": "false"}),
|
||||
MATHRING: ("˚", {}),
|
||||
OVERBRACE: ("⏞", {}),
|
||||
OVERLEFTARROW: ("←", {}),
|
||||
OVERLEFTRIGHTARROW: ("↔", {}),
|
||||
OVERLINE: ("―", {"accent": "true"}),
|
||||
OVERPAREN: ("⏜", {}),
|
||||
OVERRIGHTARROW: ("→", {}),
|
||||
TILDE: ("~", {"stretchy": "false"}),
|
||||
UNDERBRACE: ("⏟", {}),
|
||||
UNDERLEFTARROW: ("←", {}),
|
||||
UNDERLEFTRIGHTARROW: ("↔", {}),
|
||||
UNDERLINE: ("―", {"accent": "true"}),
|
||||
UNDERPAREN: ("⏝", {}),
|
||||
UNDERRIGHTARROW: ("→", {}),
|
||||
VEC: ("→", {"stretchy": "true"}),
|
||||
WIDEHAT: ("^", {}),
|
||||
WIDETILDE: ("~", {}),
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
import copy
|
||||
import enum
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from typing import Iterable, Iterator, Optional
|
||||
from xml.etree.cElementTree import Element, SubElement, tostring
|
||||
from xml.sax.saxutils import unescape
|
||||
|
||||
from latex2mathml import commands
|
||||
from latex2mathml.symbols_parser import convert_symbol
|
||||
from latex2mathml.walker import Node, walk
|
||||
|
||||
COLUMN_ALIGNMENT_MAP = {"r": "right", "l": "left", "c": "center"}
|
||||
OPERATORS = (
|
||||
"+",
|
||||
"-",
|
||||
"*",
|
||||
"/",
|
||||
"(",
|
||||
")",
|
||||
"=",
|
||||
",",
|
||||
"?",
|
||||
"[",
|
||||
"]",
|
||||
"|",
|
||||
r"\|",
|
||||
"!",
|
||||
r"\{",
|
||||
r"\}",
|
||||
r">",
|
||||
r"<",
|
||||
r".",
|
||||
r"\bigotimes",
|
||||
r"\centerdot",
|
||||
r"\dots",
|
||||
r"\dotsc",
|
||||
r"\dotso",
|
||||
r"\gt",
|
||||
r"\ldotp",
|
||||
r"\lt",
|
||||
r"\lvert",
|
||||
r"\lVert",
|
||||
r"\lvertneqq",
|
||||
r"\ngeqq",
|
||||
r"\omicron",
|
||||
r"\rvert",
|
||||
r"\rVert",
|
||||
r"\S",
|
||||
r"\smallfrown",
|
||||
r"\smallint",
|
||||
r"\smallsmile",
|
||||
r"\surd",
|
||||
r"\varsubsetneqq",
|
||||
r"\varsupsetneqq",
|
||||
)
|
||||
MATH_MODE_PATTERN = re.compile(r"\\\$|\$|\\?[^\\$]+")
|
||||
|
||||
|
||||
class Mode(enum.Enum):
|
||||
TEXT = enum.auto()
|
||||
MATH = enum.auto()
|
||||
|
||||
|
||||
def convert(
|
||||
latex: str,
|
||||
xmlns: str = "http://www.w3.org/1998/Math/MathML",
|
||||
display: str = "inline",
|
||||
parent: Optional[Element] = None,
|
||||
) -> str:
|
||||
math = convert_to_element(latex, xmlns, display, parent)
|
||||
return _convert(math)
|
||||
|
||||
|
||||
def convert_to_element(
|
||||
latex: str,
|
||||
xmlns: str = "http://www.w3.org/1998/Math/MathML",
|
||||
display: str = "inline",
|
||||
parent: Optional[Element] = None,
|
||||
) -> Element:
|
||||
tag = "math"
|
||||
attrib = {"xmlns": xmlns, "display": display}
|
||||
math = Element(tag, attrib) if parent is None else SubElement(parent, tag, attrib)
|
||||
row = SubElement(math, "mrow")
|
||||
_convert_group(iter(walk(latex, display)), row)
|
||||
return math
|
||||
|
||||
|
||||
def _convert(tree: Element) -> str:
|
||||
return unescape(tostring(tree, encoding="unicode"))
|
||||
|
||||
|
||||
def _convert_matrix(nodes: Iterator[Node], parent: Element, command: str, alignment: Optional[str] = None) -> None:
|
||||
row = None
|
||||
cell = None
|
||||
|
||||
col_index = 0
|
||||
col_alignment = None
|
||||
|
||||
max_col_size = 0
|
||||
|
||||
row_index = 0
|
||||
row_lines = []
|
||||
|
||||
hfil_indexes: list[bool] = []
|
||||
|
||||
for node in nodes:
|
||||
if row is None:
|
||||
row = SubElement(parent, "mtr")
|
||||
|
||||
if cell is None:
|
||||
col_alignment, col_index = _get_column_alignment(alignment, col_alignment, col_index)
|
||||
cell = _make_matrix_cell(row, col_alignment)
|
||||
|
||||
if node.token == commands.BRACES:
|
||||
_convert_group(iter([node]), cell)
|
||||
elif node.token == "&":
|
||||
_set_cell_alignment(cell, hfil_indexes)
|
||||
hfil_indexes = []
|
||||
col_alignment, col_index = _get_column_alignment(alignment, col_alignment, col_index)
|
||||
cell = _make_matrix_cell(row, col_alignment)
|
||||
if command in (commands.SPLIT, commands.ALIGN) and col_index % 2 == 0:
|
||||
SubElement(cell, "mi")
|
||||
elif node.token in (commands.DOUBLEBACKSLASH, commands.CARRIAGE_RETURN):
|
||||
_set_cell_alignment(cell, hfil_indexes)
|
||||
hfil_indexes = []
|
||||
row_index += 1
|
||||
if col_index > max_col_size:
|
||||
max_col_size = col_index
|
||||
col_index = 0
|
||||
col_alignment, col_index = _get_column_alignment(alignment, col_alignment, col_index)
|
||||
row = SubElement(parent, "mtr")
|
||||
cell = _make_matrix_cell(row, col_alignment)
|
||||
elif node.token == commands.HLINE:
|
||||
row_lines.append("solid")
|
||||
elif node.token == commands.HDASHLINE:
|
||||
row_lines.append("dashed")
|
||||
elif node.token == commands.HFIL:
|
||||
hfil_indexes.append(True)
|
||||
else:
|
||||
if row_index > len(row_lines):
|
||||
row_lines.append("none")
|
||||
hfil_indexes.append(False)
|
||||
_convert_group(iter([node]), cell)
|
||||
|
||||
if col_index > max_col_size:
|
||||
max_col_size = col_index
|
||||
|
||||
if any(r == "solid" for r in row_lines):
|
||||
parent.set("rowlines", " ".join(row_lines))
|
||||
|
||||
if row is not None and cell is not None and len(cell) == 0:
|
||||
# Remove last row if it does not contain anything
|
||||
parent.remove(row)
|
||||
|
||||
if max_col_size and command == commands.ALIGN:
|
||||
spacing = ("0em", "2em")
|
||||
multiplier = max_col_size // len(spacing)
|
||||
parent.set("columnspacing", " ".join(spacing * multiplier))
|
||||
|
||||
|
||||
def _set_cell_alignment(cell: Element, hfil_indexes: list[bool]) -> None:
|
||||
if cell is not None and any(hfil_indexes) and len(hfil_indexes) > 1:
|
||||
if hfil_indexes[0] and not hfil_indexes[-1]:
|
||||
cell.attrib["columnalign"] = "right"
|
||||
elif not hfil_indexes[0] and hfil_indexes[-1]:
|
||||
cell.attrib["columnalign"] = "left"
|
||||
|
||||
|
||||
def _get_column_alignment(
|
||||
alignment: Optional[str], column_alignment: Optional[str], column_index: int
|
||||
) -> tuple[Optional[str], int]:
|
||||
if alignment:
|
||||
try:
|
||||
column_alignment = COLUMN_ALIGNMENT_MAP.get(alignment[column_index])
|
||||
except IndexError:
|
||||
column_alignment = COLUMN_ALIGNMENT_MAP.get(alignment[column_index % len(alignment)])
|
||||
column_index += 1
|
||||
return column_alignment, column_index
|
||||
|
||||
|
||||
def _make_matrix_cell(row: Element, column_alignment: Optional[str]) -> Element:
|
||||
if column_alignment:
|
||||
return SubElement(row, "mtd", columnalign=column_alignment)
|
||||
return SubElement(row, "mtd")
|
||||
|
||||
|
||||
def _convert_group(nodes: Iterable[Node], parent: Element, font: Optional[dict[str, Optional[str]]] = None) -> None:
|
||||
_font = font
|
||||
for node in nodes:
|
||||
token = node.token
|
||||
if token in (*commands.MSTYLE_SIZES, *commands.STYLES):
|
||||
node = Node(token=token, children=tuple(n for n in nodes))
|
||||
_convert_command(node, parent, _font)
|
||||
elif token in commands.CONVERSION_MAP or token in (commands.MOD, commands.PMOD):
|
||||
_convert_command(node, parent, _font)
|
||||
elif token in commands.LOCAL_FONTS and node.children is not None:
|
||||
_convert_group(iter(node.children), parent, commands.LOCAL_FONTS[token])
|
||||
elif token.startswith(commands.MATH) and node.children is not None:
|
||||
_convert_group(iter(node.children), parent, _font)
|
||||
elif token in commands.GLOBAL_FONTS.keys():
|
||||
_font = commands.GLOBAL_FONTS.get(token)
|
||||
elif node.children is None:
|
||||
_convert_symbol(node, parent, _font)
|
||||
elif node.children is not None:
|
||||
attributes = node.attributes or {}
|
||||
_row = SubElement(parent, "mrow", attrib=attributes)
|
||||
_convert_group(iter(node.children), _row, _font)
|
||||
|
||||
|
||||
def _get_alignment_and_column_lines(alignment: Optional[str] = None) -> tuple[Optional[str], Optional[str]]:
|
||||
if alignment is None:
|
||||
return None, None
|
||||
if "|" not in alignment:
|
||||
return alignment, None
|
||||
_alignment = ""
|
||||
column_lines = []
|
||||
for c in alignment:
|
||||
if c == "|":
|
||||
column_lines.append("solid")
|
||||
else:
|
||||
_alignment += c
|
||||
if len(_alignment) - len(column_lines) == 2:
|
||||
column_lines.append("none")
|
||||
return _alignment, " ".join(column_lines)
|
||||
|
||||
|
||||
def separate_by_mode(text: str) -> Iterator[tuple[str, Mode]]:
|
||||
string = ""
|
||||
is_math_mode = False
|
||||
for match in MATH_MODE_PATTERN.findall(text):
|
||||
if match == "$": # should match both $ and $$
|
||||
yield string, Mode.MATH if is_math_mode else Mode.TEXT
|
||||
string = ""
|
||||
is_math_mode = not is_math_mode
|
||||
else:
|
||||
string += match
|
||||
if len(string):
|
||||
yield string, Mode.MATH if is_math_mode else Mode.TEXT
|
||||
# TODO: if stays in math mode, means not terminated properly, raise error
|
||||
|
||||
|
||||
def _convert_command(node: Node, parent: Element, font: Optional[dict[str, Optional[str]]] = None) -> None:
|
||||
command = node.token
|
||||
modifier = node.modifier
|
||||
|
||||
if command in (commands.SUBSTACK, commands.SMALLMATRIX):
|
||||
parent = SubElement(parent, "mstyle", scriptlevel="1")
|
||||
elif command == commands.CASES:
|
||||
parent = SubElement(parent, "mrow")
|
||||
lbrace = SubElement(parent, "mo", OrderedDict([("stretchy", "true"), ("fence", "true"), ("form", "prefix")]))
|
||||
lbrace.text = "&#x{};".format(convert_symbol(commands.LBRACE))
|
||||
elif command in (commands.DBINOM, commands.DFRAC):
|
||||
parent = SubElement(parent, "mstyle", displaystyle="true", scriptlevel="0")
|
||||
elif command == commands.HPHANTOM:
|
||||
parent = SubElement(parent, "mpadded", height="0", depth="0")
|
||||
elif command == commands.VPHANTOM:
|
||||
parent = SubElement(parent, "mpadded", width="0")
|
||||
elif command in (commands.TBINOM, commands.HBOX, commands.MBOX, commands.TFRAC):
|
||||
parent = SubElement(parent, "mstyle", displaystyle="false", scriptlevel="0")
|
||||
elif command in (commands.MOD, commands.PMOD):
|
||||
SubElement(parent, "mspace", width="1em")
|
||||
|
||||
tag, attributes = copy.deepcopy(commands.CONVERSION_MAP[command])
|
||||
|
||||
if node.attributes is not None and node.token != commands.SKEW:
|
||||
attributes.update(node.attributes)
|
||||
|
||||
if command == commands.LEFT:
|
||||
parent = SubElement(parent, "mrow")
|
||||
|
||||
_append_prefix_element(node, parent)
|
||||
|
||||
alignment, column_lines = _get_alignment_and_column_lines(node.alignment)
|
||||
|
||||
if column_lines:
|
||||
attributes["columnlines"] = column_lines
|
||||
|
||||
if command == commands.SUBSUP and node.children is not None and node.children[0].token == commands.GCD:
|
||||
tag = "munderover"
|
||||
elif command == commands.SUPERSCRIPT and modifier in (commands.LIMITS, commands.OVERBRACE):
|
||||
tag = "mover"
|
||||
elif command == commands.SUBSCRIPT and modifier in (commands.LIMITS, commands.UNDERBRACE):
|
||||
tag = "munder"
|
||||
elif command == commands.SUBSUP and modifier in (commands.LIMITS, commands.OVERBRACE, commands.UNDERBRACE):
|
||||
tag = "munderover"
|
||||
elif (
|
||||
command in (commands.XLEFTARROW, commands.XRIGHTARROW) and node.children is not None and len(node.children) == 2
|
||||
):
|
||||
tag = "munderover"
|
||||
|
||||
element = SubElement(parent, tag, attributes)
|
||||
|
||||
if command in commands.LIMIT:
|
||||
element.text = command[1:]
|
||||
elif command in (commands.MOD, commands.PMOD):
|
||||
element.text = "mod"
|
||||
SubElement(parent, "mspace", width="0.333em")
|
||||
elif command == commands.BMOD:
|
||||
element.text = "mod"
|
||||
elif command in (commands.XLEFTARROW, commands.XRIGHTARROW):
|
||||
style = SubElement(element, "mstyle", scriptlevel="0")
|
||||
arrow = SubElement(style, "mo")
|
||||
if command == commands.XLEFTARROW:
|
||||
arrow.text = "←"
|
||||
elif command == commands.XRIGHTARROW:
|
||||
arrow.text = "→"
|
||||
elif node.text is not None:
|
||||
if command == commands.MIDDLE:
|
||||
element.text = "&#x{};".format(convert_symbol(node.text))
|
||||
elif command == commands.HBOX:
|
||||
mtext: Optional[Element] = element
|
||||
for text, mode in separate_by_mode(node.text):
|
||||
if mode == Mode.TEXT:
|
||||
if mtext is None:
|
||||
mtext = SubElement(parent, tag, attributes)
|
||||
mtext.text = text.replace(" ", " ")
|
||||
_set_font(mtext, "mtext", font)
|
||||
mtext = None
|
||||
else:
|
||||
_row = SubElement(parent, "mrow")
|
||||
_convert_group(iter(walk(text)), _row)
|
||||
else:
|
||||
if command == commands.FBOX:
|
||||
element = SubElement(element, "mtext")
|
||||
element.text = node.text.replace(" ", " ")
|
||||
_set_font(element, "mtext", font)
|
||||
elif node.delimiter is not None and command not in (commands.FRAC, commands.GENFRAC):
|
||||
if node.delimiter != ".":
|
||||
symbol = convert_symbol(node.delimiter)
|
||||
element.text = node.delimiter if symbol is None else "&#x{};".format(symbol)
|
||||
|
||||
if node.children is not None:
|
||||
_parent = element
|
||||
if command in (commands.LEFT, commands.MOD, commands.PMOD):
|
||||
_parent = parent
|
||||
if command in commands.MATRICES:
|
||||
if command == commands.CASES:
|
||||
alignment = "l"
|
||||
elif command in (commands.SPLIT, commands.ALIGN):
|
||||
alignment = "rl"
|
||||
_convert_matrix(iter(node.children), _parent, command, alignment=alignment)
|
||||
elif command == commands.CFRAC:
|
||||
for child in node.children:
|
||||
p = SubElement(_parent, "mstyle", displaystyle="false", scriptlevel="0")
|
||||
_convert_group(iter([child]), p, font)
|
||||
elif command == commands.SIDESET:
|
||||
Node(
|
||||
r"\style",
|
||||
children=(Node(r"\mspace", attributes={"width": "-0.167em"}),),
|
||||
attributes={"scriptlevel": "0"},
|
||||
),
|
||||
left, right = node.children
|
||||
_convert_group(iter([left]), _parent, font)
|
||||
fill = SubElement(_parent, "mstyle", scriptlevel="0")
|
||||
SubElement(fill, "mspace", width="-0.167em")
|
||||
_convert_group(iter([right]), _parent, font)
|
||||
elif command == commands.SKEW:
|
||||
child = node.children[0]
|
||||
new_node = Node(
|
||||
token=child.token,
|
||||
children=(
|
||||
Node(
|
||||
token=commands.BRACES,
|
||||
children=(*child.children, Node(token=commands.MKERN, attributes=node.attributes)),
|
||||
),
|
||||
),
|
||||
)
|
||||
_convert_group(iter([new_node]), _parent, font)
|
||||
elif command in (commands.XLEFTARROW, commands.XRIGHTARROW):
|
||||
for child in node.children:
|
||||
padded = SubElement(
|
||||
_parent,
|
||||
"mpadded",
|
||||
OrderedDict(
|
||||
[("width", "+0.833em"), ("lspace", "0.556em"), ("voffset", "-.2em"), ("height", "-.2em")]
|
||||
),
|
||||
)
|
||||
_convert_group(iter([child]), padded, font)
|
||||
SubElement(padded, "mspace", depth=".25em")
|
||||
else:
|
||||
_convert_group(iter(node.children), _parent, font)
|
||||
|
||||
_add_diacritic(command, element)
|
||||
|
||||
_append_postfix_element(node, parent)
|
||||
|
||||
|
||||
def _add_diacritic(command: str, parent: Element) -> None:
|
||||
if command in commands.DIACRITICS:
|
||||
text, attributes = copy.deepcopy(commands.DIACRITICS[command])
|
||||
element = SubElement(parent, "mo", attributes)
|
||||
element.text = text
|
||||
|
||||
|
||||
def _convert_and_append_command(command: str, parent: Element, attributes: Optional[dict[str, str]] = None) -> None:
|
||||
code_point = convert_symbol(command)
|
||||
mo = SubElement(parent, "mo", attributes if attributes is not None else {})
|
||||
mo.text = "&#x{};".format(code_point) if code_point else command
|
||||
|
||||
|
||||
def _append_prefix_element(node: Node, parent: Element) -> None:
|
||||
size = "2.047em"
|
||||
if parent.attrib.get("displaystyle") == "false" or node.token == commands.TBINOM:
|
||||
size = "1.2em"
|
||||
if node.token in (r"\pmatrix", commands.PMOD):
|
||||
_convert_and_append_command(r"\lparen", parent)
|
||||
elif node.token in (commands.BINOM, commands.DBINOM, commands.TBINOM):
|
||||
_convert_and_append_command(r"\lparen", parent, {"minsize": size, "maxsize": size})
|
||||
elif node.token == r"\bmatrix":
|
||||
_convert_and_append_command(r"\lbrack", parent)
|
||||
elif node.token == r"\Bmatrix":
|
||||
_convert_and_append_command(r"\lbrace", parent)
|
||||
elif node.token == r"\vmatrix":
|
||||
_convert_and_append_command(r"\vert", parent)
|
||||
elif node.token == r"\Vmatrix":
|
||||
_convert_and_append_command(r"\Vert", parent)
|
||||
elif node.token in (commands.FRAC, commands.GENFRAC) and node.delimiter is not None and node.delimiter[0] != ".":
|
||||
# TODO: use 1.2em if inline
|
||||
_convert_and_append_command(node.delimiter[0], parent, {"minsize": size, "maxsize": size})
|
||||
|
||||
|
||||
def _append_postfix_element(node: Node, parent: Element) -> None:
|
||||
size = "2.047em"
|
||||
if parent.attrib.get("displaystyle") == "false" or node.token == commands.TBINOM:
|
||||
size = "1.2em"
|
||||
if node.token in (r"\pmatrix", commands.PMOD):
|
||||
_convert_and_append_command(r"\rparen", parent)
|
||||
elif node.token in (commands.BINOM, commands.DBINOM, commands.TBINOM):
|
||||
_convert_and_append_command(r"\rparen", parent, {"minsize": size, "maxsize": size})
|
||||
elif node.token == r"\bmatrix":
|
||||
_convert_and_append_command(r"\rbrack", parent)
|
||||
elif node.token == r"\Bmatrix":
|
||||
_convert_and_append_command(r"\rbrace", parent)
|
||||
elif node.token == r"\vmatrix":
|
||||
_convert_and_append_command(r"\vert", parent)
|
||||
elif node.token == r"\Vmatrix":
|
||||
_convert_and_append_command(r"\Vert", parent)
|
||||
elif node.token in (commands.FRAC, commands.GENFRAC) and node.delimiter is not None and node.delimiter[1] != ".":
|
||||
# TODO: use 1.2em if inline
|
||||
_convert_and_append_command(node.delimiter[1], parent, {"minsize": size, "maxsize": size})
|
||||
elif node.token == commands.SKEW and node.attributes is not None:
|
||||
SubElement(parent, "mspace", width="-" + node.attributes["width"])
|
||||
|
||||
|
||||
def _convert_symbol(node: Node, parent: Element, font: Optional[dict[str, Optional[str]]] = None) -> None:
|
||||
token = node.token
|
||||
attributes = node.attributes or {}
|
||||
symbol = convert_symbol(token)
|
||||
if re.match(r"\d+(.\d+)?", token):
|
||||
element = SubElement(parent, "mn", attrib=attributes)
|
||||
element.text = token
|
||||
_set_font(element, element.tag, font)
|
||||
elif token in OPERATORS:
|
||||
element = SubElement(parent, "mo", attrib=attributes)
|
||||
element.text = token if symbol is None else "&#x{};".format(symbol)
|
||||
if token == r"\|":
|
||||
element.attrib["fence"] = "false"
|
||||
if token == r"\smallint":
|
||||
element.attrib["largeop"] = "false"
|
||||
if token in ("(", ")", "[", "]", "|", r"\|", r"\{", r"\}", r"\surd"):
|
||||
element.attrib["stretchy"] = "false"
|
||||
_set_font(element, "fence", font)
|
||||
else:
|
||||
_set_font(element, element.tag, font)
|
||||
elif (
|
||||
symbol
|
||||
and (
|
||||
int(symbol, 16) in range(int("2200", 16), int("22FF", 16) + 1)
|
||||
or int(symbol, 16) in range(int("2190", 16), int("21FF", 16) + 1)
|
||||
)
|
||||
or symbol == "."
|
||||
):
|
||||
element = SubElement(parent, "mo", attrib=attributes)
|
||||
element.text = "&#x{};".format(symbol)
|
||||
_set_font(element, element.tag, font)
|
||||
elif token in (r"\ ", "~", commands.NOBREAKSPACE, commands.SPACE):
|
||||
element = SubElement(parent, "mtext", attrib=attributes)
|
||||
element.text = " "
|
||||
_set_font(element, "mtext", font)
|
||||
elif token == commands.NOT:
|
||||
mpadded = SubElement(parent, "mpadded", width="0")
|
||||
element = SubElement(mpadded, "mtext")
|
||||
element.text = "⧸"
|
||||
elif token in (
|
||||
commands.DETERMINANT,
|
||||
commands.GCD,
|
||||
commands.INTOP,
|
||||
commands.INJLIM,
|
||||
commands.LIMINF,
|
||||
commands.LIMSUP,
|
||||
commands.PR,
|
||||
commands.PROJLIM,
|
||||
):
|
||||
element = SubElement(parent, "mo", attrib={"movablelimits": "true", **attributes})
|
||||
texts = {
|
||||
commands.INJLIM: "inj lim",
|
||||
commands.INTOP: "∫",
|
||||
commands.LIMINF: "lim inf",
|
||||
commands.LIMSUP: "lim sup",
|
||||
commands.PROJLIM: "proj lim",
|
||||
}
|
||||
element.text = texts.get(token, token[1:])
|
||||
_set_font(element, element.tag, font)
|
||||
elif token == commands.IDOTSINT:
|
||||
_parent = SubElement(parent, "mrow", attrib=attributes)
|
||||
for s in ("∫", "⋯", "∫"):
|
||||
element = SubElement(_parent, "mo")
|
||||
element.text = s
|
||||
elif token in (commands.LATEX, commands.TEX):
|
||||
_parent = SubElement(parent, "mrow", attrib=attributes)
|
||||
if token == commands.LATEX:
|
||||
mi_l = SubElement(_parent, "mi")
|
||||
mi_l.text = "L"
|
||||
SubElement(_parent, "mspace", width="-.325em")
|
||||
mpadded = SubElement(_parent, "mpadded", height="+.21ex", depth="-.21ex", voffset="+.21ex")
|
||||
mstyle = SubElement(mpadded, "mstyle", displaystyle="false", scriptlevel="1")
|
||||
mrow = SubElement(mstyle, "mrow")
|
||||
mi_a = SubElement(mrow, "mi")
|
||||
mi_a.text = "A"
|
||||
SubElement(_parent, "mspace", width="-.17em")
|
||||
_set_font(mi_l, mi_l.tag, font)
|
||||
_set_font(mi_a, mi_a.tag, font)
|
||||
mi_t = SubElement(_parent, "mi")
|
||||
mi_t.text = "T"
|
||||
SubElement(_parent, "mspace", width="-.14em")
|
||||
mpadded = SubElement(_parent, "mpadded", height="-.5ex", depth="+.5ex", voffset="-.5ex")
|
||||
mrow = SubElement(mpadded, "mrow")
|
||||
mi_e = SubElement(mrow, "mi")
|
||||
mi_e.text = "E"
|
||||
SubElement(_parent, "mspace", width="-.115em")
|
||||
mi_x = SubElement(_parent, "mi")
|
||||
mi_x.text = "X"
|
||||
_set_font(mi_t, mi_t.tag, font)
|
||||
_set_font(mi_e, mi_e.tag, font)
|
||||
_set_font(mi_x, mi_x.tag, font)
|
||||
elif token.startswith(commands.OPERATORNAME):
|
||||
element = SubElement(parent, "mo", attrib=attributes)
|
||||
element.text = token[14:-1]
|
||||
elif token.startswith(commands.BACKSLASH):
|
||||
element = SubElement(parent, "mi", attrib=attributes)
|
||||
if symbol:
|
||||
element.text = "&#x{};".format(symbol)
|
||||
elif token in commands.FUNCTIONS:
|
||||
element.text = token[1:]
|
||||
else:
|
||||
element.text = token
|
||||
_set_font(element, element.tag, font)
|
||||
else:
|
||||
element = SubElement(parent, "mi", attrib=attributes)
|
||||
element.text = token
|
||||
_set_font(element, element.tag, font)
|
||||
|
||||
|
||||
def _set_font(element: Element, key: str, font: Optional[dict[str, Optional[str]]]) -> None:
|
||||
if font is None:
|
||||
return
|
||||
_font = font[key]
|
||||
if _font is not None:
|
||||
element.attrib["mathvariant"] = _font
|
||||
|
||||
|
||||
def main() -> None: # pragma: no cover
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(description="Pure Python library for LaTeX to MathML conversion")
|
||||
parser.add_argument("-V", "--version", dest="version", action="store_true", required=False, help="Show version")
|
||||
parser.add_argument("-b", "--block", dest="block", action="store_true", required=False, help="Display block")
|
||||
|
||||
required = parser.add_argument_group("required arguments")
|
||||
|
||||
group = required.add_mutually_exclusive_group(required=False)
|
||||
group.add_argument("-t", "--text", dest="text", type=str, required=False, help="Text")
|
||||
group.add_argument("-f", "--file", dest="file", type=str, required=False, help="File")
|
||||
group.add_argument("-s", "--stdin", dest="stdin", action="store_true", required=False, help="Stdin")
|
||||
|
||||
arguments = parser.parse_args()
|
||||
display = "block" if arguments.block else "inline"
|
||||
|
||||
if arguments.version:
|
||||
import latex2mathml
|
||||
|
||||
print("latex2mathml", latex2mathml.__version__)
|
||||
elif arguments.text:
|
||||
print(convert(arguments.text, display=display))
|
||||
elif arguments.file:
|
||||
with open(arguments.file) as f:
|
||||
print(convert(f.read(), display=display))
|
||||
elif arguments.stdin:
|
||||
print(convert(sys.stdin.read(), display=display))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
class NumeratorNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DenominatorNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ExtraLeftOrMissingRightError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingSuperScriptOrSubscriptError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DoubleSubscriptsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DoubleSuperscriptsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoAvailableTokensError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidStyleForGenfracError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MissingEndError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAlignmentError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidWidthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LimitsMustFollowMathOperatorError(Exception):
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
# PEP 581
|
||||
@@ -0,0 +1,78 @@
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
from typing import Optional, Union
|
||||
|
||||
SYMBOLS_FILE: str = os.path.join(os.path.dirname(os.path.realpath(__file__)), "unimathsymbols.txt")
|
||||
SYMBOLS: Optional[dict[str, str]] = None
|
||||
|
||||
|
||||
def convert_symbol(symbol: str) -> Union[str, None]:
|
||||
global SYMBOLS
|
||||
if not SYMBOLS:
|
||||
SYMBOLS = parse_symbols()
|
||||
return SYMBOLS.get(symbol, None)
|
||||
|
||||
|
||||
def parse_symbols() -> dict[str, str]:
|
||||
_symbols: dict[str, str] = {}
|
||||
with codecs.open(SYMBOLS_FILE, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
columns = line.strip().split("^")
|
||||
_unicode = columns[0]
|
||||
latex = columns[2]
|
||||
unicode_math = columns[3]
|
||||
if latex and latex not in _symbols:
|
||||
_symbols[latex] = _unicode
|
||||
if unicode_math and unicode_math not in _symbols:
|
||||
_symbols[unicode_math] = _unicode
|
||||
for equivalent in re.findall(r"[=#]\s*(\\[^,^ ]+),?", columns[-1]):
|
||||
if equivalent not in _symbols:
|
||||
_symbols[equivalent] = _unicode
|
||||
_symbols.update(
|
||||
{
|
||||
r"\And": _symbols[r"\ampersand"],
|
||||
r"\bigcirc": _symbols[r"\lgwhtcircle"],
|
||||
r"\Box": _symbols[r"\square"],
|
||||
r"\circledS": "024C8",
|
||||
r"\diagdown": "02572",
|
||||
r"\diagup": "02571",
|
||||
r"\dots": "02026",
|
||||
r"\dotsb": _symbols[r"\cdots"],
|
||||
r"\dotsc": "02026",
|
||||
r"\dotsi": _symbols[r"\cdots"],
|
||||
r"\dotsm": _symbols[r"\cdots"],
|
||||
r"\dotso": "02026",
|
||||
r"\emptyset": "02205",
|
||||
r"\gggtr": "022D9",
|
||||
r"\gvertneqq": "02269",
|
||||
r"\gt": _symbols[r"\greater"],
|
||||
r"\ldotp": _symbols[r"\period"],
|
||||
r"\llless": _symbols[r"\lll"],
|
||||
r"\lt": _symbols[r"\less"],
|
||||
r"\lvert": _symbols[r"\vert"],
|
||||
r"\lVert": _symbols[r"\Vert"],
|
||||
r"\lvertneqq": _symbols[r"\lneqq"],
|
||||
r"\ngeqq": _symbols[r"\ngeq"],
|
||||
r"\nshortmid": _symbols[r"\nmid"],
|
||||
r"\nshortparallel": _symbols[r"\nparallel"],
|
||||
r"\nsubseteqq": _symbols[r"\nsubseteq"],
|
||||
r"\omicron": _symbols[r"\upomicron"],
|
||||
r"\rvert": _symbols[r"\vert"],
|
||||
r"\rVert": _symbols[r"\Vert"],
|
||||
r"\shortmid": _symbols[r"\mid"],
|
||||
r"\smallfrown": _symbols[r"\frown"],
|
||||
r"\smallint": "0222B",
|
||||
r"\smallsmile": _symbols[r"\smile"],
|
||||
r"\surd": _symbols[r"\sqrt"],
|
||||
r"\thicksim": "0223C",
|
||||
r"\thickapprox": _symbols[r"\approx"],
|
||||
r"\varsubsetneqq": _symbols[r"\subsetneqq"],
|
||||
r"\varsupsetneq": "0228B",
|
||||
r"\varsupsetneqq": _symbols[r"\supsetneqq"],
|
||||
}
|
||||
)
|
||||
del _symbols[r"\mathring"] # FIXME: improve tokenizer without removing this
|
||||
return _symbols
|
||||
@@ -0,0 +1,55 @@
|
||||
import re
|
||||
from typing import Iterator
|
||||
|
||||
from latex2mathml import commands
|
||||
from latex2mathml.symbols_parser import convert_symbol
|
||||
|
||||
UNITS = ("in", "mm", "cm", "pt", "em", "ex", "pc", "bp", "dd", "cc", "sp", "mu")
|
||||
|
||||
PATTERN = re.compile(
|
||||
rf"""
|
||||
(%[^\n]+) | # comment
|
||||
(a-zA-Z) | # letter
|
||||
([_^])(\d) | # number succeeding an underscore or a caret
|
||||
(-?\d+(?:\.\d+)?\s*(?:{'|'.join(UNITS)})) | # dimension
|
||||
(\d+(?:\.\d+)?) | # integer/decimal
|
||||
(\.\d*) | # dot (.) or decimal can start with just a dot
|
||||
(\\[\\\[\]{{}}\s!,:>;|_%#$&]) | # escaped characters
|
||||
(\\(?:begin|end)\s*{{[a-zA-Z]+\*?}}) | # begin or end
|
||||
(\\operatorname\s*{{[a-zA-Z\s*]+\*?\s*}}) | # operatorname
|
||||
# color, fbox, href, hbox, mbox, style, text, textbf, textit, textrm, textsf, texttt
|
||||
(\\(?:color|fbox|hbox|href|mbox|style|text|textbf|textit|textrm|textsf|texttt))\s*{{([^}}]*)}} |
|
||||
(\\[cdt]?frac)\s*([.\d])\s*([.\d])? | # fractions
|
||||
(\\math[a-z]+)({{)([a-zA-Z])(}}) | # commands starting with math
|
||||
(\\[a-zA-Z]+) | # other commands
|
||||
(\S) # non-space character
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def tokenize(latex_string: str, skip_comments: bool = True) -> Iterator[str]:
|
||||
"""
|
||||
Converts Latex string into tokens.
|
||||
|
||||
:param latex_string: Latex string.
|
||||
:param skip_comments: Flag to skip comments (default=True).
|
||||
"""
|
||||
for match in PATTERN.finditer(latex_string):
|
||||
tokens = tuple(filter(lambda x: x is not None, match.groups()))
|
||||
if tokens[0].startswith(commands.MATH):
|
||||
full_math = "".join(tokens)
|
||||
symbol = convert_symbol(full_math)
|
||||
if symbol:
|
||||
yield f"&#x{symbol};"
|
||||
continue
|
||||
for captured in tokens:
|
||||
if skip_comments and captured.startswith("%"):
|
||||
break
|
||||
if captured.endswith(UNITS):
|
||||
yield captured.replace(" ", "")
|
||||
continue
|
||||
if captured.startswith((commands.BEGIN, commands.END, commands.OPERATORNAME)):
|
||||
yield "".join(captured.split(" "))
|
||||
continue
|
||||
yield captured
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,461 @@
|
||||
from typing import Any, Iterator, NamedTuple, Optional
|
||||
|
||||
from latex2mathml import commands
|
||||
from latex2mathml.exceptions import (
|
||||
DenominatorNotFoundError,
|
||||
DoubleSubscriptsError,
|
||||
DoubleSuperscriptsError,
|
||||
ExtraLeftOrMissingRightError,
|
||||
InvalidAlignmentError,
|
||||
InvalidStyleForGenfracError,
|
||||
InvalidWidthError,
|
||||
LimitsMustFollowMathOperatorError,
|
||||
MissingEndError,
|
||||
MissingSuperScriptOrSubscriptError,
|
||||
NoAvailableTokensError,
|
||||
NumeratorNotFoundError,
|
||||
)
|
||||
from latex2mathml.symbols_parser import convert_symbol
|
||||
from latex2mathml.tokenizer import tokenize
|
||||
|
||||
|
||||
class Node(NamedTuple):
|
||||
token: str
|
||||
children: Optional[tuple[Any, ...]] = None
|
||||
delimiter: Optional[str] = None
|
||||
alignment: Optional[str] = None
|
||||
text: Optional[str] = None
|
||||
attributes: Optional[dict[str, str]] = None
|
||||
modifier: Optional[str] = None
|
||||
|
||||
|
||||
def walk(data: str, display: str = "inline") -> list[Node]:
|
||||
tokens = tokenize(data)
|
||||
block = display == "block"
|
||||
return _walk(tokens, block=block)
|
||||
|
||||
|
||||
def _walk(tokens: Iterator[str], terminator: Optional[str] = None, limit: int = 0, block: bool = False) -> list[Node]:
|
||||
group: list[Node] = []
|
||||
token: str
|
||||
has_available_tokens = False
|
||||
for token in tokens:
|
||||
has_available_tokens = True
|
||||
if token == terminator:
|
||||
delimiter = None
|
||||
if terminator == commands.RIGHT:
|
||||
delimiter = next(tokens)
|
||||
group.append(Node(token=token, delimiter=delimiter))
|
||||
break
|
||||
elif (token == commands.RIGHT != terminator) or (token == commands.MIDDLE and terminator != commands.RIGHT):
|
||||
raise ExtraLeftOrMissingRightError
|
||||
elif token == commands.LEFT:
|
||||
delimiter = next(tokens)
|
||||
children = tuple(_walk(tokens, terminator=commands.RIGHT)) # make \right as a child of \left
|
||||
if len(children) == 0 or children[-1].token != commands.RIGHT:
|
||||
raise ExtraLeftOrMissingRightError
|
||||
node = Node(token=token, children=children if len(children) else None, delimiter=delimiter)
|
||||
elif token == commands.OPENING_BRACE:
|
||||
children = tuple(_walk(tokens, terminator=commands.CLOSING_BRACE))
|
||||
if len(children) and children[-1].token == commands.CLOSING_BRACE:
|
||||
children = children[:-1]
|
||||
node = Node(token=commands.BRACES, children=children)
|
||||
elif token in (commands.SUBSCRIPT, commands.SUPERSCRIPT):
|
||||
try:
|
||||
previous = group.pop()
|
||||
except IndexError:
|
||||
previous = Node(token="") # left operand can be empty if not present
|
||||
|
||||
if token == previous.token == commands.SUBSCRIPT:
|
||||
raise DoubleSubscriptsError
|
||||
if (token == previous.token == commands.SUPERSCRIPT) and (
|
||||
previous.children is not None
|
||||
and len(previous.children) >= 2
|
||||
and previous.children[1].token != commands.PRIME
|
||||
):
|
||||
raise DoubleSuperscriptsError
|
||||
|
||||
modifier = None
|
||||
if previous.token == commands.LIMITS:
|
||||
modifier = commands.LIMITS
|
||||
try:
|
||||
previous = group.pop()
|
||||
if not previous.token.startswith("\\"): # TODO: Complete list of operators
|
||||
raise LimitsMustFollowMathOperatorError
|
||||
except IndexError:
|
||||
raise LimitsMustFollowMathOperatorError
|
||||
elif block and previous.token in (commands.SUMMATION, commands.PRODUCT):
|
||||
# block summation and product should result in limited sub/sup
|
||||
modifier = commands.LIMITS
|
||||
|
||||
if token == commands.SUBSCRIPT and previous.token == commands.SUPERSCRIPT and previous.children is not None:
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
node = Node(
|
||||
token=commands.SUBSUP,
|
||||
children=(previous.children[0], *children, previous.children[1]),
|
||||
modifier=previous.modifier,
|
||||
)
|
||||
elif (
|
||||
token == commands.SUPERSCRIPT and previous.token == commands.SUBSCRIPT and previous.children is not None
|
||||
):
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
node = Node(token=commands.SUBSUP, children=(*previous.children, *children), modifier=previous.modifier)
|
||||
elif (
|
||||
token == commands.SUPERSCRIPT
|
||||
and previous.token == commands.SUPERSCRIPT
|
||||
and previous.children is not None
|
||||
and previous.children[1].token == commands.PRIME
|
||||
):
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
|
||||
node = Node(
|
||||
token=commands.SUPERSCRIPT,
|
||||
children=(
|
||||
previous.children[0],
|
||||
Node(token=commands.BRACES, children=(previous.children[1], *children)),
|
||||
),
|
||||
modifier=previous.modifier,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
except NoAvailableTokensError:
|
||||
raise MissingSuperScriptOrSubscriptError
|
||||
if previous.token in (commands.OVERBRACE, commands.UNDERBRACE):
|
||||
modifier = previous.token
|
||||
node = Node(token=token, children=(previous, *children), modifier=modifier)
|
||||
elif token == commands.APOSTROPHE:
|
||||
try:
|
||||
previous = group.pop()
|
||||
except IndexError:
|
||||
previous = Node(token="") # left operand can be empty if not present
|
||||
|
||||
if (
|
||||
previous.token == commands.SUPERSCRIPT
|
||||
and previous.children is not None
|
||||
and len(previous.children) >= 2
|
||||
and previous.children[1].token != commands.PRIME
|
||||
):
|
||||
raise DoubleSuperscriptsError
|
||||
|
||||
if (
|
||||
previous.token == commands.SUPERSCRIPT
|
||||
and previous.children is not None
|
||||
and len(previous.children) >= 2
|
||||
and previous.children[1].token == commands.PRIME
|
||||
):
|
||||
node = Node(token=commands.SUPERSCRIPT, children=(previous.children[0], Node(token=commands.DPRIME)))
|
||||
elif previous.token == commands.SUBSCRIPT and previous.children is not None:
|
||||
node = Node(
|
||||
token=commands.SUBSUP,
|
||||
children=(*previous.children, Node(token=commands.PRIME)),
|
||||
modifier=previous.modifier,
|
||||
)
|
||||
else:
|
||||
node = Node(token=commands.SUPERSCRIPT, children=(previous, Node(token=commands.PRIME)))
|
||||
elif token in commands.COMMANDS_WITH_TWO_PARAMETERS:
|
||||
attributes = None
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=2))
|
||||
if token in (commands.OVERSET, commands.UNDERSET):
|
||||
children = children[::-1]
|
||||
node = Node(token=token, children=children, attributes=attributes)
|
||||
elif token in commands.COMMANDS_WITH_ONE_PARAMETER or token.startswith(commands.MATH):
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
node = Node(token=token, children=children)
|
||||
elif token == commands.NOT:
|
||||
try:
|
||||
next_node = tuple(_walk(tokens, terminator=terminator, limit=1))[0]
|
||||
if next_node.token.startswith("\\"):
|
||||
negated_symbol = r"\n" + next_node.token[1:]
|
||||
symbol = convert_symbol(negated_symbol)
|
||||
if symbol:
|
||||
node = Node(token=negated_symbol)
|
||||
group.append(node)
|
||||
continue
|
||||
node = Node(token=token)
|
||||
group.extend((node, next_node))
|
||||
continue
|
||||
except NoAvailableTokensError:
|
||||
node = Node(token=token)
|
||||
elif token in (commands.XLEFTARROW, commands.XRIGHTARROW):
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
if children[0].token == commands.OPENING_BRACKET:
|
||||
children = (
|
||||
Node(
|
||||
token=commands.BRACES, children=tuple(_walk(tokens, terminator=commands.CLOSING_BRACKET))[:-1]
|
||||
),
|
||||
*tuple(_walk(tokens, terminator=terminator, limit=1)),
|
||||
)
|
||||
node = Node(token=token, children=children)
|
||||
elif token in (commands.HSKIP, commands.HSPACE, commands.KERN, commands.MKERN, commands.MSKIP, commands.MSPACE):
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
if children[0].token == commands.BRACES and children[0].children is not None:
|
||||
children = children[0].children
|
||||
node = Node(token=token, attributes={"width": children[0].token})
|
||||
elif token == commands.COLOR:
|
||||
attributes = {"mathcolor": next(tokens)}
|
||||
children = tuple(_walk(tokens, terminator=terminator))
|
||||
sibling = None
|
||||
if len(children) and children[-1].token == terminator:
|
||||
children, sibling = children[:-1], children[-1]
|
||||
group.append(Node(token=token, children=children, attributes=attributes))
|
||||
if sibling:
|
||||
group.append(sibling)
|
||||
break
|
||||
elif token == commands.STYLE:
|
||||
attributes = {"style": next(tokens)}
|
||||
next_node = tuple(_walk(tokens, terminator=terminator, limit=1))[0]
|
||||
node = next_node._replace(attributes=attributes)
|
||||
elif token in (
|
||||
*commands.BIG.keys(),
|
||||
*commands.BIG_OPEN_CLOSE.keys(),
|
||||
commands.FBOX,
|
||||
commands.HBOX,
|
||||
commands.MBOX,
|
||||
commands.MIDDLE,
|
||||
commands.TEXT,
|
||||
commands.TEXTBF,
|
||||
commands.TEXTIT,
|
||||
commands.TEXTRM,
|
||||
commands.TEXTSF,
|
||||
commands.TEXTTT,
|
||||
):
|
||||
node = Node(token=token, text=next(tokens))
|
||||
elif token == commands.HREF:
|
||||
attributes = {"href": next(tokens)}
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=1))
|
||||
node = Node(token=token, children=children, attributes=attributes)
|
||||
elif token in (
|
||||
commands.ABOVE,
|
||||
commands.ATOP,
|
||||
commands.ABOVEWITHDELIMS,
|
||||
commands.ATOPWITHDELIMS,
|
||||
commands.BRACE,
|
||||
commands.BRACK,
|
||||
commands.CHOOSE,
|
||||
commands.OVER,
|
||||
):
|
||||
attributes = None
|
||||
delimiter = None
|
||||
|
||||
if token == commands.ABOVEWITHDELIMS:
|
||||
delimiter = next(tokens).lstrip("\\") + next(tokens).lstrip("\\")
|
||||
elif token == commands.ATOPWITHDELIMS:
|
||||
attributes = {"linethickness": "0"}
|
||||
delimiter = next(tokens).lstrip("\\") + next(tokens).lstrip("\\")
|
||||
elif token == commands.BRACE:
|
||||
delimiter = "{}"
|
||||
elif token == commands.BRACK:
|
||||
delimiter = "[]"
|
||||
elif token == commands.CHOOSE:
|
||||
delimiter = "()"
|
||||
|
||||
if token in (commands.ABOVE, commands.ABOVEWITHDELIMS):
|
||||
dimension_node = tuple(_walk(tokens, terminator=terminator, limit=1))[0]
|
||||
dimension = _get_dimension(dimension_node)
|
||||
attributes = {"linethickness": dimension}
|
||||
elif token in (commands.ATOP, commands.BRACE, commands.BRACK, commands.CHOOSE):
|
||||
attributes = {"linethickness": "0"}
|
||||
|
||||
denominator = tuple(_walk(tokens, terminator=terminator))
|
||||
|
||||
sibling = None
|
||||
if len(denominator) and denominator[-1].token == terminator:
|
||||
denominator, sibling = denominator[:-1], denominator[-1]
|
||||
|
||||
if len(denominator) == 0:
|
||||
if token in (commands.BRACE, commands.BRACK):
|
||||
denominator = (Node(token=commands.BRACES, children=()),)
|
||||
else:
|
||||
raise DenominatorNotFoundError
|
||||
if len(group) == 0:
|
||||
if token in (commands.BRACE, commands.BRACK):
|
||||
group = [Node(token=commands.BRACES, children=())]
|
||||
else:
|
||||
raise NumeratorNotFoundError
|
||||
if len(denominator) > 1:
|
||||
denominator = (Node(token=commands.BRACES, children=denominator),)
|
||||
|
||||
if len(group) == 1:
|
||||
children = (*group, *denominator)
|
||||
else:
|
||||
children = (Node(token=commands.BRACES, children=tuple(group)), *denominator)
|
||||
group = [Node(token=commands.FRAC, children=children, attributes=attributes, delimiter=delimiter)]
|
||||
if sibling is not None:
|
||||
group.append(sibling)
|
||||
break
|
||||
elif token == commands.SQRT:
|
||||
root_nodes = None
|
||||
next_node = tuple(_walk(tokens, limit=1))[0]
|
||||
if next_node.token == commands.OPENING_BRACKET:
|
||||
root_nodes = tuple(_walk(tokens, terminator=commands.CLOSING_BRACKET))[:-1]
|
||||
next_node = tuple(_walk(tokens, limit=1))[0]
|
||||
if len(root_nodes) > 1:
|
||||
root_nodes = (Node(token=commands.BRACES, children=root_nodes),)
|
||||
|
||||
if root_nodes:
|
||||
node = Node(token=commands.ROOT, children=(next_node, *root_nodes))
|
||||
else:
|
||||
node = Node(token=token, children=(next_node,))
|
||||
elif token == commands.ROOT:
|
||||
root_nodes = tuple(_walk(tokens, terminator=r"\of"))[:-1]
|
||||
next_node = tuple(_walk(tokens, limit=1))[0]
|
||||
if len(root_nodes) > 1:
|
||||
root_nodes = (Node(token=commands.BRACES, children=root_nodes),)
|
||||
if root_nodes:
|
||||
node = Node(token=token, children=(next_node, *root_nodes))
|
||||
else:
|
||||
node = Node(token=token, children=(next_node, Node(token=commands.BRACES, children=())))
|
||||
elif token in commands.MATRICES:
|
||||
children = tuple(_walk(tokens, terminator=terminator))
|
||||
sibling = None
|
||||
if len(children) and children[-1].token == terminator:
|
||||
children, sibling = children[:-1], children[-1]
|
||||
if len(children) == 1 and children[0].token == commands.BRACES and children[0].children:
|
||||
children = children[0].children
|
||||
if sibling is not None:
|
||||
group.extend([Node(token=token, children=children, alignment=""), sibling])
|
||||
break
|
||||
else:
|
||||
node = Node(token=token, children=children, alignment="")
|
||||
elif token == commands.GENFRAC:
|
||||
delimiter = next(tokens).lstrip("\\") + next(tokens).lstrip("\\")
|
||||
dimension_node, style_node = tuple(_walk(tokens, terminator=terminator, limit=2))
|
||||
dimension = _get_dimension(dimension_node)
|
||||
style = _get_style(style_node)
|
||||
attributes = {"linethickness": dimension}
|
||||
children = tuple(_walk(tokens, terminator=terminator, limit=2))
|
||||
group.extend(
|
||||
[Node(token=style), Node(token=token, children=children, delimiter=delimiter, attributes=attributes)]
|
||||
)
|
||||
break
|
||||
elif token == commands.SIDESET:
|
||||
left, right, operator = tuple(_walk(tokens, terminator=terminator, limit=3))
|
||||
left_token, left_children = _make_subsup(left)
|
||||
right_token, right_children = _make_subsup(right)
|
||||
attributes = {"movablelimits": "false"}
|
||||
node = Node(
|
||||
token=token,
|
||||
children=(
|
||||
Node(
|
||||
token=left_token,
|
||||
children=(
|
||||
Node(
|
||||
token=commands.VPHANTOM,
|
||||
children=(
|
||||
Node(token=operator.token, children=operator.children, attributes=attributes),
|
||||
),
|
||||
),
|
||||
*left_children,
|
||||
),
|
||||
),
|
||||
Node(
|
||||
token=right_token,
|
||||
children=(
|
||||
Node(token=operator.token, children=operator.children, attributes=attributes),
|
||||
*right_children,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
elif token == commands.SKEW:
|
||||
width_node, child = tuple(_walk(tokens, terminator=terminator, limit=2))
|
||||
width = width_node.token
|
||||
if width == commands.BRACES:
|
||||
if width_node.children is None or len(width_node.children) == 0:
|
||||
raise InvalidWidthError
|
||||
width = width_node.children[0].token
|
||||
if not width.isdigit():
|
||||
raise InvalidWidthError
|
||||
node = Node(token=token, children=(child,), attributes={"width": f"{0.0555 * int(width):.3f}em"})
|
||||
elif token.startswith(commands.BEGIN):
|
||||
node = _get_environment_node(token, tokens)
|
||||
else:
|
||||
node = Node(token=token)
|
||||
|
||||
group.append(node)
|
||||
|
||||
if limit and len(group) >= limit:
|
||||
break
|
||||
if not has_available_tokens:
|
||||
raise NoAvailableTokensError
|
||||
return group
|
||||
|
||||
|
||||
def _make_subsup(node: Node) -> tuple[str, tuple[Node, ...]]:
|
||||
# TODO: raise error instead of assertion
|
||||
assert node.token == commands.BRACES
|
||||
try:
|
||||
assert (
|
||||
node.children is not None
|
||||
and 2 <= len(node.children[0].children) <= 3
|
||||
and node.children[0].token
|
||||
in (
|
||||
commands.SUBSUP,
|
||||
commands.SUBSCRIPT,
|
||||
commands.SUPERSCRIPT,
|
||||
)
|
||||
)
|
||||
token = node.children[0].token
|
||||
children = node.children[0].children[1:]
|
||||
return token, children
|
||||
except IndexError:
|
||||
return "", ()
|
||||
|
||||
|
||||
def _get_dimension(node: Node) -> str:
|
||||
dimension = node.token
|
||||
if node.token == commands.BRACES and node.children is not None:
|
||||
dimension = node.children[0].token
|
||||
return dimension
|
||||
|
||||
|
||||
def _get_style(node: Node) -> str:
|
||||
style = node.token
|
||||
if node.token == commands.BRACES and node.children is not None:
|
||||
style = node.children[0].token
|
||||
if style == "0":
|
||||
return commands.DISPLAYSTYLE
|
||||
if style == "1":
|
||||
return commands.TEXTSTYLE
|
||||
if style == "2":
|
||||
return commands.SCRIPTSTYLE
|
||||
if style == "3":
|
||||
return commands.SCRIPTSCRIPTSTYLE
|
||||
raise InvalidStyleForGenfracError
|
||||
|
||||
|
||||
def _get_environment_node(token: str, tokens: Iterator[str]) -> Node:
|
||||
# TODO: support non-matrix environments
|
||||
start_index = token.index("{") + 1
|
||||
environment = token[start_index:-1]
|
||||
terminator = rf"{commands.END}{{{environment}}}"
|
||||
children = tuple(_walk(tokens, terminator=terminator))
|
||||
if len(children) and children[-1].token != terminator:
|
||||
raise MissingEndError
|
||||
children = children[:-1]
|
||||
alignment = ""
|
||||
|
||||
if len(children) and children[0].token == commands.OPENING_BRACKET:
|
||||
children_iter = iter(children)
|
||||
next(children_iter) # remove BRACKET
|
||||
for c in children_iter:
|
||||
if c.token == commands.CLOSING_BRACKET:
|
||||
break
|
||||
elif c.token not in "lcr|":
|
||||
raise InvalidAlignmentError
|
||||
alignment += c.token
|
||||
children = tuple(children_iter)
|
||||
elif (
|
||||
len(children)
|
||||
and children[0].children is not None
|
||||
and (
|
||||
children[0].token == commands.BRACES
|
||||
or (environment.endswith("*") and children[0].token == commands.BRACKETS)
|
||||
)
|
||||
and all(c.token in "lcr|" for c in children[0].children)
|
||||
):
|
||||
alignment = "".join(c.token for c in children[0].children)
|
||||
children = children[1:]
|
||||
|
||||
return Node(token=rf"\{environment}", children=children, alignment=alignment)
|
||||
Reference in New Issue
Block a user