From 227173c6a5ced991da7b373b85d39e065a4dcced Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 23 Jan 2026 21:13:09 -0300 Subject: [PATCH 1/2] Use MathicsScanner tables for render LaTeX --- admin-tools/make-JSON-tables.sh | 2 ++ mathics/core/convert/op.py | 8 ++++-- mathics/format/render/latex.py | 50 ++++++++++++++++++++++----------- test/format/format_tests.yaml | 18 ++++++------ 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/admin-tools/make-JSON-tables.sh b/admin-tools/make-JSON-tables.sh index 8a166d9da..5172988e8 100755 --- a/admin-tools/make-JSON-tables.sh +++ b/admin-tools/make-JSON-tables.sh @@ -6,6 +6,8 @@ PYTHON=${PYTHON:-python} cd $mydir/../mathics/data mathics3-generate-json-table \ + --field=latex-named-characters \ + --field=unicode-to-latex \ --field=ascii-operator-to-symbol \ --field=ascii-operator-to-unicode \ --field=ascii-operator-to-wl-unicode \ diff --git a/mathics/core/convert/op.py b/mathics/core/convert/op.py index f93b0d7a8..de474ec72 100644 --- a/mathics/core/convert/op.py +++ b/mathics/core/convert/op.py @@ -29,10 +29,12 @@ unicode_operator_to_ascii = { val: operator_to_ascii[key] for key, val in operator_to_unicode.items() } -unicode_to_amslatex = OPERATOR_CONVERSION_TABLES["unicode-to-amslatex"] +UNICODE_TO_AMSLATEX = OPERATOR_CONVERSION_TABLES["unicode-to-amslatex"] +UNICODE_TO_LATEX = OPERATOR_CONVERSION_TABLES["unicode-to-latex"] -amstex_operators = { + +AMSTEX_OPERATORS = { "\u2032": "'", "\u2032\u2032": "''", "\u2062": " ", @@ -92,7 +94,7 @@ def hex_form_code(char_str): return hex(ord(char_str))[2:] for candidate_dict in ( - unicode_to_amslatex, + UNICODE_TO_AMSLATEX, # amstex_operators, unicode_operator_to_ascii, ): diff --git a/mathics/format/render/latex.py b/mathics/format/render/latex.py index fe9adc75c..114dcb4f0 100644 --- a/mathics/format/render/latex.py +++ b/mathics/format/render/latex.py @@ -31,7 +31,12 @@ ) from mathics.builtin.colors.color_directives import RGBColor from mathics.core.atoms import String -from mathics.core.convert.op import amstex_operators, get_latex_operator +from mathics.core.convert.op import ( + AMSTEX_OPERATORS, + UNICODE_TO_AMSLATEX, + UNICODE_TO_LATEX, + get_latex_operator, +) from mathics.core.exceptions import BoxConstructError from mathics.core.formatter import ( add_conversion_fn, @@ -57,26 +62,37 @@ "^": r"{}^{\wedge}", "~": r"\sim{}", "|": r"\vert{}", - "\u222b": r"\int ", - "\u2146": r"\, d", - "\uF74C": r"\, d", - "\U0001D451": r"\, d", - "\u00d7": r"\times ", } -TEX_TEXT_REPLACE = TEX_REPLACE.copy() +TEX_TEXT_REPLACE = { + r"{": "\{", + r"}": "\}", + "<": r"$<$", + ">": r"$>$", + "~": r"$\sim$", + "|": r"$\vert$", + "\\": r"$\backslash$", + "^": r"${}^{\wedge}$", +} + +TEX_REPLACE.update(UNICODE_TO_AMSLATEX) +TEX_REPLACE.update( + { + key: r"\text{" + val + "}" + for key, val in UNICODE_TO_LATEX.items() + if key not in TEX_REPLACE + } +) + +TEX_TEXT_REPLACE.update(UNICODE_TO_LATEX) TEX_TEXT_REPLACE.update( { - "<": r"$<$", - ">": r"$>$", - "~": r"$\sim$", - "|": r"$\vert$", - "\\": r"$\backslash$", - "^": r"${}^{\wedge}$", - "\u222b": r"$\int$ ", - "\uF74C": r"\, d", - "\u00d7": r"$\times$", + key: f"${val}$" + for key, val in UNICODE_TO_AMSLATEX.items() + if key not in TEX_TEXT_REPLACE } ) + + TEX_REPLACE_RE = re.compile("([" + "".join([re.escape(c) for c in TEX_REPLACE]) + "])") @@ -117,7 +133,7 @@ def render(format, string, in_text=False): return render("%s", text) else: # First consider the special cases - op_string = amstex_operators.get(text, None) + op_string = AMSTEX_OPERATORS.get(text, None) if op_string: return op_string diff --git a/test/format/format_tests.yaml b/test/format/format_tests.yaml index b8eb032e6..a94ae3af1 100644 --- a/test/format/format_tests.yaml +++ b/test/format/format_tests.yaml @@ -64,10 +64,10 @@ '"\[Pi] is a trascendental number"': msg: A String latex: - System`InputForm: "\\text{``\u03C0 is a trascendental number''}" - System`OutputForm: "\\text{\u03C0 is a trascendental number}" - System`StandardForm: "\\text{\u03C0 is a trascendental number}" - System`TraditionalForm: "\\text{\u03C0 is a trascendental number}" + System`InputForm: "\\text{``$\\pi$ is a trascendental number''}" + System`OutputForm: "\\text{$\\pi$ is a trascendental number}" + System`StandardForm: "\\text{$\\pi$ is a trascendental number}" + System`TraditionalForm: "\\text{$\\pi$ is a trascendental number}" mathml: System`InputForm: "\u03C0 is a trascendental number" System`OutputForm: "\u03C0 is a trascendental number" @@ -455,12 +455,12 @@ Graphics[{}]: "Grid[{{\"Spanish\", \"Hola!\"},{\"Portuguese\", \"Ol\xE0!\"},{\"English\", \"Hi!\"}}]": msg: Strings in a GridBox latex: - System`InputForm: \text{Grid[\{\{"Spanish", "Hola!"\}, \{"Portuguese", "Olà!"\}, \{"English", "Hi!"\}\}]} + System`InputForm: \text{Grid[\{\{"Spanish", "Hola!"\}, \{"Portuguese", "Ol\`{a}!"\}, \{"English", "Hi!"\}\}]} System`OutputForm: '\text{Spanish Hola!\newline \newline - Portuguese Olà!\newline + Portuguese Ol\`{a}!\newline \newline @@ -469,9 +469,9 @@ Graphics[{}]: } ' System`StandardForm: "\\begin{array}{cc} \\text{Spanish} & \\text{Hola!}\\\\ \\\ - text{Portuguese} & \\text{Ol\xE0!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" + text{Portuguese} & \\text{Ol\\`{a}!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" System`TraditionalForm: "\\begin{array}{cc} \\text{Spanish} & \\text{Hola!}\\\\\ - \ \\text{Portuguese} & \\text{Ol\xE0!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" + \ \\text{Portuguese} & \\text{Ol\\`{a}!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" mathml: System`InputForm: "Grid[{{"Spanish", "Hola!"}, {"Portuguese", "Olà!"}, {"English", "Hi!"}}]" System`OutputForm: 'Spanish      Hola!Portuguese   Olà!English      Hi!' @@ -800,7 +800,7 @@ TableForm[{{a,b},{c,d}}]: msg: A greek letter symbol latex: System`InputForm: \alpha - System`OutputForm: \text{α} + System`OutputForm: \text{$\alpha$} System`StandardForm: \alpha System`TraditionalForm: \alpha mathml: From e5833f3a048c5431ba69a219259659d0e8e49231 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 24 Jan 2026 12:14:35 -0300 Subject: [PATCH 2/2] Handle text-mode character in operators. Handle . add tests. --- mathics/core/convert/op.py | 16 +++++++++------- mathics/format/render/latex.py | 5 +++-- test/format/test_latex.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/mathics/core/convert/op.py b/mathics/core/convert/op.py index de474ec72..c0fad95b1 100644 --- a/mathics/core/convert/op.py +++ b/mathics/core/convert/op.py @@ -108,10 +108,12 @@ def hex_form_code(char_str): # if it is already an ascii, return without changes. if unicode_op.isascii(): return unicode_op - - # the `unicode_op` cannot be converted into an ascii string. Show a - # warning and return a `\symbol{code}` expression. - logging.warning( - "Unicode op" + unicode_op + "(" + hex_form_code(unicode_op) + ") not found." - ) - return '\\symbol{"' + hex_form_code(unicode_op) + "}" + try: + return r"\text{" + UNICODE_TO_LATEX[unicode_op] + "}" + except KeyError: + # the `unicode_op` cannot be converted into an ascii string. Show a + # warning and return a `\symbol{code}` expression. + logging.warning( + "Unicode op" + unicode_op + "(" + hex_form_code(unicode_op) + ") not found." + ) + return '\\symbol{"' + hex_form_code(unicode_op) + "}" diff --git a/mathics/format/render/latex.py b/mathics/format/render/latex.py index 114dcb4f0..e432992ca 100644 --- a/mathics/format/render/latex.py +++ b/mathics/format/render/latex.py @@ -64,8 +64,9 @@ "|": r"\vert{}", } TEX_TEXT_REPLACE = { - r"{": "\{", - r"}": "\}", + r"{": r"\{", + r"}": r"\}", + r"_": r"\_", "<": r"$<$", ">": r"$>$", "~": r"$\sim$", diff --git a/test/format/test_latex.py b/test/format/test_latex.py index bd631a191..d7e851e9e 100644 --- a/test/format/test_latex.py +++ b/test/format/test_latex.py @@ -22,6 +22,8 @@ def get_latex(wl_expression): @pytest.mark.parametrize( ("testcase", "expected"), [ + ("_", r"\_"), + ('"_"', r"\text{\_}"), ('"["', r"\text{[}"), ('"]"', r"\text{]}"), ("HoldForm[A[[1,2]]]", r"A\left[\left[1, 2\right]\right]"), @@ -31,6 +33,22 @@ def get_latex(wl_expression): ("CupCap[c,b]", r"c \stackrel{\smile}{\frown} b"), ("Congruent[c,b]", r"c \equiv b"), ("Pi", r"\pi"), + # In symbols and expressions + (r"\[Alpha]", r"\alpha"), + # In this case, without the linebreak, the tokeniser + # produce an error... + ("\\[Alpha]s\n", r"\text{$\alpha$s}"), + (r"\[Alpha] s", r"s \alpha"), + (r"\[AAcute]", r"\text{\'{a}}"), + # In this case, without the linebreak, the tokeniser + # produce an error... + ("\\[AAcute]s\n", r"\text{\'{a}s}"), + (r"\[AAcute] s", r"s \text{\'{a}}"), + # In strings + (r'"\[Alpha]"', r"\text{$\alpha$}"), + (r'"\[Alpha]s"', r"\text{$\alpha$s}"), + (r'"\[AAcute]"', r"\text{\'{a}}"), + (r'"M\[AAcute]s!"', r"\text{M\'{a}s!}"), ], ) def test_expressions(testcase, expected):