From 95d09f2bee15016c928ef1f699d904cf52a8e4d7 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Wed, 28 Jan 2026 22:16:23 -0300 Subject: [PATCH 01/31] Improve processing of MathMLForm for Input and Output Form --- mathics/builtin/forms/print.py | 1 - mathics/format/render/mathml.py | 77 +++++++++++------- test/format/format_tests-WMA.yaml | 111 ++++++++++++++------------ test/format/format_tests.yaml | 126 +++++++++++------------------- test/test_session.py | 3 +- 5 files changed, 156 insertions(+), 162 deletions(-) diff --git a/mathics/builtin/forms/print.py b/mathics/builtin/forms/print.py index cf730d17c..239c52e46 100644 --- a/mathics/builtin/forms/print.py +++ b/mathics/builtin/forms/print.py @@ -158,7 +158,6 @@ class OutputForm(FormBaseClass): = -Graphics- """ - formats = {"OutputForm[s_String]": "s"} summary_text = "format expression in plain text" diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 5e99980f9..d7f9b6ff8 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -45,6 +45,7 @@ def encode_mathml(text: str) -> str: return text +# "Operators" which are not in display_operators_set extra_operators = { ",", "(", @@ -53,20 +54,21 @@ def encode_mathml(text: str) -> str: "]", "{", "}", - "\u301a", - "\u301b", - "\u00d7", - "\u2032", - "\u2032\u2032", - " ", - "\u2062", - "\u222b", - "\u2146", + "\u301a", # [[ + "\u301b", # ]] + "\u00d7", # \[Times] + "\u2032", # \[RawComma] + "\u2032\u2032", # \[RawComma]\[RawComma] + " ", # \[RawSpace] + "\u2062", # \[InvisibleTimes] + "\u222b", # \[Integral] + "\u2146", # \[DifferentialD] } def string(self, **options) -> str: text = self.value + print("mathml string", text, "options:", options) number_as_text = options.get("number_as_text", None) show_string_characters = ( @@ -81,13 +83,10 @@ def render(format, string): return format % encoded_text if text.startswith('"') and text.endswith('"'): - if show_string_characters: - return render("%s", text[1:-1]) - else: - outtext = "" - for line in text[1:-1].split("\n"): - outtext += render("%s", line) - return outtext + text = text[1:-1] + if not show_string_characters: + return render("%s", text) + return render("%s", text) elif ( text and (number_as_text is SymbolFalse) @@ -102,11 +101,11 @@ def render(format, string): # Mathics-Django: if text == "": return "" - if text == "\u2146": + if text == "\u2146": # DifferentialD return render( '%s', text ) - if text == "\u2062": + if text == "\u2062": # InvisibleTimes return render( '%s', text ) @@ -114,17 +113,35 @@ def render(format, string): elif is_symbol_name(text): return render("%s", text) else: - outtext = "" - for line in text.split("\n"): - outtext += render("%s", line) - return outtext + return "".join( + render("%s", line) for line in text.split("\n") + ) add_conversion_fn(String, string) def interpretation_box(self, **options): - return lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) + boxes = self.boxes + origin = self.expr + if origin.has_form("InputForm", None): + # InputForm produce outputs of the form + # InterpretationBox[Style[_String, ...], origin_InputForm, opts___] + assert isinstance(boxes, StyleBox), f"boxes={boxes} are not a StyleBox" + boxes = boxes.boxes + options["System`ShowStringCharacters"] = SymbolTrue + assert isinstance(boxes, String) + # Remove the outer quotes + elif origin.has_form("OutputForm", None): + # OutputForm produce outputs of the form + # InterpretationBox[PaneBox[_String, ...], origin_OutputForm, opts___] + assert boxes.has_form("PaneBox", 1, None) + boxes = boxes.boxes + assert isinstance(boxes, String) + # Remove the outer quotes + boxes = String(boxes.value) + + return lookup_conversion_method(boxes, "mathml")(boxes, **options) add_conversion_fn(InterpretationBox, interpretation_box) @@ -172,7 +189,7 @@ def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) options = _options - return "%s %s" % ( + return "\n%s\n %s\n" % ( lookup_conversion_method(self.num, "mathml")(self.num, **options), lookup_conversion_method(self.den, "mathml")(self.den, **options), ) @@ -232,7 +249,7 @@ def sqrtbox(self, **options): lookup_conversion_method(self.index, "mathml")(self.index, **options), ) - return " %s " % lookup_conversion_method(self.radicand, "mathml")( + return "\n %s\n" % lookup_conversion_method(self.radicand, "mathml")( self.radicand, **options ) @@ -244,7 +261,7 @@ def subscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "%s %s" % ( + return "\n%s\n %s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), ) @@ -257,7 +274,7 @@ def superscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "%s %s" % ( + return "\n%s \n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), ) @@ -271,7 +288,7 @@ def subsuperscriptbox(self, **options): _options.update(options) options = _options options["inside_row"] = True - return "%s %s %s" % ( + return "\n%s\n %s\n %s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), @@ -319,7 +336,7 @@ def is_list_interior(content): # print(f"mrow: {result}") - return "%s" % " ".join(result) + return "\n%s\n" % " ".join(result) add_conversion_fn(RowBox, rowbox) @@ -365,7 +382,7 @@ def graphics3dbox(self, elements=None, **options) -> str: """Turn the Graphics3DBox into a MathML string""" json_repr = self.boxes_to_json(elements, **options) mathml = f'' - mathml = f"{mathml}" + mathml = f"\n\n\n{mathml}\n\n\n" return mathml diff --git a/test/format/format_tests-WMA.yaml b/test/format/format_tests-WMA.yaml index 4a00ee4b1..975084146 100644 --- a/test/format/format_tests-WMA.yaml +++ b/test/format/format_tests-WMA.yaml @@ -23,7 +23,6 @@ # because we use both in documentation and in the web interface. # - '"-7.32"': msg: A String with a number latex: @@ -402,36 +401,15 @@ Graphics[{}]: Grid[{{a,b},{c,d}}]: msg: GridBox latex: - InputForm: \text{Grid[\{\{a, b\}, \{c, d\}\}]} + InputForm: '\text{Grid[$\{\{$a, b$\}$, $\{$c, d$\}\}$]}' OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} - StandardForm: \begin{array}{cc} a & b\\ c & d\end{array} - TraditionalForm: \begin{array}{cc} a & b\\ c & d\end{array} + StandardForm: "\\begin{array}{cc}\n a & b\\\\\n c & d\n\\end{array}" + TraditionalForm: "\\begin{array}{cc}\n a & b\\\\\n c & d\n\\end{array}" mathml: - InputForm: Grid [ { { - a b } - { c - d } } ] - OutputForm: ' - - ab - - cd - - ' - StandardForm: ' - - ab - - cd - - ' - TraditionalForm: ' - - ab - - cd - - ' + InputForm: 'Grid [ { { a b } { c d } } ]' + StandardForm: "\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n " + TraditionalForm: "\n ab\n + cd\n " text: InputForm: Grid[{{a, b}, {c, d}}] OutputForm: 'a b @@ -556,12 +534,8 @@ Subscript[a, 4]: - Subscript [ a 4 ] - Fragile! - OutputForm: - - Subscript [ a - 4 ] - - Fragile! - StandardForm: a 4 - TraditionalForm: a 4 + StandardForm: "\n a\n 4\n " + TraditionalForm: "\n a\n 4\n " text: InputForm: Subscript[a, 4] OutputForm: "a\n 4" @@ -583,8 +557,8 @@ Subsuperscript[a, p, q]: p q ] OutputForm: Subsuperscript [ a p q ] - StandardForm: a p q - TraditionalForm: a p q + StandardForm: "\n a\n p\n q\n " + TraditionalForm: "\n a\n p\n q\n " text: InputForm: Subsuperscript[a, p, q] OutputForm: " q\na\n p" @@ -703,15 +677,13 @@ a^(g[b]/c): latex: InputForm: \text{a${}^{\wedge}$(g[b]/c)} OutputForm: a\text{ ${}^{\wedge}$ }\left(g(b)\text{ / }c\right) - StandardForm: a^{\frac{g(b)}{c}} + StandardForm: a^{\frac{g[b]}{c}} TraditionalForm: a^{\frac{g(b)}{c}} mathml: - InputForm: a ^ ( b -  /  c ) - OutputForm: a  ^  ( - b  /  c ) - StandardForm: a b c - TraditionalForm: a g ( b ) c + InputForm: 'a^(g[b]/c)' + OutputForm: '<><>' + StandardForm: "\n a\n \n \n g\n [\n b\n ]\n \n c\n \n " + TraditionalForm: "\n a\n \n \n g\n \n (\n b\n )\n \n c\n \n " text: InputForm: a^(g[b]/c) OutputForm: " g[b]/c\na" @@ -727,12 +699,55 @@ a^4: StandardForm: a^4 TraditionalForm: a^4 mathml: - InputForm: a ^ 4 - OutputForm: a  ^  4 - StandardForm: a 4 - TraditionalForm: a 4 + InputForm: a^4 + StandardForm: "\n a\n 4\n " + TraditionalForm: "\n a\n 4\n " text: InputForm: a^4 OutputForm: " 4\na" StandardForm: "\\!\\(\\*SuperscriptBox[\"a\", \"4\"]\\)" TraditionalForm: "\\!\\(\\*FormBox[SuperscriptBox[\"a\", \"4\"], TraditionalForm]\\)" + + +Optional[x__]: + msg: Optional with one argument + latex: + System`OutputForm: '\text{x$\_\_$.}' + System`StandardForm: '\text{x$\_\_$.}' + mathml: + System`OutputForm: 'x__.' + System`StandardForm: 'x__.' + text: + System`InputForm: 'x__.' + System`OutputForm: 'x__.' + System`StandardForm: '\!\(\*RowBox[{"Optional", "[", "x__", "]"}]\)' + System`TraditionalForm: '\!\(\*FormBox[RowBox[{"Optional", "[", "x__", "]"}], TraditionalForm]\)' + + +Optional[x__, a+b]: + msg: Optional with two arguments + latex: + System`OutputForm: '\text{x$\_\_$:a + b}' + System`StandardForm: '\text{x$\_\_$}:a+b' + mathml: + System`OutputForm: 'x__ : a + b' + text: + System`InputForm: 'x__:a + b' + System`OutputForm: 'x__:a + b' + System`StandardForm: '\!\(\*RowBox[{"x__", ":", RowBox[{"a", "+", "b"}]}]\)' + System`TraditionalForm: '\!\(\*FormBox[RowBox[{"x__", ":", RowBox[{"a", "+", "b"}]}], TraditionalForm]\)' + + +a+PrecedenceForm[b+c,10]: + msg: "PrecedenceForm" + latex: + System`OutputForm: '\text{a + (b + c)}' + System`StandardForm: 'a+(b+c)' + mathml: + System`OutputForm: 'a + (b + c)' + System`StandardForm: "\n a\n +\n \n (\n \n b\n +\n c\n \n )\n \n " + text: + System`InputForm: 'a + PrecedenceForm[b + c, 10]' + System`OutputForm: 'a + (b + c)' + System`StandardForm: '\!\(\*RowBox[{"a", "+", RowBox[{"(", TagBox[RowBox[{"b", "+", "c"}], Function[PrecedenceForm[Slot[1], 10]]], ")"}]}]\)' + System`TraditionalForm: '\!\(\*FormBox[RowBox[{"a", "+", RowBox[{"(", TagBox[RowBox[{"b", "+", "c"}], Function[PrecedenceForm[Slot[1], 10]]], ")"}]}], TraditionalForm]\)' diff --git a/test/format/format_tests.yaml b/test/format/format_tests.yaml index 3a7ad203c..7f2150b50 100644 --- a/test/format/format_tests.yaml +++ b/test/format/format_tests.yaml @@ -139,8 +139,8 @@ mathml: System`InputForm: -4 System`OutputForm: -4 - System`StandardForm: - 4 - System`TraditionalForm: - 4 + System`StandardForm: "\n- 4\n" + System`TraditionalForm: "\n- 4\n" text: System`InputForm: '-4' System`OutputForm: '-4' @@ -158,8 +158,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.32 - System`StandardForm: - 4.32 - System`TraditionalForm: - 4.32 + System`StandardForm: "\n- 4.32\n" + System`TraditionalForm: "\n- 4.32\n" text: System`InputForm: '-4.32' System`OutputForm: '-4.32' @@ -177,8 +177,8 @@ mathml: System`InputForm: -4.33 System`OutputForm: -4.3 - System`StandardForm: - 4.33 - System`TraditionalForm: - 4.33 + System`StandardForm: "\n- 4.33\n" + System`TraditionalForm: "\n- 4.33\n" text: System`InputForm: -4.33`2. System`OutputForm: '-4.3' @@ -196,8 +196,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.320 - System`StandardForm: - 4.32 - System`TraditionalForm: - 4.32 + System`StandardForm: "\n- 4.32\n" + System`TraditionalForm: "\n- 4.32\n" text: System`InputForm: -4.32`4. System`OutputForm: '-4.320' @@ -272,8 +272,7 @@ - '1 / (1 + 1 / (1 + 1 / a))' - Fragile! System`StandardForm: &id001 - - 1 1 + 1 1 - + 1 a + - "\n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n" - Fragile! System`TraditionalForm: *id001 text: @@ -293,14 +292,8 @@ mathml: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t|>|>' - System`StandardForm: <| a -> - x , b -> y - , c -> <| d - -> t |> |> - System`TraditionalForm: <| a -> - x , b -> y - , c -> <| d - -> t |> |> + System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" + System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t|>|> @@ -316,18 +309,10 @@ Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: System`StandardForm: '\text{$<$$\vert$}a->x, b->y, c->\text{$<$$\vert$}d->t, e->u\text{$\vert$$>$}\text{$\vert$$>$}' System`TraditionalForm: '\text{$<$$\vert$}a->x, b->y, c->\text{$<$$\vert$}d->t, e->u\text{$\vert$$>$}\text{$\vert$$>$}' mathml: - System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> + System`InputForm: "<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>" System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>' - System`StandardForm: <| a -> - x , b -> y - , c -> <| d - -> t , e -> - u |> |> - System`TraditionalForm: <| a -> - x , b -> y - , c -> <| d - -> t , e -> - u |> |> + System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" + System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> @@ -345,11 +330,8 @@ Complex[1.09*^12, 3.]: mathml: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: '1.09×10^12 + 3. I' - System`StandardForm: 1.09 *^ 12 - + 3.   I - System`TraditionalForm: "1.09 \xD7 10\ - \ 12 + 3. \u2062 I" + System`StandardForm: "\n\n1.09 *^ 12\n + \n3.   I\n\n" + System`TraditionalForm: "\n\n1.09 × \n10 \n12\n\n + \n3. I\n\n" text: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: "1.09\xD710^12 + 3. I" @@ -475,7 +457,7 @@ Graphics[{}]: \ \\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!' + System`OutputForm: 'Spanish      Hola!Portuguese   Olà!English      Hi!' System`StandardForm: "\nSpanishHola!\n\ PortugueseGrid[{{a, b}, {c, d}}] - System`OutputForm: 'a   bc   d' + System`OutputForm: 'a   bc   d' System`StandardForm: ' ab @@ -559,11 +541,15 @@ Integrate[F[x], {x, a, g[b]}]: System`StandardForm: \int_a^{g\left[b\right]} F\left[x\right] \, dx System`TraditionalForm: \int_a^{g\left(b\right)} F\left(x\right) \, dx mathml: - System`InputForm: Integrate[F[x], {x, a, g[b]}] + System`InputForm: 'Integrate[F[x], {x, a, g[b]}]' System`OutputForm: 'Integrate[F[x], {x, a, g[b]}]' + System`StandardForm: "\n\n\n a\n \ng [ b ]\n\n \nF [ x ]\n \n𝑑 x\n\n" + System`TraditionalForm: "\n\n\n a\n \ng ( b )\n\n \nF ( x )\n \n𝑑 x\n\n" text: + System`InputForm: Integrate[F[x], {x, a, g[b]}] System`OutputForm: Integrate[F[x], {x, a, g[b]}] - + System`StandardForm: 'Subsuperscript[∫, a, g[b]]⁢F[x]⁢𝑑x' + System`TraditionalForm: 'Subsuperscript[∫, a, g(b)]⁢F(x)⁢𝑑x' MatrixForm[{{a,b},{c,d}}]: msg: GridBox in a matrix @@ -581,21 +567,9 @@ MatrixForm[{{a,b},{c,d}}]: System`TraditionalForm: \left(\begin{array}{cc} a & b\\ c & d\end{array}\right) mathml: System`InputForm: MatrixForm[{{a, b}, {c, d}}] - System`OutputForm: 'a   bc   d' - System`StandardForm: '( - - ab - - cd - - )' - System`TraditionalForm: '( - - ab - - cd - - )' + System`OutputForm: 'a   bc   d' + System`StandardForm: "\n( \nab\ncd\n )\n" + System`TraditionalForm: "\n( \nab\ncd\n )\n" text: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -627,17 +601,17 @@ Sqrt[1/(1+1/(1+1/a))]: System`TraditionalForm: \sqrt{\frac{1}{1+\frac{1}{1+\frac{1}{a}}}} mathml: System`InputForm: - - Sqrt[1/(1 + 1/(1 + 1/a))] + - Sqrt[1/(1 + 1/(1 + 1/a))] - Fragile! System`OutputForm: - 'Sqrt[1 / (1 + 1 / (1 + 1 / a))]' - Fragile! - System`StandardForm: &id002 - - 1 1 + 1 1 - + 1 a - + System`StandardForm: + - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" - Fragile! - System`TraditionalForm: *id002 + System`TraditionalForm: + - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" + - Fragile! text: System`InputForm: Sqrt[1/(1 + 1/(1 + 1/a))] System`OutputForm: Sqrt[1 / (1 + 1 / (1 + 1 / a))] @@ -659,8 +633,8 @@ Subscript[a, 4]: System`OutputForm: - 'Subscript[a, 4]' - Fragile! - System`StandardForm: a 4 - System`TraditionalForm: a 4 + System`StandardForm: "\na\n 4\n" + System`TraditionalForm: "\na\n 4\n" text: System`InputForm: Subscript[a, 4] System`OutputForm: Subscript[a, 4] @@ -680,8 +654,8 @@ Subsuperscript[a, p, q]: mathml: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: 'Subsuperscript[a, p, q]' - System`StandardForm: a p q - System`TraditionalForm: a p q + System`StandardForm: "\na\n p\n q\n" + System`TraditionalForm: "\na\n p\n q\n" text: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: Subsuperscript[a, p, q] @@ -741,21 +715,9 @@ TableForm[{{a,b},{c,d}}]: System`TraditionalForm: \begin{array}{cc} a & b\\ c & d\end{array} mathml: System`InputForm: TableForm[{{a, b}, {c, d}}] - System`OutputForm: 'a   bc   d' - System`StandardForm: ' - - ab - - cd - - ' - System`TraditionalForm: ' - - ab - - cd - - ' + System`OutputForm: 'a   bc   d' + System`StandardForm: "\nab\ncd\n" + System`TraditionalForm: "\nab\ncd\n" text: System`InputForm: TableForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -845,7 +807,7 @@ a^(g[b]/c): mathml: System`InputForm: a^(g[b]/c) System`OutputForm: 'a ^ (g[b] / c)' - System`TraditionalForm: a g ( b ) c + System`TraditionalForm: "\na \n\n\ng ( b )\n\n c\n\n" text: System`InputForm: a^(g[b]/c) System`OutputForm: a ^ (g[b] / c) @@ -863,8 +825,8 @@ a^4: mathml: System`InputForm: 'a^4' System`OutputForm: 'a ^ 4' - System`StandardForm: a 4 - System`TraditionalForm: a 4 + System`StandardForm: "\na \n4\n" + System`TraditionalForm: "\na \n4\n" text: System`InputForm: a^4 System`OutputForm: a ^ 4 @@ -908,7 +870,7 @@ a+PrecedenceForm[b+c,10]: System`StandardForm: 'a+\left(b+c\right)' mathml: System`OutputForm: 'a + (b + c)' - System`StandardForm: 'a + ( b + c )' + System`StandardForm: "\na + \n( \nb + c\n )\n\n" text: System`InputForm: 'a + (PrecedenceForm[b + c, 10])' System`OutputForm: 'a + (b + c)' diff --git a/test/test_session.py b/test/test_session.py index 132d1617c..8716a6893 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -55,11 +55,12 @@ def test_session_format_evaluation(): assert session.format_result(form="text") == "a / b" assert session.format_result(form="latex") == "\\frac{a}{b}" assert session.format_result(form="xml") == ( - '' "a b" "" + '\n' "a\n b" "\n" ) def test_session_parse(): + session.reset() parsed = session.parse("a/b") expected = Expression( SymbolTimes, From 842fc5895418f4d05e426978bb97a1d77c1dec58 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Thu, 29 Jan 2026 06:57:56 -0300 Subject: [PATCH 02/31] remove trailing print --- mathics/format/render/mathml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index d7f9b6ff8..a926c750c 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -68,7 +68,6 @@ def encode_mathml(text: str) -> str: def string(self, **options) -> str: text = self.value - print("mathml string", text, "options:", options) number_as_text = options.get("number_as_text", None) show_string_characters = ( From 466bc6ed84eb1628fc4156f7364189dc3d74bc29 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Thu, 29 Jan 2026 10:58:34 -0300 Subject: [PATCH 03/31] remove space from extra_operators, and add a comment --- mathics/format/render/mathml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index a926c750c..59698ea13 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -54,12 +54,12 @@ def encode_mathml(text: str) -> str: "]", "{", "}", + # TODO: check why the following characters are not in `operators`: "\u301a", # [[ "\u301b", # ]] "\u00d7", # \[Times] "\u2032", # \[RawComma] "\u2032\u2032", # \[RawComma]\[RawComma] - " ", # \[RawSpace] "\u2062", # \[InvisibleTimes] "\u222b", # \[Integral] "\u2146", # \[DifferentialD] From 8f0f52b870034a50d6d415e7d21fe6f3ed481f4a Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Thu, 29 Jan 2026 13:50:15 -0300 Subject: [PATCH 04/31] add a comment about mathml multiline output. Give a more consistent output --- mathics/format/render/mathml.py | 36 ++++++++++++++++---- test/format/format_tests.yaml | 58 ++++++++++++++++----------------- test/test_session.py | 2 +- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 59698ea13..a9ecb2297 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -3,6 +3,30 @@ Lower-level formatter of Mathics objects as MathML strings. MathML formatting is usually initiated in Mathics via MathMLForm[]. + +For readability, and following WMA MathML generated code, tags \ +containing sub-tags are split on several lines, one by +sub element. For example, the Box expression + +>> FractionBox[RowBox[{"a", "+", SuperscriptBox["b", "c"]}], "d"] + +produces +``` + + +a ++ + +b +c + + +d + +``` +In WMA, each line would be also indented adding one space on each \ +level of indentation. + """ import base64 @@ -188,7 +212,7 @@ def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s\n %s\n" % ( + return "\n%s\n%s\n" % ( lookup_conversion_method(self.num, "mathml")(self.num, **options), lookup_conversion_method(self.den, "mathml")(self.den, **options), ) @@ -248,7 +272,7 @@ def sqrtbox(self, **options): lookup_conversion_method(self.index, "mathml")(self.index, **options), ) - return "\n %s\n" % lookup_conversion_method(self.radicand, "mathml")( + return "\n%s\n" % lookup_conversion_method(self.radicand, "mathml")( self.radicand, **options ) @@ -260,7 +284,7 @@ def subscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s\n %s\n" % ( + return "\n%s\n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), ) @@ -273,7 +297,7 @@ def superscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s \n%s\n" % ( + return "\n%s\n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), ) @@ -287,7 +311,7 @@ def subsuperscriptbox(self, **options): _options.update(options) options = _options options["inside_row"] = True - return "\n%s\n %s\n %s\n" % ( + return "\n%s\n%s\n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), @@ -335,7 +359,7 @@ def is_list_interior(content): # print(f"mrow: {result}") - return "\n%s\n" % " ".join(result) + return "\n%s\n" % "\n".join(result) add_conversion_fn(RowBox, rowbox) diff --git a/test/format/format_tests.yaml b/test/format/format_tests.yaml index 7f2150b50..6254bbd02 100644 --- a/test/format/format_tests.yaml +++ b/test/format/format_tests.yaml @@ -139,8 +139,8 @@ mathml: System`InputForm: -4 System`OutputForm: -4 - System`StandardForm: "\n- 4\n" - System`TraditionalForm: "\n- 4\n" + System`StandardForm: "\n-\n4\n" + System`TraditionalForm: "\n-\n4\n" text: System`InputForm: '-4' System`OutputForm: '-4' @@ -158,8 +158,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.32 - System`StandardForm: "\n- 4.32\n" - System`TraditionalForm: "\n- 4.32\n" + System`StandardForm: "\n-\n4.32\n" + System`TraditionalForm: "\n-\n4.32\n" text: System`InputForm: '-4.32' System`OutputForm: '-4.32' @@ -177,8 +177,8 @@ mathml: System`InputForm: -4.33 System`OutputForm: -4.3 - System`StandardForm: "\n- 4.33\n" - System`TraditionalForm: "\n- 4.33\n" + System`StandardForm: "\n-\n4.33\n" + System`TraditionalForm: "\n-\n4.33\n" text: System`InputForm: -4.33`2. System`OutputForm: '-4.3' @@ -196,8 +196,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.320 - System`StandardForm: "\n- 4.32\n" - System`TraditionalForm: "\n- 4.32\n" + System`StandardForm: "\n-\n4.32\n" + System`TraditionalForm: "\n-\n4.32\n" text: System`InputForm: -4.32`4. System`OutputForm: '-4.320' @@ -272,7 +272,7 @@ - '1 / (1 + 1 / (1 + 1 / a))' - Fragile! System`StandardForm: &id001 - - "\n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n" + - "\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n" - Fragile! System`TraditionalForm: *id001 text: @@ -292,8 +292,8 @@ mathml: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t|>|>' - System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" - System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" + System`StandardForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\nd\n->\nt\n\n|>\n\n\n\n|>\n" + System`TraditionalForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\nd\n->\nt\n\n|>\n\n\n\n|>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t|>|> @@ -311,8 +311,8 @@ Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: mathml: System`InputForm: "<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>" System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>' - System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" - System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" + System`StandardForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\n\nd\n->\nt\n\n,\n\ne\n->\nu\n\n\n|>\n\n\n\n|>\n" + System`TraditionalForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\n\nd\n->\nt\n\n,\n\ne\n->\nu\n\n\n|>\n\n\n\n|>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> @@ -330,8 +330,8 @@ Complex[1.09*^12, 3.]: mathml: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: '1.09×10^12 + 3. I' - System`StandardForm: "\n\n1.09 *^ 12\n + \n3.   I\n\n" - System`TraditionalForm: "\n\n1.09 × \n10 \n12\n\n + \n3. I\n\n" + System`StandardForm: "\n\n1.09\n*^\n12\n\n+\n\n3.\n \nI\n\n" + System`TraditionalForm: "\n\n1.09\n×\n\n10\n12\n\n\n+\n\n3.\n\nI\n\n" text: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: "1.09\xD710^12 + 3. I" @@ -543,8 +543,8 @@ Integrate[F[x], {x, a, g[b]}]: mathml: System`InputForm: 'Integrate[F[x], {x, a, g[b]}]' System`OutputForm: 'Integrate[F[x], {x, a, g[b]}]' - System`StandardForm: "\n\n\n a\n \ng [ b ]\n\n \nF [ x ]\n \n𝑑 x\n\n" - System`TraditionalForm: "\n\n\n a\n \ng ( b )\n\n \nF ( x )\n \n𝑑 x\n\n" + System`StandardForm: "\n\n\na\n\ng\n[\nb\n]\n\n\n\n\nF\n[\nx\n]\n\n\n\n𝑑\nx\n\n" + System`TraditionalForm: "\n\n\na\n\ng\n(\nb\n)\n\n\n\n\nF\n(\nx\n)\n\n\n\n𝑑\nx\n\n" text: System`InputForm: Integrate[F[x], {x, a, g[b]}] System`OutputForm: Integrate[F[x], {x, a, g[b]}] @@ -568,8 +568,8 @@ MatrixForm[{{a,b},{c,d}}]: mathml: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a   bc   d' - System`StandardForm: "\n( \nab\ncd\n )\n" - System`TraditionalForm: "\n( \nab\ncd\n )\n" + System`StandardForm: "\n(\n\nab\ncd\n\n)\n" + System`TraditionalForm: "\n(\n\nab\ncd\n\n)\n" text: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -607,10 +607,10 @@ Sqrt[1/(1+1/(1+1/a))]: - 'Sqrt[1 / (1 + 1 / (1 + 1 / a))]' - Fragile! System`StandardForm: - - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" + - "\n\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n\n" - Fragile! System`TraditionalForm: - - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" + - "\n\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n\n" - Fragile! text: System`InputForm: Sqrt[1/(1 + 1/(1 + 1/a))] @@ -633,8 +633,8 @@ Subscript[a, 4]: System`OutputForm: - 'Subscript[a, 4]' - Fragile! - System`StandardForm: "\na\n 4\n" - System`TraditionalForm: "\na\n 4\n" + System`StandardForm: "\na\n4\n" + System`TraditionalForm: "\na\n4\n" text: System`InputForm: Subscript[a, 4] System`OutputForm: Subscript[a, 4] @@ -654,8 +654,8 @@ Subsuperscript[a, p, q]: mathml: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: 'Subsuperscript[a, p, q]' - System`StandardForm: "\na\n p\n q\n" - System`TraditionalForm: "\na\n p\n q\n" + System`StandardForm: "\na\np\nq\n" + System`TraditionalForm: "\na\np\nq\n" text: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: Subsuperscript[a, p, q] @@ -807,7 +807,7 @@ a^(g[b]/c): mathml: System`InputForm: a^(g[b]/c) System`OutputForm: 'a ^ (g[b] / c)' - System`TraditionalForm: "\na \n\n\ng ( b )\n\n c\n\n" + System`TraditionalForm: "\na\n\n\ng\n(\nb\n)\n\nc\n\n" text: System`InputForm: a^(g[b]/c) System`OutputForm: a ^ (g[b] / c) @@ -825,8 +825,8 @@ a^4: mathml: System`InputForm: 'a^4' System`OutputForm: 'a ^ 4' - System`StandardForm: "\na \n4\n" - System`TraditionalForm: "\na \n4\n" + System`StandardForm: "\na\n4\n" + System`TraditionalForm: "\na\n4\n" text: System`InputForm: a^4 System`OutputForm: a ^ 4 @@ -870,7 +870,7 @@ a+PrecedenceForm[b+c,10]: System`StandardForm: 'a+\left(b+c\right)' mathml: System`OutputForm: 'a + (b + c)' - System`StandardForm: "\na + \n( \nb + c\n )\n\n" + System`StandardForm: "\na\n+\n\n(\n\nb\n+\nc\n\n)\n\n" text: System`InputForm: 'a + (PrecedenceForm[b + c, 10])' System`OutputForm: 'a + (b + c)' diff --git a/test/test_session.py b/test/test_session.py index 8716a6893..4f04fb0df 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -55,7 +55,7 @@ def test_session_format_evaluation(): assert session.format_result(form="text") == "a / b" assert session.format_result(form="latex") == "\\frac{a}{b}" assert session.format_result(form="xml") == ( - '\n' "a\n b" "\n" + '\na\nb\n' ) From f513083ee2ee0361b22df168a8137736b44854a1 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 30 Jan 2026 09:56:53 -0300 Subject: [PATCH 05/31] indentation --- mathics/format/box/outputforms.py | 2 +- mathics/format/render/mathml.py | 80 ++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/mathics/format/box/outputforms.py b/mathics/format/box/outputforms.py index 60d187f4d..415adba6e 100644 --- a/mathics/format/box/outputforms.py +++ b/mathics/format/box/outputforms.py @@ -30,7 +30,7 @@ def eval_mathmlform(expr: BaseElement, evaluation: Evaluation) -> BoxElementMixi boxes = format_element(expr, evaluation, SymbolTraditionalForm) try: - mathml = boxes.boxes_to_mathml(evaluation=evaluation) + mathml = boxes.boxes_to_mathml(evaluation=evaluation,_indent_level=1) except BoxError: evaluation.message( "General", diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index a9ecb2297..fa20b3a90 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -91,6 +91,7 @@ def encode_mathml(text: str) -> str: def string(self, **options) -> str: + text = self.value number_as_text = options.get("number_as_text", None) @@ -101,10 +102,15 @@ def string(self, **options) -> str: if number_as_text is None: number_as_text = SymbolFalse + + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + def render(format, string): encoded_text = encode_mathml(string) - return format % encoded_text + return indent_spaces + format % encoded_text + if text.startswith('"') and text.endswith('"'): text = text[1:-1] if not show_string_characters: @@ -171,6 +177,11 @@ def interpretation_box(self, **options): def pane_box(self, **options): + + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + options["_indent_level"] = indent_level + 1 + content = lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) options = self.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() @@ -201,8 +212,8 @@ def pane_box(self, **options): dims += "overflow:hidden;" dims = f' style="{dims}" ' if dims: - return f"\n{content}\n" - return content + return f"{indent_level}\n{content}\n{indent_level}" + return f"{indent_level}{content}" add_conversion_fn(PaneBox, pane_box) @@ -212,7 +223,10 @@ def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s\n%s\n" % ( + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + options["_indent_level"] = indent_level + 1 + return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.num, "mathml")(self.num, **options), lookup_conversion_method(self.den, "mathml")(self.den, **options), ) @@ -243,18 +257,22 @@ def boxes_to_mathml(box, **options): # invalid column alignment raise BoxConstructError joined_attrs = " ".join(f'{name}="{value}"' for name, value in attrs.items()) - result = f"\n" + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + result = f"{indent_spaces}\n" new_box_options = box_options.copy() new_box_options["inside_list"] = True + new_box_options["_indent_level"] = indent_level + 3 + for row in items: - result += "" + result += "{indent_spaces} \n" if isinstance(row, tuple): for item in row: - result += f"{boxes_to_mathml(item, **new_box_options)}" + result += f"\n{indent_spaces} {boxes_to_mathml(item, **new_box_options)}" else: - result += f"{boxes_to_mathml(row, **new_box_options)}" - result += "\n" - result += "" + result += f"\n{indent_spaces} \n{boxes_to_mathml(row, **new_box_options)}\n{indent_spaces} " + result += "{indent_spaces} \n" + result += "{indent_spaces}" # print(f"gridbox: {result}") return result @@ -266,13 +284,17 @@ def sqrtbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + options["_indent_level"] = indent_level + 1 + if self.index: - return " %s %s " % ( + return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.radicand, "mathml")(self.radicand, **options), lookup_conversion_method(self.index, "mathml")(self.index, **options), ) - return "\n%s\n" % lookup_conversion_method(self.radicand, "mathml")( + return f"{indent_spaces}\n%s\n{indent_spaces}" % lookup_conversion_method(self.radicand, "mathml")( self.radicand, **options ) @@ -284,7 +306,10 @@ def subscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s\n%s\n" % ( + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + options["_indent_level"] = indent_level + 1 + return "{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), ) @@ -297,7 +322,11 @@ def superscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s\n%s\n" % ( + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + options["_indent_level"] = indent_level + 1 + + return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), ) @@ -311,7 +340,12 @@ def subsuperscriptbox(self, **options): _options.update(options) options = _options options["inside_row"] = True - return "\n%s\n%s\n%s\n" % ( + + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + options["_indent_level"] = indent_level + 1 + + return f"{indent_spaces}\n%s\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), @@ -354,12 +388,15 @@ def is_list_interior(content): else: options["inside_row"] = True + indent_level = options.get("_indent_level", 0) + indent_spaces = " "*indent_level + options["_indent_level"] = indent_level + 1 + for element in self.items: result.append(lookup_conversion_method(element, "mathml")(element, **options)) - # print(f"mrow: {result}") - - return "\n%s\n" % "\n".join(result) + # print(f"mrow: {result}") + return f"{indent_spaces}\n{'\n'.join(result)}\n{indent_spaces}" add_conversion_fn(RowBox, rowbox) @@ -394,6 +431,10 @@ def graphicsbox(self, elements=None, **options) -> str: int(self.height), base64.b64encode(svg_body.encode("utf8")).decode("utf8"), ) + indent_level = options.get("_indent_level",0) + if indent_level: + mathml = " " * indent_level + mathml + # print("boxes_to_mathml", mathml) return mathml @@ -406,6 +447,9 @@ def graphics3dbox(self, elements=None, **options) -> str: json_repr = self.boxes_to_json(elements, **options) mathml = f'' mathml = f"\n\n\n{mathml}\n\n\n" + indent_level = options.get("_indent_level",0) + if indent_level: + mathml = " " * indent_level + mathml return mathml From d26de0d7f2e499150e7b5840962bf75159331f40 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 30 Jan 2026 09:59:51 -0300 Subject: [PATCH 06/31] mathml indent --- mathics/format/render/mathml.py | 49 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index fa20b3a90..7465465d0 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -102,15 +102,13 @@ def string(self, **options) -> str: if number_as_text is None: number_as_text = SymbolFalse - indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level - + indent_spaces = " " * indent_level + def render(format, string): encoded_text = encode_mathml(string) return indent_spaces + format % encoded_text - if text.startswith('"') and text.endswith('"'): text = text[1:-1] if not show_string_characters: @@ -179,9 +177,9 @@ def interpretation_box(self, **options): def pane_box(self, **options): indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level - options["_indent_level"] = indent_level + 1 - + indent_spaces = " " * indent_level + options["_indent_level"] = indent_level + 1 + content = lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) options = self.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() @@ -224,7 +222,7 @@ def fractionbox(self, **options) -> str: _options.update(options) options = _options indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level + indent_spaces = " " * indent_level options["_indent_level"] = indent_level + 1 return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.num, "mathml")(self.num, **options), @@ -258,12 +256,12 @@ def boxes_to_mathml(box, **options): raise BoxConstructError joined_attrs = " ".join(f'{name}="{value}"' for name, value in attrs.items()) indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level + indent_spaces = " " * indent_level result = f"{indent_spaces}\n" new_box_options = box_options.copy() new_box_options["inside_list"] = True new_box_options["_indent_level"] = indent_level + 3 - + for row in items: result += "{indent_spaces} \n" if isinstance(row, tuple): @@ -285,17 +283,18 @@ def sqrtbox(self, **options): _options.update(options) options = _options indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level + indent_spaces = " " * indent_level options["_indent_level"] = indent_level + 1 - + if self.index: return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.radicand, "mathml")(self.radicand, **options), lookup_conversion_method(self.index, "mathml")(self.index, **options), ) - return f"{indent_spaces}\n%s\n{indent_spaces}" % lookup_conversion_method(self.radicand, "mathml")( - self.radicand, **options + return ( + f"{indent_spaces}\n%s\n{indent_spaces}" + % lookup_conversion_method(self.radicand, "mathml")(self.radicand, **options) ) @@ -307,8 +306,8 @@ def subscriptbox(self, **options): _options.update(options) options = _options indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level - options["_indent_level"] = indent_level + 1 + indent_spaces = " " * indent_level + options["_indent_level"] = indent_level + 1 return "{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), @@ -323,9 +322,9 @@ def superscriptbox(self, **options): _options.update(options) options = _options indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level + indent_spaces = " " * indent_level options["_indent_level"] = indent_level + 1 - + return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), @@ -342,9 +341,9 @@ def subsuperscriptbox(self, **options): options["inside_row"] = True indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level + indent_spaces = " " * indent_level options["_indent_level"] = indent_level + 1 - + return f"{indent_spaces}\n%s\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), @@ -389,13 +388,13 @@ def is_list_interior(content): options["inside_row"] = True indent_level = options.get("_indent_level", 0) - indent_spaces = " "*indent_level + indent_spaces = " " * indent_level options["_indent_level"] = indent_level + 1 for element in self.items: result.append(lookup_conversion_method(element, "mathml")(element, **options)) - # print(f"mrow: {result}") + # print(f"mrow: {result}") return f"{indent_spaces}\n{'\n'.join(result)}\n{indent_spaces}" @@ -431,10 +430,10 @@ def graphicsbox(self, elements=None, **options) -> str: int(self.height), base64.b64encode(svg_body.encode("utf8")).decode("utf8"), ) - indent_level = options.get("_indent_level",0) + indent_level = options.get("_indent_level", 0) if indent_level: mathml = " " * indent_level + mathml - + # print("boxes_to_mathml", mathml) return mathml @@ -447,7 +446,7 @@ def graphics3dbox(self, elements=None, **options) -> str: json_repr = self.boxes_to_json(elements, **options) mathml = f'' mathml = f"\n\n\n{mathml}\n\n\n" - indent_level = options.get("_indent_level",0) + indent_level = options.get("_indent_level", 0) if indent_level: mathml = " " * indent_level + mathml return mathml From cbceb63c8eb40d34e3cab236c938c71b965755c2 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 30 Jan 2026 13:56:31 -0300 Subject: [PATCH 07/31] update docs --- mathics/format/render/mathml.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 7465465d0..787d5086b 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -6,26 +6,25 @@ For readability, and following WMA MathML generated code, tags \ containing sub-tags are split on several lines, one by -sub element. For example, the Box expression +sub element, and indented according to the level of the part. \ +For example, the Box expression >> FractionBox[RowBox[{"a", "+", SuperscriptBox["b", "c"]}], "d"] produces ``` - -a -+ - -b -c - - -d + + a + + + + b + c + + + d ``` -In WMA, each line would be also indented adding one space on each \ -level of indentation. """ @@ -91,7 +90,6 @@ def encode_mathml(text: str) -> str: def string(self, **options) -> str: - text = self.value number_as_text = options.get("number_as_text", None) @@ -175,7 +173,6 @@ def interpretation_box(self, **options): def pane_box(self, **options): - indent_level = options.get("_indent_level", 0) indent_spaces = " " * indent_level options["_indent_level"] = indent_level + 1 @@ -210,8 +207,8 @@ def pane_box(self, **options): dims += "overflow:hidden;" dims = f' style="{dims}" ' if dims: - return f"{indent_level}\n{content}\n{indent_level}" - return f"{indent_level}{content}" + return f"{indent_spaces}\n{content}\n{indent_spaces}" + return f"{indent_spaces}{content}" add_conversion_fn(PaneBox, pane_box) @@ -308,7 +305,7 @@ def subscriptbox(self, **options): indent_level = options.get("_indent_level", 0) indent_spaces = " " * indent_level options["_indent_level"] = indent_level + 1 - return "{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( + return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), ) @@ -395,7 +392,7 @@ def is_list_interior(content): result.append(lookup_conversion_method(element, "mathml")(element, **options)) # print(f"mrow: {result}") - return f"{indent_spaces}\n{'\n'.join(result)}\n{indent_spaces}" + return f"{indent_spaces}\n%s\n{indent_spaces}" % ("\n".join(result),) add_conversion_fn(RowBox, rowbox) From e556ec9c411e2e7f0c52b5fa4d54e1de156affc3 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 30 Jan 2026 15:46:19 -0300 Subject: [PATCH 08/31] update tests. adjust more indentation --- Makefile | 2 +- mathics/format/box/outputforms.py | 14 +++-- mathics/format/render/mathml.py | 9 +-- test/format/format_tests.yaml | 98 ++++++++++++------------------- test/test_session.py | 2 +- 5 files changed, 56 insertions(+), 69 deletions(-) diff --git a/Makefile b/Makefile index ffad84fd6..df51eae7c 100644 --- a/Makefile +++ b/Makefile @@ -137,7 +137,7 @@ plot-detailed-tests: #: Run pytest tests. Use environment variable "PYTEST_OPTIONS" for pytest options pytest: - MATHICS_CHARACTER_ENCODING="ASCII" $(PYTHON) -m pytest $(PYTEST_OPTIONS) $(PYTEST_WORKERS) test + MATHICS_CHARACTER_ENCODING="ASCII" $(PYTHON) -m pytest $(PYTEST_OPTIONS) $(PYTEST_WORKERS) #: Run pytest tests stopping at first failure. pytest-x : diff --git a/mathics/format/box/outputforms.py b/mathics/format/box/outputforms.py index 415adba6e..a248d127f 100644 --- a/mathics/format/box/outputforms.py +++ b/mathics/format/box/outputforms.py @@ -30,7 +30,7 @@ def eval_mathmlform(expr: BaseElement, evaluation: Evaluation) -> BoxElementMixi boxes = format_element(expr, evaluation, SymbolTraditionalForm) try: - mathml = boxes.boxes_to_mathml(evaluation=evaluation,_indent_level=1) + mathml = boxes.boxes_to_mathml(evaluation=evaluation, _indent_level=1) except BoxError: evaluation.message( "General", @@ -44,9 +44,15 @@ def eval_mathmlform(expr: BaseElement, evaluation: Evaluation) -> BoxElementMixi # #convert_box(boxes) query = evaluation.parse("Settings`$UseSansSerif") usesansserif = query.evaluate(evaluation).to_python() - if not is_a_picture: - if isinstance(usesansserif, bool) and usesansserif: - mathml = '%s' % mathml + if is_a_picture: + usesansserif = False + elif not isinstance(usesansserif, bool): + usesansserif = False + + if usesansserif: + mathml = '\n%s\n' % mathml + else: + mathml = "\n%s\n" % mathml mathml = '%s' % mathml # convert_box(boxes) return InterpretationBox( diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 787d5086b..c3dc3f6a2 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -260,14 +260,15 @@ def boxes_to_mathml(box, **options): new_box_options["_indent_level"] = indent_level + 3 for row in items: - result += "{indent_spaces} \n" + result += f"{indent_spaces} " if isinstance(row, tuple): for item in row: - result += f"\n{indent_spaces} {boxes_to_mathml(item, **new_box_options)}" + new_box_options["_indent_level"] = indent_level + 4 + result += f"\n{indent_spaces} \n{boxes_to_mathml(item, **new_box_options)}\n{indent_spaces} " else: result += f"\n{indent_spaces} \n{boxes_to_mathml(row, **new_box_options)}\n{indent_spaces} " - result += "{indent_spaces} \n" - result += "{indent_spaces}" + result += f"\n{indent_spaces} \n" + result += f"{indent_spaces}" # print(f"gridbox: {result}") return result diff --git a/test/format/format_tests.yaml b/test/format/format_tests.yaml index 6254bbd02..b4bcd921d 100644 --- a/test/format/format_tests.yaml +++ b/test/format/format_tests.yaml @@ -139,8 +139,8 @@ mathml: System`InputForm: -4 System`OutputForm: -4 - System`StandardForm: "\n-\n4\n" - System`TraditionalForm: "\n-\n4\n" + System`StandardForm: "\n -\n 4\n" + System`TraditionalForm: "\n -\n 4\n" text: System`InputForm: '-4' System`OutputForm: '-4' @@ -158,8 +158,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.32 - System`StandardForm: "\n-\n4.32\n" - System`TraditionalForm: "\n-\n4.32\n" + System`StandardForm: "\n -\n 4.32\n" + System`TraditionalForm: "\n -\n 4.32\n" text: System`InputForm: '-4.32' System`OutputForm: '-4.32' @@ -177,8 +177,8 @@ mathml: System`InputForm: -4.33 System`OutputForm: -4.3 - System`StandardForm: "\n-\n4.33\n" - System`TraditionalForm: "\n-\n4.33\n" + System`StandardForm: "\n -\n 4.33\n" + System`TraditionalForm: "\n -\n 4.33\n" text: System`InputForm: -4.33`2. System`OutputForm: '-4.3' @@ -196,8 +196,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.320 - System`StandardForm: "\n-\n4.32\n" - System`TraditionalForm: "\n-\n4.32\n" + System`StandardForm: "\n -\n 4.32\n" + System`TraditionalForm: "\n -\n 4.32\n" text: System`InputForm: -4.32`4. System`OutputForm: '-4.320' @@ -271,10 +271,12 @@ System`OutputForm: - '1 / (1 + 1 / (1 + 1 / a))' - Fragile! - System`StandardForm: &id001 - - "\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n" + System`StandardForm: + - "\n 1\n \n 1\n +\n \n 1\n \n 1\n +\n \n 1\n a\n \n \n \n \n" + - Fragile! + System`TraditionalForm: + - "\n 1\n \n 1\n +\n \n 1\n \n 1\n +\n \n 1\n a\n \n \n \n \n" - Fragile! - System`TraditionalForm: *id001 text: System`InputForm: 1/(1 + 1/(1 + 1/a)) System`OutputForm: 1 / (1 + 1 / (1 + 1 / a)) @@ -292,8 +294,8 @@ mathml: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t|>|>' - System`StandardForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\nd\n->\nt\n\n|>\n\n\n\n|>\n" - System`TraditionalForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\nd\n->\nt\n\n|>\n\n\n\n|>\n" + System`StandardForm: "\n <|\n \n \n a\n ->\n x\n \n ,\n \n b\n ->\n y\n \n ,\n \n c\n ->\n \n <|\n \n d\n ->\n t\n \n |>\n \n \n \n |>\n" + System`TraditionalForm: "\n <|\n \n \n a\n ->\n x\n \n ,\n \n b\n ->\n y\n \n ,\n \n c\n ->\n \n <|\n \n d\n ->\n t\n \n |>\n \n \n \n |>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t|>|> @@ -311,8 +313,8 @@ Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: mathml: System`InputForm: "<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>" System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>' - System`StandardForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\n\nd\n->\nt\n\n,\n\ne\n->\nu\n\n\n|>\n\n\n\n|>\n" - System`TraditionalForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\n\nd\n->\nt\n\n,\n\ne\n->\nu\n\n\n|>\n\n\n\n|>\n" + System`StandardForm: "\n <|\n \n \n a\n ->\n x\n \n ,\n \n b\n ->\n y\n \n ,\n \n c\n ->\n \n <|\n \n \n d\n ->\n t\n \n ,\n \n e\n ->\n u\n \n \n |>\n \n \n \n |>\n" + System`TraditionalForm: "\n <|\n \n \n a\n ->\n x\n \n ,\n \n b\n ->\n y\n \n ,\n \n c\n ->\n \n <|\n \n \n d\n ->\n t\n \n ,\n \n e\n ->\n u\n \n \n |>\n \n \n \n |>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> @@ -330,8 +332,8 @@ Complex[1.09*^12, 3.]: mathml: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: '1.09×10^12 + 3. I' - System`StandardForm: "\n\n1.09\n*^\n12\n\n+\n\n3.\n \nI\n\n" - System`TraditionalForm: "\n\n1.09\n×\n\n10\n12\n\n\n+\n\n3.\n\nI\n\n" + System`StandardForm: "\n \n 1.09\n *^\n 12\n \n +\n \n 3.\n  \n I\n \n" + System`TraditionalForm: "\n \n 1.09\n ×\n \n 10\n 12\n \n \n +\n \n 3.\n \n I\n \n" text: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: "1.09\xD710^12 + 3. I" @@ -458,18 +460,8 @@ Graphics[{}]: mathml: System`InputForm: "Grid[{{"Spanish", "Hola!"}, {"Portuguese", "Olà!"}, {"English", "Hi!"}}]" System`OutputForm: 'Spanish      Hola!Portuguese   Olà!English      Hi!' - System`StandardForm: "\nSpanishHola!\n\ - PortugueseOl\xE0!\nEnglishHi!\n\ - " - System`TraditionalForm: "\nSpanishHola!\n\ - PortugueseOl\xE0!\nEnglishHi!\n\ - " + System`StandardForm: "\n \n \n Spanish\n \n \n Hola!\n \n \n \n \n Portuguese\n \n \n Olà!\n \n \n \n \n English\n \n \n Hi!\n \n \n" + System`TraditionalForm: "\n \n \n Spanish\n \n \n Hola!\n \n \n \n \n Portuguese\n \n \n Olà!\n \n \n \n \n English\n \n \n Hi!\n \n \n" text: System`InputForm: "Grid[{{\"Spanish\", \"Hola!\"}, {\"Portuguese\", \"Ol\xE0!\"\ }, {\"English\", \"Hi!\"}}]" @@ -497,20 +489,8 @@ Grid[{{a,b},{c,d}}]: mathml: System`InputForm: Grid[{{a, b}, {c, d}}] System`OutputForm: 'a   bc   d' - System`StandardForm: ' - - ab - - cd - - ' - System`TraditionalForm: ' - - ab - - cd - - ' + System`StandardForm: "\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n" + System`TraditionalForm: "\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n" text: System`InputForm: Grid[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -543,8 +523,8 @@ Integrate[F[x], {x, a, g[b]}]: mathml: System`InputForm: 'Integrate[F[x], {x, a, g[b]}]' System`OutputForm: 'Integrate[F[x], {x, a, g[b]}]' - System`StandardForm: "\n\n\na\n\ng\n[\nb\n]\n\n\n\n\nF\n[\nx\n]\n\n\n\n𝑑\nx\n\n" - System`TraditionalForm: "\n\n\na\n\ng\n(\nb\n)\n\n\n\n\nF\n(\nx\n)\n\n\n\n𝑑\nx\n\n" + System`StandardForm: "\n \n \n a\n \n g\n [\n b\n ]\n \n \n \u2062\n \n F\n [\n x\n ]\n \n \u2062\n \n 𝑑\n x\n \n" + System`TraditionalForm: "\n \n \n a\n \n g\n (\n b\n )\n \n \n \u2062\n \n F\n (\n x\n )\n \n \u2062\n \n 𝑑\n x\n \n" text: System`InputForm: Integrate[F[x], {x, a, g[b]}] System`OutputForm: Integrate[F[x], {x, a, g[b]}] @@ -568,8 +548,8 @@ MatrixForm[{{a,b},{c,d}}]: mathml: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a   bc   d' - System`StandardForm: "\n(\n\nab\ncd\n\n)\n" - System`TraditionalForm: "\n(\n\nab\ncd\n\n)\n" + System`StandardForm: "\n (\n\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n\n )\n" + System`TraditionalForm: "\n (\n\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n\n )\n" text: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -607,10 +587,10 @@ Sqrt[1/(1+1/(1+1/a))]: - 'Sqrt[1 / (1 + 1 / (1 + 1 / a))]' - Fragile! System`StandardForm: - - "\n\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n\n" + - "\n \n 1\n \n 1\n +\n \n 1\n \n 1\n +\n \n 1\n a\n \n \n \n \n \n" - Fragile! System`TraditionalForm: - - "\n\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n\n" + - "\n \n 1\n \n 1\n +\n \n 1\n \n 1\n +\n \n 1\n a\n \n \n \n \n \n" - Fragile! text: System`InputForm: Sqrt[1/(1 + 1/(1 + 1/a))] @@ -633,8 +613,8 @@ Subscript[a, 4]: System`OutputForm: - 'Subscript[a, 4]' - Fragile! - System`StandardForm: "\na\n4\n" - System`TraditionalForm: "\na\n4\n" + System`StandardForm: "\n a\n 4\n" + System`TraditionalForm: "\n a\n 4\n" text: System`InputForm: Subscript[a, 4] System`OutputForm: Subscript[a, 4] @@ -654,8 +634,8 @@ Subsuperscript[a, p, q]: mathml: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: 'Subsuperscript[a, p, q]' - System`StandardForm: "\na\np\nq\n" - System`TraditionalForm: "\na\np\nq\n" + System`StandardForm: "\n a\n p\n q\n" + System`TraditionalForm: "\n a\n p\n q\n" text: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: Subsuperscript[a, p, q] @@ -716,8 +696,8 @@ TableForm[{{a,b},{c,d}}]: mathml: System`InputForm: TableForm[{{a, b}, {c, d}}] System`OutputForm: 'a   bc   d' - System`StandardForm: "\nab\ncd\n" - System`TraditionalForm: "\nab\ncd\n" + System`StandardForm: "\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n" + System`TraditionalForm: "\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n" text: System`InputForm: TableForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -807,7 +787,7 @@ a^(g[b]/c): mathml: System`InputForm: a^(g[b]/c) System`OutputForm: 'a ^ (g[b] / c)' - System`TraditionalForm: "\na\n\n\ng\n(\nb\n)\n\nc\n\n" + System`TraditionalForm: "\n a\n \n \n g\n (\n b\n )\n \n c\n \n" text: System`InputForm: a^(g[b]/c) System`OutputForm: a ^ (g[b] / c) @@ -825,8 +805,8 @@ a^4: mathml: System`InputForm: 'a^4' System`OutputForm: 'a ^ 4' - System`StandardForm: "\na\n4\n" - System`TraditionalForm: "\na\n4\n" + System`StandardForm: "\n a\n 4\n" + System`TraditionalForm: "\n a\n 4\n" text: System`InputForm: a^4 System`OutputForm: a ^ 4 @@ -870,7 +850,7 @@ a+PrecedenceForm[b+c,10]: System`StandardForm: 'a+\left(b+c\right)' mathml: System`OutputForm: 'a + (b + c)' - System`StandardForm: "\na\n+\n\n(\n\nb\n+\nc\n\n)\n\n" + System`StandardForm: "\n a\n +\n \n (\n \n b\n +\n c\n \n )\n \n" text: System`InputForm: 'a + (PrecedenceForm[b + c, 10])' System`OutputForm: 'a + (b + c)' diff --git a/test/test_session.py b/test/test_session.py index 4f04fb0df..08d779a1f 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -55,7 +55,7 @@ def test_session_format_evaluation(): assert session.format_result(form="text") == "a / b" assert session.format_result(form="latex") == "\\frac{a}{b}" assert session.format_result(form="xml") == ( - '\na\nb\n' + '\n \n a\n b\n \n' ) From 2951a175963168feadb61a977af190dbe78c9deb Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 30 Jan 2026 18:02:55 -0300 Subject: [PATCH 09/31] restore Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index df51eae7c..ffad84fd6 100644 --- a/Makefile +++ b/Makefile @@ -137,7 +137,7 @@ plot-detailed-tests: #: Run pytest tests. Use environment variable "PYTEST_OPTIONS" for pytest options pytest: - MATHICS_CHARACTER_ENCODING="ASCII" $(PYTHON) -m pytest $(PYTEST_OPTIONS) $(PYTEST_WORKERS) + MATHICS_CHARACTER_ENCODING="ASCII" $(PYTHON) -m pytest $(PYTEST_OPTIONS) $(PYTEST_WORKERS) test #: Run pytest tests stopping at first failure. pytest-x : From a5e31049d44903d2b0aa000b16aae1f818641958 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 2 Feb 2026 10:26:34 -0300 Subject: [PATCH 10/31] improve Expression.sameQ to deal with BoxExpressions. Add tests for BoxExpressions. --- mathics/core/expression.py | 7 ++++- test/format/test_boxexpressions.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/format/test_boxexpressions.py diff --git a/mathics/core/expression.py b/mathics/core/expression.py index b527c636c..35a34c978 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -1434,7 +1434,12 @@ def round_to_float( def sameQ(self, other: BaseElement) -> bool: """Mathics3 SameQ""" if not isinstance(other, Expression): - return False + # TODO: consider the alternative + # if not hasattr(other, "to_expression"): + # return False + # other = other.to_expression() + # + return other.sameQ(self) if self is other: return True diff --git a/test/format/test_boxexpressions.py b/test/format/test_boxexpressions.py new file mode 100644 index 000000000..9f7f134da --- /dev/null +++ b/test/format/test_boxexpressions.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +from test.helper import check_evaluation, session + +import pytest + +from mathics.core.element import BoxElementMixin +from mathics.core.expression import Expression + + +@pytest.mark.parametrize( + ("expr_str",), + [ + # Notice that in the evaluation, Box Expression objects + # removes options with the corresponding default values, and + # options are sorted in lexicographical order. + # In the examples, also use only expressions in their fully evaluated + # forms. For example, do not use `Color->Red` but + # `Color->RGBColor[1, 0, 0]`. + ('StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True]',), + ( + 'InterpretationBox[StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True], AutoDelete->True, Editable->True]', + ), + ( + 'RowBox[{"Say hi!:", StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True]}]', + ), + ], +) +def test_boxexpressions(expr_str): + expr = session.parse(expr_str) + boxes = expr.evaluate(session.evaluation) + assert isinstance( + boxes, BoxElementMixin + ), f"boxes of type {type(boxes)} should be a Box expression or string." + + expr_from_boxes = boxes.to_expression() + assert isinstance( + expr_from_boxes, Expression + ), f"{type(boxes)}.to_expression() should be Expression." + assert expr_from_boxes.sameQ( + expr + ), f"The initial and the reconstructed expressions must be the same:\n{expr}\n{expr_from_boxes}\n" + assert boxes.sameQ( + expr_from_boxes + ), "Boxes should be equivalent to the reconstructed expression.\n{expr_from_boxes}\n{boxes}\n" + # This seems to be an issue with the sameQ method of `Expression`. + assert expr_from_boxes.sameQ( + boxes + ), f"Boxes should be equivalent to the reconstructed expression.\n{expr_from_boxes}\n{boxes}\n" From 270f82d49a4063c49cf57cd72e83ac114b48fb59 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 2 Feb 2026 10:29:01 -0300 Subject: [PATCH 11/31] add a test involving FractionBox --- test/format/test_boxexpressions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/format/test_boxexpressions.py b/test/format/test_boxexpressions.py index 9f7f134da..53f4ff6a8 100644 --- a/test/format/test_boxexpressions.py +++ b/test/format/test_boxexpressions.py @@ -17,6 +17,9 @@ # In the examples, also use only expressions in their fully evaluated # forms. For example, do not use `Color->Red` but # `Color->RGBColor[1, 0, 0]`. + ( + 'StyleBox[FractionBox[StyleBox["3", Color->RGBColor[1, 0, 0], ShowStringCharacters->True], "2"], Alignment->Left]', + ), ('StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True]',), ( 'InterpretationBox[StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True], AutoDelete->True, Editable->True]', From 70f84ed15a7aa2d6b18613208f4468d161becfdd Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Wed, 28 Jan 2026 22:16:23 -0300 Subject: [PATCH 12/31] Improve processing of MathMLForm for Input and Output Form --- mathics/format/render/mathml.py | 75 +++++++++++------- test/format/format_tests-WMA.yaml | 111 ++++++++++++++------------ test/format/format_tests.yaml | 126 +++++++++++------------------- test/test_session.py | 3 +- 4 files changed, 155 insertions(+), 160 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 683471d0d..7c484e016 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -44,6 +44,7 @@ def encode_mathml(text: str) -> str: return text +# "Operators" which are not in display_operators_set extra_operators = { ",", "(", @@ -52,20 +53,21 @@ def encode_mathml(text: str) -> str: "]", "{", "}", - "\u301a", - "\u301b", - "\u00d7", - "\u2032", - "\u2032\u2032", - " ", - "\u2062", - "\u222b", - "\u2146", + "\u301a", # [[ + "\u301b", # ]] + "\u00d7", # \[Times] + "\u2032", # \[RawComma] + "\u2032\u2032", # \[RawComma]\[RawComma] + " ", # \[RawSpace] + "\u2062", # \[InvisibleTimes] + "\u222b", # \[Integral] + "\u2146", # \[DifferentialD] } def string(self, **options) -> str: text = self.value + print("mathml string", text, "options:", options) number_as_text = options.get("number_as_text", None) show_string_characters = ( @@ -80,13 +82,10 @@ def render(format, string): return format % encoded_text if text.startswith('"') and text.endswith('"'): - if show_string_characters: - return render("%s", text[1:-1]) - else: - outtext = "" - for line in text[1:-1].split("\n"): - outtext += render("%s", line) - return outtext + text = text[1:-1] + if not show_string_characters: + return render("%s", text) + return render("%s", text) elif ( text and (number_as_text is SymbolFalse) @@ -101,11 +100,11 @@ def render(format, string): # Mathics-Django: if text == "": return "" - if text == "\u2146": + if text == "\u2146": # DifferentialD return render( '%s', text ) - if text == "\u2062": + if text == "\u2062": # InvisibleTimes return render( '%s', text ) @@ -113,17 +112,35 @@ def render(format, string): elif is_symbol_name(text): return render("%s", text) else: - outtext = "" - for line in text.split("\n"): - outtext += render("%s", line) - return outtext + return "".join( + render("%s", line) for line in text.split("\n") + ) add_conversion_fn(String, string) def interpretation_box(self, **options): - return lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) + boxes = self.boxes + origin = self.expr + if origin.has_form("InputForm", None): + # InputForm produce outputs of the form + # InterpretationBox[Style[_String, ...], origin_InputForm, opts___] + assert isinstance(boxes, StyleBox), f"boxes={boxes} are not a StyleBox" + boxes = boxes.boxes + options["System`ShowStringCharacters"] = SymbolTrue + assert isinstance(boxes, String) + # Remove the outer quotes + elif origin.has_form("OutputForm", None): + # OutputForm produce outputs of the form + # InterpretationBox[PaneBox[_String, ...], origin_OutputForm, opts___] + assert boxes.has_form("PaneBox", 1, None) + boxes = boxes.boxes + assert isinstance(boxes, String) + # Remove the outer quotes + boxes = String(boxes.value) + + return lookup_conversion_method(boxes, "mathml")(boxes, **options) add_conversion_fn(InterpretationBox, interpretation_box) @@ -171,7 +188,7 @@ def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) options = _options - return "%s %s" % ( + return "\n%s\n %s\n" % ( lookup_conversion_method(self.num, "mathml")(self.num, **options), lookup_conversion_method(self.den, "mathml")(self.den, **options), ) @@ -231,7 +248,7 @@ def sqrtbox(self, **options): lookup_conversion_method(self.index, "mathml")(self.index, **options), ) - return " %s " % lookup_conversion_method(self.radicand, "mathml")( + return "\n %s\n" % lookup_conversion_method(self.radicand, "mathml")( self.radicand, **options ) @@ -243,7 +260,7 @@ def subscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "%s %s" % ( + return "\n%s\n %s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), ) @@ -256,7 +273,7 @@ def superscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "%s %s" % ( + return "\n%s \n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), ) @@ -270,7 +287,7 @@ def subsuperscriptbox(self, **options): _options.update(options) options = _options options["inside_row"] = True - return "%s %s %s" % ( + return "\n%s\n %s\n %s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), @@ -318,7 +335,7 @@ def is_list_interior(content): # print(f"mrow: {result}") - return "%s" % " ".join(result) + return "\n%s\n" % " ".join(result) add_conversion_fn(RowBox, rowbox) diff --git a/test/format/format_tests-WMA.yaml b/test/format/format_tests-WMA.yaml index 4a00ee4b1..975084146 100644 --- a/test/format/format_tests-WMA.yaml +++ b/test/format/format_tests-WMA.yaml @@ -23,7 +23,6 @@ # because we use both in documentation and in the web interface. # - '"-7.32"': msg: A String with a number latex: @@ -402,36 +401,15 @@ Graphics[{}]: Grid[{{a,b},{c,d}}]: msg: GridBox latex: - InputForm: \text{Grid[\{\{a, b\}, \{c, d\}\}]} + InputForm: '\text{Grid[$\{\{$a, b$\}$, $\{$c, d$\}\}$]}' OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} - StandardForm: \begin{array}{cc} a & b\\ c & d\end{array} - TraditionalForm: \begin{array}{cc} a & b\\ c & d\end{array} + StandardForm: "\\begin{array}{cc}\n a & b\\\\\n c & d\n\\end{array}" + TraditionalForm: "\\begin{array}{cc}\n a & b\\\\\n c & d\n\\end{array}" mathml: - InputForm: Grid [ { { - a b } - { c - d } } ] - OutputForm: ' - - ab - - cd - - ' - StandardForm: ' - - ab - - cd - - ' - TraditionalForm: ' - - ab - - cd - - ' + InputForm: 'Grid [ { { a b } { c d } } ]' + StandardForm: "\n \n \n a\n \n \n b\n \n \n \n \n c\n \n \n d\n \n \n " + TraditionalForm: "\n ab\n + cd\n " text: InputForm: Grid[{{a, b}, {c, d}}] OutputForm: 'a b @@ -556,12 +534,8 @@ Subscript[a, 4]: - Subscript [ a 4 ] - Fragile! - OutputForm: - - Subscript [ a - 4 ] - - Fragile! - StandardForm: a 4 - TraditionalForm: a 4 + StandardForm: "\n a\n 4\n " + TraditionalForm: "\n a\n 4\n " text: InputForm: Subscript[a, 4] OutputForm: "a\n 4" @@ -583,8 +557,8 @@ Subsuperscript[a, p, q]: p q ] OutputForm: Subsuperscript [ a p q ] - StandardForm: a p q - TraditionalForm: a p q + StandardForm: "\n a\n p\n q\n " + TraditionalForm: "\n a\n p\n q\n " text: InputForm: Subsuperscript[a, p, q] OutputForm: " q\na\n p" @@ -703,15 +677,13 @@ a^(g[b]/c): latex: InputForm: \text{a${}^{\wedge}$(g[b]/c)} OutputForm: a\text{ ${}^{\wedge}$ }\left(g(b)\text{ / }c\right) - StandardForm: a^{\frac{g(b)}{c}} + StandardForm: a^{\frac{g[b]}{c}} TraditionalForm: a^{\frac{g(b)}{c}} mathml: - InputForm: a ^ ( b -  /  c ) - OutputForm: a  ^  ( - b  /  c ) - StandardForm: a b c - TraditionalForm: a g ( b ) c + InputForm: 'a^(g[b]/c)' + OutputForm: '<><>' + StandardForm: "\n a\n \n \n g\n [\n b\n ]\n \n c\n \n " + TraditionalForm: "\n a\n \n \n g\n \n (\n b\n )\n \n c\n \n " text: InputForm: a^(g[b]/c) OutputForm: " g[b]/c\na" @@ -727,12 +699,55 @@ a^4: StandardForm: a^4 TraditionalForm: a^4 mathml: - InputForm: a ^ 4 - OutputForm: a  ^  4 - StandardForm: a 4 - TraditionalForm: a 4 + InputForm: a^4 + StandardForm: "\n a\n 4\n " + TraditionalForm: "\n a\n 4\n " text: InputForm: a^4 OutputForm: " 4\na" StandardForm: "\\!\\(\\*SuperscriptBox[\"a\", \"4\"]\\)" TraditionalForm: "\\!\\(\\*FormBox[SuperscriptBox[\"a\", \"4\"], TraditionalForm]\\)" + + +Optional[x__]: + msg: Optional with one argument + latex: + System`OutputForm: '\text{x$\_\_$.}' + System`StandardForm: '\text{x$\_\_$.}' + mathml: + System`OutputForm: 'x__.' + System`StandardForm: 'x__.' + text: + System`InputForm: 'x__.' + System`OutputForm: 'x__.' + System`StandardForm: '\!\(\*RowBox[{"Optional", "[", "x__", "]"}]\)' + System`TraditionalForm: '\!\(\*FormBox[RowBox[{"Optional", "[", "x__", "]"}], TraditionalForm]\)' + + +Optional[x__, a+b]: + msg: Optional with two arguments + latex: + System`OutputForm: '\text{x$\_\_$:a + b}' + System`StandardForm: '\text{x$\_\_$}:a+b' + mathml: + System`OutputForm: 'x__ : a + b' + text: + System`InputForm: 'x__:a + b' + System`OutputForm: 'x__:a + b' + System`StandardForm: '\!\(\*RowBox[{"x__", ":", RowBox[{"a", "+", "b"}]}]\)' + System`TraditionalForm: '\!\(\*FormBox[RowBox[{"x__", ":", RowBox[{"a", "+", "b"}]}], TraditionalForm]\)' + + +a+PrecedenceForm[b+c,10]: + msg: "PrecedenceForm" + latex: + System`OutputForm: '\text{a + (b + c)}' + System`StandardForm: 'a+(b+c)' + mathml: + System`OutputForm: 'a + (b + c)' + System`StandardForm: "\n a\n +\n \n (\n \n b\n +\n c\n \n )\n \n " + text: + System`InputForm: 'a + PrecedenceForm[b + c, 10]' + System`OutputForm: 'a + (b + c)' + System`StandardForm: '\!\(\*RowBox[{"a", "+", RowBox[{"(", TagBox[RowBox[{"b", "+", "c"}], Function[PrecedenceForm[Slot[1], 10]]], ")"}]}]\)' + System`TraditionalForm: '\!\(\*FormBox[RowBox[{"a", "+", RowBox[{"(", TagBox[RowBox[{"b", "+", "c"}], Function[PrecedenceForm[Slot[1], 10]]], ")"}]}], TraditionalForm]\)' diff --git a/test/format/format_tests.yaml b/test/format/format_tests.yaml index 14d511606..a98c35377 100644 --- a/test/format/format_tests.yaml +++ b/test/format/format_tests.yaml @@ -139,8 +139,8 @@ mathml: System`InputForm: -4 System`OutputForm: -4 - System`StandardForm: - 4 - System`TraditionalForm: - 4 + System`StandardForm: "\n- 4\n" + System`TraditionalForm: "\n- 4\n" text: System`InputForm: '-4' System`OutputForm: '-4' @@ -158,8 +158,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.32 - System`StandardForm: - 4.32 - System`TraditionalForm: - 4.32 + System`StandardForm: "\n- 4.32\n" + System`TraditionalForm: "\n- 4.32\n" text: System`InputForm: '-4.32' System`OutputForm: '-4.32' @@ -177,8 +177,8 @@ mathml: System`InputForm: -4.33 System`OutputForm: -4.3 - System`StandardForm: - 4.33 - System`TraditionalForm: - 4.33 + System`StandardForm: "\n- 4.33\n" + System`TraditionalForm: "\n- 4.33\n" text: System`InputForm: -4.33`2. System`OutputForm: '-4.3' @@ -196,8 +196,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.320 - System`StandardForm: - 4.32 - System`TraditionalForm: - 4.32 + System`StandardForm: "\n- 4.32\n" + System`TraditionalForm: "\n- 4.32\n" text: System`InputForm: -4.32`4. System`OutputForm: '-4.320' @@ -272,8 +272,7 @@ - '1 / (1 + 1 / (1 + 1 / a))' - Fragile! System`StandardForm: &id001 - - 1 1 + 1 1 - + 1 a + - "\n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n" - Fragile! System`TraditionalForm: *id001 text: @@ -293,14 +292,8 @@ mathml: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t|>|>' - System`StandardForm: <| a -> - x , b -> y - , c -> <| d - -> t |> |> - System`TraditionalForm: <| a -> - x , b -> y - , c -> <| d - -> t |> |> + System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" + System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t|>|> @@ -316,18 +309,10 @@ Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: System`StandardForm: '\langle\vert a->x, b->y, c->\langle\vert d->t, e->u\vert\rangle \vert\rangle' System`TraditionalForm: '\langle\vert a->x, b->y, c->\langle\vert d->t, e->u\vert\rangle \vert\rangle' mathml: - System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> + System`InputForm: "<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>" System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>' - System`StandardForm: <| a -> - x , b -> y - , c -> <| d - -> t , e -> - u |> |> - System`TraditionalForm: <| a -> - x , b -> y - , c -> <| d - -> t , e -> - u |> |> + System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" + System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> @@ -345,11 +330,8 @@ Complex[1.09*^12, 3.]: mathml: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: '1.09×10^12 + 3. I' - System`StandardForm: 1.09 *^ 12 - + 3.   I - System`TraditionalForm: "1.09 \xD7 10\ - \ 12 + 3. \u2062 I" + System`StandardForm: "\n\n1.09 *^ 12\n + \n3.   I\n\n" + System`TraditionalForm: "\n\n1.09 × \n10 \n12\n\n + \n3. I\n\n" text: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: "1.09\xD710^12 + 3. I" @@ -475,7 +457,7 @@ Graphics[{}]: \ \\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!' + System`OutputForm: 'Spanish      Hola!Portuguese   Olà!English      Hi!' System`StandardForm: "\nSpanishHola!\n\ PortugueseGrid[{{a, b}, {c, d}}] - System`OutputForm: 'a   bc   d' + System`OutputForm: 'a   bc   d' System`StandardForm: ' ab @@ -559,11 +541,15 @@ Integrate[F[x], {x, a, g[b]}]: System`StandardForm: \int_a^{g[b]} F[x] \, dx System`TraditionalForm: \int_a^{g(b)} F(x) \, dx mathml: - System`InputForm: Integrate[F[x], {x, a, g[b]}] + System`InputForm: 'Integrate[F[x], {x, a, g[b]}]' System`OutputForm: 'Integrate[F[x], {x, a, g[b]}]' + System`StandardForm: "\n\n\n a\n \ng [ b ]\n\n \nF [ x ]\n \n𝑑 x\n\n" + System`TraditionalForm: "\n\n\n a\n \ng ( b )\n\n \nF ( x )\n \n𝑑 x\n\n" text: + System`InputForm: Integrate[F[x], {x, a, g[b]}] System`OutputForm: Integrate[F[x], {x, a, g[b]}] - + System`StandardForm: 'Subsuperscript[∫, a, g[b]]⁢F[x]⁢𝑑x' + System`TraditionalForm: 'Subsuperscript[∫, a, g(b)]⁢F(x)⁢𝑑x' MatrixForm[{{a,b},{c,d}}]: msg: GridBox in a matrix @@ -581,21 +567,9 @@ MatrixForm[{{a,b},{c,d}}]: System`TraditionalForm: \left(\begin{array}{cc} a & b\\ c & d\end{array}\right) mathml: System`InputForm: MatrixForm[{{a, b}, {c, d}}] - System`OutputForm: 'a   bc   d' - System`StandardForm: '( - - ab - - cd - - )' - System`TraditionalForm: '( - - ab - - cd - - )' + System`OutputForm: 'a   bc   d' + System`StandardForm: "\n( \nab\ncd\n )\n" + System`TraditionalForm: "\n( \nab\ncd\n )\n" text: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -627,17 +601,17 @@ Sqrt[1/(1+1/(1+1/a))]: System`TraditionalForm: \sqrt{\frac{1}{1+\frac{1}{1+\frac{1}{a}}}} mathml: System`InputForm: - - Sqrt[1/(1 + 1/(1 + 1/a))] + - Sqrt[1/(1 + 1/(1 + 1/a))] - Fragile! System`OutputForm: - 'Sqrt[1 / (1 + 1 / (1 + 1 / a))]' - Fragile! - System`StandardForm: &id002 - - 1 1 + 1 1 - + 1 a - + System`StandardForm: + - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" - Fragile! - System`TraditionalForm: *id002 + System`TraditionalForm: + - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" + - Fragile! text: System`InputForm: Sqrt[1/(1 + 1/(1 + 1/a))] System`OutputForm: Sqrt[1 / (1 + 1 / (1 + 1 / a))] @@ -659,8 +633,8 @@ Subscript[a, 4]: System`OutputForm: - 'Subscript[a, 4]' - Fragile! - System`StandardForm: a 4 - System`TraditionalForm: a 4 + System`StandardForm: "\na\n 4\n" + System`TraditionalForm: "\na\n 4\n" text: System`InputForm: Subscript[a, 4] System`OutputForm: Subscript[a, 4] @@ -680,8 +654,8 @@ Subsuperscript[a, p, q]: mathml: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: 'Subsuperscript[a, p, q]' - System`StandardForm: a p q - System`TraditionalForm: a p q + System`StandardForm: "\na\n p\n q\n" + System`TraditionalForm: "\na\n p\n q\n" text: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: Subsuperscript[a, p, q] @@ -741,21 +715,9 @@ TableForm[{{a,b},{c,d}}]: System`TraditionalForm: \begin{array}{cc} a & b\\ c & d\end{array} mathml: System`InputForm: TableForm[{{a, b}, {c, d}}] - System`OutputForm: 'a   bc   d' - System`StandardForm: ' - - ab - - cd - - ' - System`TraditionalForm: ' - - ab - - cd - - ' + System`OutputForm: 'a   bc   d' + System`StandardForm: "\nab\ncd\n" + System`TraditionalForm: "\nab\ncd\n" text: System`InputForm: TableForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -845,7 +807,7 @@ a^(g[b]/c): mathml: System`InputForm: a^(g[b]/c) System`OutputForm: 'a ^ (g[b] / c)' - System`TraditionalForm: a g ( b ) c + System`TraditionalForm: "\na \n\n\ng ( b )\n\n c\n\n" text: System`InputForm: a^(g[b]/c) System`OutputForm: a ^ (g[b] / c) @@ -863,8 +825,8 @@ a^4: mathml: System`InputForm: 'a^4' System`OutputForm: 'a ^ 4' - System`StandardForm: a 4 - System`TraditionalForm: a 4 + System`StandardForm: "\na \n4\n" + System`TraditionalForm: "\na \n4\n" text: System`InputForm: a^4 System`OutputForm: a ^ 4 @@ -908,7 +870,7 @@ a+PrecedenceForm[b+c,10]: System`StandardForm: 'a+(b+c)' mathml: System`OutputForm: 'a + (b + c)' - System`StandardForm: 'a + ( b + c )' + System`StandardForm: "\na + \n( \nb + c\n )\n\n" text: System`InputForm: 'a + (PrecedenceForm[b + c, 10])' System`OutputForm: 'a + (b + c)' diff --git a/test/test_session.py b/test/test_session.py index 132d1617c..8716a6893 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -55,11 +55,12 @@ def test_session_format_evaluation(): assert session.format_result(form="text") == "a / b" assert session.format_result(form="latex") == "\\frac{a}{b}" assert session.format_result(form="xml") == ( - '' "a b" "" + '\n' "a\n b" "\n" ) def test_session_parse(): + session.reset() parsed = session.parse("a/b") expected = Expression( SymbolTimes, From 5b89c3f7ad36c9c2ace769fc27591b1b4ae4eb5f Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Thu, 29 Jan 2026 06:57:56 -0300 Subject: [PATCH 13/31] remove trailing print --- mathics/format/render/mathml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 7c484e016..edbdf478c 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -67,7 +67,6 @@ def encode_mathml(text: str) -> str: def string(self, **options) -> str: text = self.value - print("mathml string", text, "options:", options) number_as_text = options.get("number_as_text", None) show_string_characters = ( From e88108d8ba6db50f061b000ef56ccc43708e97a4 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Thu, 29 Jan 2026 10:58:34 -0300 Subject: [PATCH 14/31] remove space from extra_operators, and add a comment --- mathics/format/render/mathml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index edbdf478c..784666bd6 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -53,12 +53,12 @@ def encode_mathml(text: str) -> str: "]", "{", "}", + # TODO: check why the following characters are not in `operators`: "\u301a", # [[ "\u301b", # ]] "\u00d7", # \[Times] "\u2032", # \[RawComma] "\u2032\u2032", # \[RawComma]\[RawComma] - " ", # \[RawSpace] "\u2062", # \[InvisibleTimes] "\u222b", # \[Integral] "\u2146", # \[DifferentialD] From 5295f48e4d9fcae34eee74cdd90b05a75d7f87c7 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Thu, 29 Jan 2026 13:50:15 -0300 Subject: [PATCH 15/31] add a comment about mathml multiline output. Give a more consistent output --- mathics/format/render/mathml.py | 36 ++++++++++++++++---- test/format/format_tests.yaml | 58 ++++++++++++++++----------------- test/test_session.py | 2 +- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 784666bd6..c8bd347d1 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -3,6 +3,30 @@ Lower-level formatter of Mathics objects as MathML strings. MathML formatting is usually initiated in Mathics via MathMLForm[]. + +For readability, and following WMA MathML generated code, tags \ +containing sub-tags are split on several lines, one by +sub element. For example, the Box expression + +>> FractionBox[RowBox[{"a", "+", SuperscriptBox["b", "c"]}], "d"] + +produces +``` + + +a ++ + +b +c + + +d + +``` +In WMA, each line would be also indented adding one space on each \ +level of indentation. + """ import base64 @@ -187,7 +211,7 @@ def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s\n %s\n" % ( + return "\n%s\n%s\n" % ( lookup_conversion_method(self.num, "mathml")(self.num, **options), lookup_conversion_method(self.den, "mathml")(self.den, **options), ) @@ -247,7 +271,7 @@ def sqrtbox(self, **options): lookup_conversion_method(self.index, "mathml")(self.index, **options), ) - return "\n %s\n" % lookup_conversion_method(self.radicand, "mathml")( + return "\n%s\n" % lookup_conversion_method(self.radicand, "mathml")( self.radicand, **options ) @@ -259,7 +283,7 @@ def subscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s\n %s\n" % ( + return "\n%s\n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), ) @@ -272,7 +296,7 @@ def superscriptbox(self, **options): _options = self.box_options.copy() _options.update(options) options = _options - return "\n%s \n%s\n" % ( + return "\n%s\n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), ) @@ -286,7 +310,7 @@ def subsuperscriptbox(self, **options): _options.update(options) options = _options options["inside_row"] = True - return "\n%s\n %s\n %s\n" % ( + return "\n%s\n%s\n%s\n" % ( lookup_conversion_method(self.base, "mathml")(self.base, **options), lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), @@ -334,7 +358,7 @@ def is_list_interior(content): # print(f"mrow: {result}") - return "\n%s\n" % " ".join(result) + return "\n%s\n" % "\n".join(result) add_conversion_fn(RowBox, rowbox) diff --git a/test/format/format_tests.yaml b/test/format/format_tests.yaml index a98c35377..b15252cd9 100644 --- a/test/format/format_tests.yaml +++ b/test/format/format_tests.yaml @@ -139,8 +139,8 @@ mathml: System`InputForm: -4 System`OutputForm: -4 - System`StandardForm: "\n- 4\n" - System`TraditionalForm: "\n- 4\n" + System`StandardForm: "\n-\n4\n" + System`TraditionalForm: "\n-\n4\n" text: System`InputForm: '-4' System`OutputForm: '-4' @@ -158,8 +158,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.32 - System`StandardForm: "\n- 4.32\n" - System`TraditionalForm: "\n- 4.32\n" + System`StandardForm: "\n-\n4.32\n" + System`TraditionalForm: "\n-\n4.32\n" text: System`InputForm: '-4.32' System`OutputForm: '-4.32' @@ -177,8 +177,8 @@ mathml: System`InputForm: -4.33 System`OutputForm: -4.3 - System`StandardForm: "\n- 4.33\n" - System`TraditionalForm: "\n- 4.33\n" + System`StandardForm: "\n-\n4.33\n" + System`TraditionalForm: "\n-\n4.33\n" text: System`InputForm: -4.33`2. System`OutputForm: '-4.3' @@ -196,8 +196,8 @@ mathml: System`InputForm: -4.32 System`OutputForm: -4.320 - System`StandardForm: "\n- 4.32\n" - System`TraditionalForm: "\n- 4.32\n" + System`StandardForm: "\n-\n4.32\n" + System`TraditionalForm: "\n-\n4.32\n" text: System`InputForm: -4.32`4. System`OutputForm: '-4.320' @@ -272,7 +272,7 @@ - '1 / (1 + 1 / (1 + 1 / a))' - Fragile! System`StandardForm: &id001 - - "\n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n" + - "\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n" - Fragile! System`TraditionalForm: *id001 text: @@ -292,8 +292,8 @@ mathml: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t|>|>' - System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" - System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \nd -> t\n |>\n\n\n |>\n" + System`StandardForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\nd\n->\nt\n\n|>\n\n\n\n|>\n" + System`TraditionalForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\nd\n->\nt\n\n|>\n\n\n\n|>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t|>|> @@ -311,8 +311,8 @@ Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: mathml: System`InputForm: "<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>" System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>' - System`StandardForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" - System`TraditionalForm: "\n<| \n\na -> x\n , \nb -> y\n , \nc -> \n<| \n\nd -> t\n , \ne -> u\n\n |>\n\n\n |>\n" + System`StandardForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\n\nd\n->\nt\n\n,\n\ne\n->\nu\n\n\n|>\n\n\n\n|>\n" + System`TraditionalForm: "\n<|\n\n\na\n->\nx\n\n,\n\nb\n->\ny\n\n,\n\nc\n->\n\n<|\n\n\nd\n->\nt\n\n,\n\ne\n->\nu\n\n\n|>\n\n\n\n|>\n" text: System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> System`OutputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> @@ -330,8 +330,8 @@ Complex[1.09*^12, 3.]: mathml: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: '1.09×10^12 + 3. I' - System`StandardForm: "\n\n1.09 *^ 12\n + \n3.   I\n\n" - System`TraditionalForm: "\n\n1.09 × \n10 \n12\n\n + \n3. I\n\n" + System`StandardForm: "\n\n1.09\n*^\n12\n\n+\n\n3.\n \nI\n\n" + System`TraditionalForm: "\n\n1.09\n×\n\n10\n12\n\n\n+\n\n3.\n\nI\n\n" text: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: "1.09\xD710^12 + 3. I" @@ -543,8 +543,8 @@ Integrate[F[x], {x, a, g[b]}]: mathml: System`InputForm: 'Integrate[F[x], {x, a, g[b]}]' System`OutputForm: 'Integrate[F[x], {x, a, g[b]}]' - System`StandardForm: "\n\n\n a\n \ng [ b ]\n\n \nF [ x ]\n \n𝑑 x\n\n" - System`TraditionalForm: "\n\n\n a\n \ng ( b )\n\n \nF ( x )\n \n𝑑 x\n\n" + System`StandardForm: "\n\n\na\n\ng\n[\nb\n]\n\n\n\n\nF\n[\nx\n]\n\n\n\n𝑑\nx\n\n" + System`TraditionalForm: "\n\n\na\n\ng\n(\nb\n)\n\n\n\n\nF\n(\nx\n)\n\n\n\n𝑑\nx\n\n" text: System`InputForm: Integrate[F[x], {x, a, g[b]}] System`OutputForm: Integrate[F[x], {x, a, g[b]}] @@ -568,8 +568,8 @@ MatrixForm[{{a,b},{c,d}}]: mathml: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a   bc   d' - System`StandardForm: "\n( \nab\ncd\n )\n" - System`TraditionalForm: "\n( \nab\ncd\n )\n" + System`StandardForm: "\n(\n\nab\ncd\n\n)\n" + System`TraditionalForm: "\n(\n\nab\ncd\n\n)\n" text: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -607,10 +607,10 @@ Sqrt[1/(1+1/(1+1/a))]: - 'Sqrt[1 / (1 + 1 / (1 + 1 / a))]' - Fragile! System`StandardForm: - - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" + - "\n\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n\n" - Fragile! System`TraditionalForm: - - "\n \n1\n \n1 + \n1\n \n1 + \n1\n a\n\n\n\n\n\n" + - "\n\n1\n\n1\n+\n\n1\n\n1\n+\n\n1\na\n\n\n\n\n\n" - Fragile! text: System`InputForm: Sqrt[1/(1 + 1/(1 + 1/a))] @@ -633,8 +633,8 @@ Subscript[a, 4]: System`OutputForm: - 'Subscript[a, 4]' - Fragile! - System`StandardForm: "\na\n 4\n" - System`TraditionalForm: "\na\n 4\n" + System`StandardForm: "\na\n4\n" + System`TraditionalForm: "\na\n4\n" text: System`InputForm: Subscript[a, 4] System`OutputForm: Subscript[a, 4] @@ -654,8 +654,8 @@ Subsuperscript[a, p, q]: mathml: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: 'Subsuperscript[a, p, q]' - System`StandardForm: "\na\n p\n q\n" - System`TraditionalForm: "\na\n p\n q\n" + System`StandardForm: "\na\np\nq\n" + System`TraditionalForm: "\na\np\nq\n" text: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: Subsuperscript[a, p, q] @@ -807,7 +807,7 @@ a^(g[b]/c): mathml: System`InputForm: a^(g[b]/c) System`OutputForm: 'a ^ (g[b] / c)' - System`TraditionalForm: "\na \n\n\ng ( b )\n\n c\n\n" + System`TraditionalForm: "\na\n\n\ng\n(\nb\n)\n\nc\n\n" text: System`InputForm: a^(g[b]/c) System`OutputForm: a ^ (g[b] / c) @@ -825,8 +825,8 @@ a^4: mathml: System`InputForm: 'a^4' System`OutputForm: 'a ^ 4' - System`StandardForm: "\na \n4\n" - System`TraditionalForm: "\na \n4\n" + System`StandardForm: "\na\n4\n" + System`TraditionalForm: "\na\n4\n" text: System`InputForm: a^4 System`OutputForm: a ^ 4 @@ -870,7 +870,7 @@ a+PrecedenceForm[b+c,10]: System`StandardForm: 'a+(b+c)' mathml: System`OutputForm: 'a + (b + c)' - System`StandardForm: "\na + \n( \nb + c\n )\n\n" + System`StandardForm: "\na\n+\n\n(\n\nb\n+\nc\n\n)\n\n" text: System`InputForm: 'a + (PrecedenceForm[b + c, 10])' System`OutputForm: 'a + (b + c)' diff --git a/test/test_session.py b/test/test_session.py index 8716a6893..4f04fb0df 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -55,7 +55,7 @@ def test_session_format_evaluation(): assert session.format_result(form="text") == "a / b" assert session.format_result(form="latex") == "\\frac{a}{b}" assert session.format_result(form="xml") == ( - '\n' "a\n b" "\n" + '\na\nb\n' ) From 1831e6b6a5f7814e679461c0f20bea8d5d86e348 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Mon, 2 Feb 2026 10:26:34 -0300 Subject: [PATCH 16/31] improve Expression.sameQ to deal with BoxExpressions. Add tests for BoxExpressions. --- test/format/test_boxexpressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/format/test_boxexpressions.py b/test/format/test_boxexpressions.py index 53f4ff6a8..4f4597fd2 100644 --- a/test/format/test_boxexpressions.py +++ b/test/format/test_boxexpressions.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from test.helper import check_evaluation, session +from test.helper import session import pytest From 6c529bd30847a78be4df901a9be0a0ac92e895f5 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 3 Feb 2026 09:54:14 -0500 Subject: [PATCH 17/31] Add Box type annotations to render routines. Also: remove "_" from non-private classes. Go over render docstrings. --- mathics/builtin/box/graphics.py | 28 +- mathics/builtin/box/graphics3d.py | 12 +- mathics/builtin/box/uniform_polyhedra.py | 4 +- mathics/format/render/__init__.py | 35 ++- mathics/format/render/asy.py | 309 ++++++++++++----------- mathics/format/render/json.py | 130 +++++----- mathics/format/render/latex.py | 136 +++++----- mathics/format/render/mathml.py | 113 +++++---- mathics/format/render/svg.py | 232 ++++++++--------- mathics/format/render/text.py | 6 +- 10 files changed, 502 insertions(+), 503 deletions(-) diff --git a/mathics/builtin/box/graphics.py b/mathics/builtin/box/graphics.py index efd72feb3..c7ce1084a 100644 --- a/mathics/builtin/box/graphics.py +++ b/mathics/builtin/box/graphics.py @@ -43,7 +43,7 @@ SymbolRegularPolygonBox = Symbol("RegularPolygonBox") -class _GraphicsElementBox(BoxExpression, ABC): +class GraphicsElementBox(BoxExpression, ABC): def init(self, graphics, item=None, style={}, opacity=1.0): if item is not None and not item.has_form(self.get_name(), None): raise BoxExpressionError @@ -53,7 +53,7 @@ def init(self, graphics, item=None, style={}, opacity=1.0): self.is_completely_visible = False # True for axis elements -class _Polyline(_GraphicsElementBox): +class _Polyline(GraphicsElementBox): """ A structure containing a list of line segments stored in ``self.lines`` created from @@ -109,12 +109,12 @@ def extent(self) -> list: return result -# Note: has to come before _ArcBox -class _RoundBox(_GraphicsElementBox): +# Note: has to come before ArcBox +class RoundBox(GraphicsElementBox): face_element: Optional[bool] = None def init(self, graphics, style, item): - super(_RoundBox, self).init(graphics, item, style) + super().init(graphics, item, style) if len(item.elements) not in (1, 2): raise BoxExpressionError self.edge_color, self.face_color = style.get_style( @@ -137,7 +137,7 @@ def init(self, graphics, style, item): def extent(self) -> list: """ - Compute the bounding box for _RoundBox. Note that + Compute the bounding box for RoundBox. Note that We handle ellipses here too. """ line_width = self.style.get_line_width(face_element=self.face_element) / 2 @@ -150,7 +150,7 @@ def extent(self) -> list: return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)] -class _ArcBox(_RoundBox): +class ArcBox(RoundBox): def init(self, graphics, style, item): if len(item.elements) == 3: arc_expr = item.elements[2] @@ -175,7 +175,7 @@ def init(self, graphics, style, item): item = Expression(Symbol(item.get_head_name()), *item.elements[:2]) else: self.arc = None - super(_ArcBox, self).init(graphics, style, item) + super().init(graphics, style, item) def _arc_params(self): x, y = self.c.pos() @@ -214,7 +214,7 @@ def init(self, graphics, style, item=None): if not item: raise BoxExpressionError - super(ArrowBox, self).init(graphics, item, style) + super().init(graphics, item, style) elements = item.elements if len(elements) == 2: @@ -428,7 +428,7 @@ def init(self, graphics, style, item, options): self.spline_degree = spline_degree.get_int_value() -class CircleBox(_ArcBox): +class CircleBox(ArcBox): """
'CircleBox' @@ -440,7 +440,7 @@ class CircleBox(_ArcBox): summary_text = "is the symbol used in boxing 'Circle' expressions" -class DiskBox(_ArcBox): +class DiskBox(ArcBox): """
'DiskBox' @@ -500,7 +500,7 @@ def boxes_to_svg(self, elements=None, **options) -> str: return svg_body -class FilledCurveBox(_GraphicsElementBox): +class FilledCurveBox(GraphicsElementBox): """
'FilledCurveBox' @@ -580,7 +580,7 @@ def extent(self): return result -class InsetBox(_GraphicsElementBox): +class InsetBox(GraphicsElementBox): # We have no documentation for this (yet). no_doc = True @@ -791,7 +791,7 @@ def process_option(self, name, value): raise BoxExpressionError -class RectangleBox(_GraphicsElementBox): +class RectangleBox(GraphicsElementBox): # We have no documentation for this (yet). no_doc = True diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index 3350513c6..6f8b69e2a 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -7,10 +7,10 @@ from mathics.builtin.box.graphics import ( ArrowBox, GraphicsBox, + GraphicsElementBox, LineBox, PointBox, PolygonBox, - _GraphicsElementBox, ) from mathics.builtin.colors.color_directives import Opacity, RGBColor, _ColorObject from mathics.builtin.drawing.graphics3d import Graphics3D, Style3D @@ -75,7 +75,7 @@ def _apply_boxscaling(self, boxscale): coords.scale(boxscale) -class Cone3DBox(_GraphicsElementBox): +class Cone3DBox(GraphicsElementBox): # """ # Internal Python class used when Boxing a 'Cone' object. # """ @@ -119,7 +119,7 @@ def _apply_boxscaling(self, boxscale): pass -class Cuboid3DBox(_GraphicsElementBox): +class Cuboid3DBox(GraphicsElementBox): # """ # Internal Python class used when Boxing a 'Cuboid' object. # """ @@ -147,7 +147,7 @@ def _apply_boxscaling(self, boxscale): pass -class Cylinder3DBox(_GraphicsElementBox): +class Cylinder3DBox(GraphicsElementBox): # """ # Internal Python class used when Boxing a 'Cylinder' object. # """ @@ -271,7 +271,7 @@ def _apply_boxscaling(self, boxscale): coords.scale(boxscale) -class Sphere3DBox(_GraphicsElementBox): +class Sphere3DBox(GraphicsElementBox): # summary_text = "box representation for a sphere" # We have no documentation for this (yet). @@ -311,7 +311,7 @@ def _apply_boxscaling(self, boxscale): pass -class Tube3DBox(_GraphicsElementBox): +class Tube3DBox(GraphicsElementBox): # summary_text = "box representation for a tube" # We have no documentation for this (yet). diff --git a/mathics/builtin/box/uniform_polyhedra.py b/mathics/builtin/box/uniform_polyhedra.py index eeaf59cb6..2644ea6b8 100644 --- a/mathics/builtin/box/uniform_polyhedra.py +++ b/mathics/builtin/box/uniform_polyhedra.py @@ -1,6 +1,6 @@ import numbers -from mathics.builtin.box.graphics import _GraphicsElementBox +from mathics.builtin.box.graphics import GraphicsElementBox from mathics.builtin.colors.color_directives import Opacity, _ColorObject from mathics.builtin.drawing.graphics3d import Coords3D from mathics.builtin.drawing.graphics_internals import GLOBALS3D @@ -11,7 +11,7 @@ no_doc = True -class UniformPolyhedron3DBox(_GraphicsElementBox): +class UniformPolyhedron3DBox(GraphicsElementBox): # Let's overwrite the default summary_text here, # to recover the spaces. summary_text = "box representation of a 3d uniform polyhedron" diff --git a/mathics/format/render/__init__.py b/mathics/format/render/__init__.py index dc361bf7c..a66f28e18 100644 --- a/mathics/format/render/__init__.py +++ b/mathics/format/render/__init__.py @@ -1,27 +1,22 @@ -""" -Lower-level formatting routines. - -Built-in Lower-level formatting includes Asymptote, MathML, SVG, -threejs, and plain text. We hope and expect other formatting to other -kinds backend renderers like matplotlib, can be done by following the -pattern used here. - -These routines typically get called in formatting Mathics3 Box objects. +"""Rendering routines. -The higher level *Forms* (e.g. TeXForm, MathMLForm) typically cause -specific formatters to get called, (e.g. latex, mathml). However, the -two concepts and levels are a little bit different. A given From can -cause invoke of several formatters, which the front-end can influence -based on its capabilities and back-end renders available to it. +Mathics3 Built-in rendering includes renderers to Asymptote, MathML, +SVG, threejs, and plain text. We hope and expect other formatting to +other kinds backend renderers, like matplotlib, can be done by +following the pattern used here. -For example, in graphics there may be several different kinds of -renderers, SVG, or Asymptote for a particular kind of graphics Box. -The front-end needs to decides which format it better suited for it. -The Box, however, is created via a particular high-level Form. +Input to the renders come from some sort of Mathics3 Box. -As another example, front-end may decide to use MathJaX to render -TeXForm if the front-end supports this and the user so desires that. +The higher level Forms (e.g. TeXForm, MathMLForm) typically cause +specific boxing routines to get invoked. From this and the capabilites +and desires of a front end, different rendering routines will invoked +for each kind boxes created. This, in turn, produces strings in +(AMS)LaTeX, MathML, SVG, asymptote, or plain text. +For example, to process the Mathics3 builtin BezierCurve, a +BezierCurveBox will get created. Mathics3 has SVG and an Asymptote +renderers for BezierCurveBoxes. Which one is used is decided on by +the front-end's needs. """ import glob diff --git a/mathics/format/render/asy.py b/mathics/format/render/asy.py index e904eef37..2217b114a 100644 --- a/mathics/format/render/asy.py +++ b/mathics/format/render/asy.py @@ -1,21 +1,22 @@ # -*- coding: utf-8 -*- """ -Lower-level format of Mathics objects as Asymptote Vector graphics strings. +Mathics3 Graphics box rendering to Asymptote Vector graphics strings. """ import re from mathics.builtin.box.graphics import ( + ArcBox, ArrowBox, BezierCurveBox, FilledCurveBox, + GraphicsElementBox, InsetBox, LineBox, PointBox, PolygonBox, RectangleBox, - _ArcBox, - _RoundBox, + RoundBox, ) from mathics.builtin.box.graphics3d import ( Arrow3DBox, @@ -83,16 +84,16 @@ def apply(self, asy): return self._template % (" * ".join(self.transforms), asy) -def arcbox(self: _ArcBox, **options) -> str: +def arcbox(box: ArcBox, **options) -> str: """ Aysmptote formatting for an arc of a circle or an ellipse. """ - if self.arc is None: + if box.arc is None: # We have a doughnut graph and this is the inner blank hole of that. # It is an empty circle - return _roundbox(self) + return roundbox(box) - x, y, rx, ry, sx, sy, ex, ey, _ = self._arc_params() + x, y, rx, ry, sx, sy, ex, ey, _ = box._arc_params() ry = max(ry, 0.1) # Avoid division by 0 yscale = ry / rx @@ -122,39 +123,39 @@ def create_arc_path(is_closed: bool, yscale: float) -> str: return arc_path - stroke_width = self.style.get_line_width(face_element=self.face_element) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + stroke_width = box.style.get_line_width(face_element=box.face_element) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, stroke_width=stroke_width, - is_face_element=bool(self.face_element), + is_face_element=bool(box.face_element), ) - command = "filldraw" if self.face_element else "draw" - arc_path = create_arc_path(self.face_element or False, yscale) + command = "filldraw" if box.face_element else "draw" + arc_path = create_arc_path(box.face_element or False, yscale) asy = f"""// ArcBox {command}({arc_path}, {pen});""" # print("### arcbox", asy) return asy -add_conversion_fn(_ArcBox, arcbox) +add_conversion_fn(ArcBox, arcbox) -def arrow_box(self: ArrowBox, **options) -> str: - width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def arrow_box(box: ArrowBox, **options) -> str: + width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, stroke_width=width, edge_opacity=edge_opacity_value + edge_color=box.edge_color, stroke_width=width, edge_opacity=edge_opacity_value ) - polyline = self.curve.make_draw_asy(pen) + polyline = box.curve.make_draw_asy(pen) arrow_pen = asy_create_pens( - face_color=self.edge_color, stroke_width=width, face_opacity=edge_opacity_value + face_color=box.edge_color, stroke_width=width, face_opacity=edge_opacity_value ) def polygon(points): @@ -162,10 +163,10 @@ def polygon(points): yield "--".join(["(%.5g,%5g)" % xy for xy in points]) yield "--cycle, % s);" % arrow_pen - extent = self.graphics.view_width or 0 - default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("asy", _ASYTransform) - asy = "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) + extent = box.graphics.view_width or 0 + default_arrow = box._default_arrow(polygon) + custom_arrow = box._custom_arrow("asy", _ASYTransform) + asy = "".join(box._draw(polyline, default_arrow, custom_arrow, extent)) # print("### arrowbox", asy) return asy @@ -185,26 +186,26 @@ def build_3d_pen_color(color, opacity=None): return color_str -def arrow3dbox(self, **options) -> str: +def arrow3dbox(box, **options) -> str: """ Asymptote 3D formatter for Arrow3DBox """ # Set style parameters. - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, stroke_width=1, edge_opacity=edge_opacity_value + edge_color=box.edge_color, stroke_width=1, edge_opacity=edge_opacity_value ) # Draw lines between all points except the last. lines_str = "--".join( - ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in self.lines[0][:-1]] + ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in box.lines[0][:-1]] ) asy = f"draw({lines_str}, {pen});\n" # Draw an arrow between the penultimate and the last point. last_line_str = "--".join( - ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in self.lines[0][-2:]] + ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in box.lines[0][-2:]] ) asy += f"draw(({last_line_str}), {pen}, Arrow3);\n" @@ -215,22 +216,22 @@ def arrow3dbox(self, **options) -> str: add_conversion_fn(Arrow3DBox) -def bezier_curve_box(self: BezierCurveBox, **options) -> str: +def bezier_curve_box(box: BezierCurveBox, **options) -> str: """ Asymptote formatter for BezierCurveBox. """ - line_width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + line_width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, edge_opacity=edge_opacity_value, ) asy = "// BezierCurveBox\n" - asy += asy_add_graph_import(self) - asy += asy_add_bezier_fn(self) - for i, line in enumerate(self.lines): + asy += asy_add_graph_import(box) + asy += asy_add_bezier_fn(box) + for i, line in enumerate(box.lines): pts = [str(xy.pos()) for xy in line] for j in range(1, len(pts) - 1, 3): triple = ", ".join(pts[j - 1 : j + 3]) @@ -244,18 +245,18 @@ def bezier_curve_box(self: BezierCurveBox, **options) -> str: add_conversion_fn(BezierCurveBox, bezier_curve_box) -def cone3dbox(self: Cone3DBox, **options) -> str: - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity +def cone3dbox(box: Cone3DBox, **options) -> str: + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = "// Cone3DBox\n" i = 0 - while i < len(self.points) / 2: + while i < len(box.points) / 2: try: # See https://tex.stackexchange.com/questions/736116/how-to-draw-the-base-geometrical-face-of-a-cone-surface-by-asymptote/736120#736120 - cone_center = self.points[i * 2].pos()[0] - cone_tip = self.points[i * 2 + 1].pos()[0] + cone_center = box.points[i * 2].pos()[0] + cone_tip = box.points[i * 2 + 1].pos()[0] if cone_center is None or cone_tip is None: continue @@ -270,7 +271,7 @@ def cone3dbox(self: Cone3DBox, **options) -> str: asy += f""" triple cone_center = {tuple(cone_center)}; triple cone_tip = {tuple(cone_tip)}; - real cone_radius = {self.radius}; + real cone_radius = {box.radius}; real cone_height = {cone_height}; path3 cone_circle = circle(cone_center, cone_radius, cone_tip); @@ -289,17 +290,17 @@ def cone3dbox(self: Cone3DBox, **options) -> str: add_conversion_fn(Cone3DBox) -def cuboid3dbox(self: Cuboid3DBox, **options) -> str: - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity +def cuboid3dbox(box: Cuboid3DBox, **options) -> str: + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = "// Cuboid3DBox\n" i = 0 - while i < len(self.points) / 2: + while i < len(box.points) / 2: try: - point1 = self.points[i * 2].pos()[0] - point2 = self.points[i * 2 + 1].pos()[0] + point1 = box.points[i * 2].pos()[0] + point2 = box.points[i * 2 + 1].pos()[0] if point1 is None or point2 is None: continue @@ -327,23 +328,23 @@ def cuboid3dbox(self: Cuboid3DBox, **options) -> str: add_conversion_fn(Cuboid3DBox) -def cylinder3dbox(self: Cylinder3DBox, **options) -> str: - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity +def cylinder3dbox(box: Cylinder3DBox, **options) -> str: + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = "// Cylinder3DBox\n" # asy += "currentprojection=orthographic(3,1,4,center=true,zoom=.9);\n" i = 0 - while i < len(self.points) / 2: + while i < len(box.points) / 2: try: - point1 = self.points[i * 2].pos()[0] - point2 = self.points[i * 2 + 1].pos()[0] + point1 = box.points[i * 2].pos()[0] + point2 = box.points[i * 2 + 1].pos()[0] if point1 is None or point2 is None: continue - asy += f"real r={self.radius};\n" + asy += f"real r={box.radius};\n" asy += f"triple A={tuple(point1)}, B={tuple(point2)};\n" asy += "real h=abs(A-B);\n" asy += "revolution cyl=cylinder(A,r,h,B-A);\n" @@ -364,11 +365,11 @@ def cylinder3dbox(self: Cylinder3DBox, **options) -> str: add_conversion_fn(Cylinder3DBox) -def filled_curve_box(self, **options) -> str: - line_width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def filled_curve_box(box, **options) -> str: + line_width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, edge_opacity=edge_opacity_value, ) @@ -377,7 +378,7 @@ def filled_curve_box(self, **options) -> str: pen = "currentpen" def components(): - for component in self.components: + for component in box.components: transformed = [(k, [xy.pos() for xy in p]) for k, p in component] yield "fill(%s--cycle, %s);" % ("".join(asy_bezier(*transformed)), pen) @@ -387,12 +388,12 @@ def components(): add_conversion_fn(FilledCurveBox, filled_curve_box) -def graphics_elements(self, **options) -> str: +def graphics_elements(box: GraphicsElementBox, **options) -> str: """ - Asymptote formatting on a list of graphics elements. + Asymptote formatting on a GraphicsElementBox which may contain other GraphicsElementBox's. """ result = [] - for element in self.elements: + for element in box.elements: try: format_fn = lookup_method(element, "asy") except Exception: @@ -416,25 +417,25 @@ def graphics_elements(self, **options) -> str: add_conversion_fn(Graphics3DElements) -def inset_box(self, **options) -> str: +def inset_box(box: InsetBox, **options) -> str: """Asymptote formatting for boxing an Inset in a graphic.""" - x, y = self.pos.pos() + x, y = box.pos.pos() alignment = "SW" - if hasattr(self, "alignment"): - if self.alignment == "bottom": + if hasattr(box, "alignment"): + if box.alignment == "bottom": # This is typically done for labels under the x axis. alignment = "S" - elif self.alignment == "left": + elif box.alignment == "left": # This is typically done for labels to the left of the y axis. alignment = "W" - opacity_value = self.opacity.opacity if self.opacity else None - content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) + opacity_value = box.opacity.opacity if box.opacity else None + content = box.content.boxes_to_tex(evaluation=box.graphics.evaluation) # FIXME: don't hard code text_style_opts, but allow these to be adjustable. font_size = 3 pen = asy_create_pens( - edge_color=self.color, edge_opacity=opacity_value, fontsize=font_size + edge_color=box.color, edge_opacity=opacity_value, fontsize=font_size ) asy = f"""// InsetBox label("${content}$", ({x},{y}), align={alignment}, {pen});\n""" @@ -444,11 +445,11 @@ def inset_box(self, **options) -> str: add_conversion_fn(InsetBox, inset_box) -def line3dbox(self, **options) -> str: - # l = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def line3dbox(box: Line3DBox, **options) -> str: + # l = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, stroke_width=1, edge_opacity=edge_opacity_value + edge_color=box.edge_color, stroke_width=1, edge_opacity=edge_opacity_value ) return "".join( @@ -456,23 +457,23 @@ def line3dbox(self, **options) -> str: "--".join("({0},{1},{2})".format(*coords.pos()[0]) for coords in line), pen, ) - for line in self.lines + for line in box.lines ) add_conversion_fn(Line3DBox) -def line_box(self: LineBox) -> str: - line_width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def line_box(box: LineBox) -> str: + line_width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, edge_opacity=edge_opacity_value, ) asy = "// LineBox\n" - for line in self.lines: + for line in box.lines: path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) asy += "draw(%s, %s);" % (path, pen) # print("### linebox", asy) @@ -482,15 +483,15 @@ def line_box(self: LineBox) -> str: add_conversion_fn(LineBox, line_box) -def point3dbox(self: Point3DBox, **options) -> str: +def point3dbox(box: Point3DBox, **options) -> str: """ Asymptote 3D formatter for Point3DBox """ - face_color = self.face_color + face_color = box.face_color face_opacity_value = face_color.to_rgba()[3] if face_opacity_value is None: - face_opacity_value = self.face_opacity.opacity + face_opacity_value = box.face_opacity.opacity # Tempoary bug fix: default Point color should be black not white if list(face_color.to_rgba()[:3]) == [1, 1, 1]: @@ -500,7 +501,7 @@ def point3dbox(self: Point3DBox, **options) -> str: face_color=face_color, is_face_element=False, face_opacity=face_opacity_value ) points = [] - for line in self.lines: + for line in box.lines: point_coords = "--".join( "(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line ) @@ -515,24 +516,24 @@ def point3dbox(self: Point3DBox, **options) -> str: add_conversion_fn(Point3DBox) -def pointbox(self: PointBox, **options) -> str: - point_size, _ = self.style.get_style(PointSize, face_element=False) +def pointbox(box: PointBox, **options) -> str: + point_size, _ = box.style.get_style(PointSize, face_element=False) if point_size is None: - point_size = PointSize(self.graphics, value=DEFAULT_POINT_FACTOR) + point_size = PointSize(box.graphics, value=DEFAULT_POINT_FACTOR) # We'll use the heuristic that the default line width is 1 should correspond # to the DEFAULT_POINT_FACTOR dotfactor = INVERSE_POINT_FACTOR * point_size.value - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pen = asy_create_pens( - face_color=self.face_color, + face_color=box.face_color, is_face_element=False, dotfactor=dotfactor, face_opacity=face_opacity_value, ) asy = "// PointBox\n" - for line in self.lines: + for line in box.lines: for coords in line: asy += "dot(%s, %s);" % (coords.pos(), pen) @@ -543,21 +544,21 @@ def pointbox(self: PointBox, **options) -> str: add_conversion_fn(PointBox) -def polygon_3d_box(self: Polygon3DBox, **options) -> str: +def polygon_3d_box(box: Polygon3DBox, **options) -> str: """ Asymptote formatting of a Polygon3DBox. """ - stroke_width = self.style.get_line_width(face_element=True) - if self.vertex_colors is None: - face_color = self.face_color - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + stroke_width = box.style.get_line_width(face_element=True) + if box.vertex_colors is None: + face_color = box.face_color + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None else: face_color = None face_opacity_value = None - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, face_color=face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, @@ -566,7 +567,7 @@ def polygon_3d_box(self: Polygon3DBox, **options) -> str: ) asy = "// Polygon3DBox\n" - for line in self.lines: + for line in box.lines: asy += ( "path3 g=" + "--".join(["(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line]) @@ -581,18 +582,18 @@ def polygon_3d_box(self: Polygon3DBox, **options) -> str: add_conversion_fn(Polygon3DBox, polygon_3d_box) -def polygonbox(self: PolygonBox, **options) -> str: - line_width = self.style.get_line_width(face_element=True) - if self.vertex_colors is None: - face_color = self.face_color - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None +def polygonbox(box: PolygonBox, **options) -> str: + line_width = box.style.get_line_width(face_element=True) + if box.vertex_colors is None: + face_color = box.face_color + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None else: face_color = None face_opacity_value = None - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pens = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, face_color=face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, @@ -600,21 +601,21 @@ def polygonbox(self: PolygonBox, **options) -> str: is_face_element=True, ) asy = "// PolygonBox\n" - if self.vertex_colors is not None: + if box.vertex_colors is not None: paths = [] colors = [] edges = [] - for index, line in enumerate(self.lines): + for index, line in enumerate(box.lines): paths.append( "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" ) # ignore opacity colors.append( - ",".join([asy_color(color)[0] for color in self.vertex_colors[index]]) + ",".join([asy_color(color)[0] for color in box.vertex_colors[index]]) ) - edges.append(",".join(["0"] + ["1"] * (len(self.vertex_colors[index]) - 1))) + edges.append(",".join(["0"] + ["1"] * (len(box.vertex_colors[index]) - 1))) asy += "gouraudshade(%s, new pen[] {%s}, new int[] {%s});" % ( "^^".join(paths), @@ -622,7 +623,7 @@ def polygonbox(self: PolygonBox, **options) -> str: ",".join(edges), ) if pens and pens != "nullpen": - for line in self.lines: + for line in box.lines: path = ( "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" ) @@ -635,15 +636,15 @@ def polygonbox(self: PolygonBox, **options) -> str: add_conversion_fn(PolygonBox) -def rectanglebox(self: RectangleBox, **options) -> str: - line_width = self.style.get_line_width(face_element=True) - x1, y1 = self.p1.pos() - x2, y2 = self.p2.pos() - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None +def rectanglebox(box: RectangleBox, **options) -> str: + line_width = box.style.get_line_width(face_element=True) + x1, y1 = box.p1.pos() + x2, y2 = box.p2.pos() + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pens = asy_create_pens( - self.edge_color, - self.face_color, + box.edge_color, + box.face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, stroke_width=line_width, @@ -669,23 +670,23 @@ def rectanglebox(self: RectangleBox, **options) -> str: add_conversion_fn(RectangleBox) -def _roundbox(self: _RoundBox): - x, y = self.c.pos() - rx, ry = self.r.pos() +def roundbox(box: RoundBox): + x, y = box.c.pos() + rx, ry = box.r.pos() rx -= x ry -= y - line_width = self.style.get_line_width(face_element=self.face_element) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + line_width = box.style.get_line_width(face_element=box.face_element) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, stroke_width=line_width, - is_face_element=self.face_element, + is_face_element=box.face_element, ) - cmd = "filldraw" if self.face_element else "draw" + cmd = "filldraw" if box.face_element else "draw" return "%s(ellipse((%s,%s),%s,%s), %s);" % ( cmd, asy_number(x), @@ -696,44 +697,44 @@ def _roundbox(self: _RoundBox): ) -add_conversion_fn(_RoundBox) +add_conversion_fn(RoundBox) -def sphere3dbox(self: Sphere3DBox, **options) -> str: - # l = self.style.get_line_width(face_element=True) +def sphere3dbox(box: Sphere3DBox, **options) -> str: + # l = box.style.get_line_width(face_element=True) - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) return "// Sphere3DBox\n" + "\n".join( "draw(surface(sphere({0}, {1})), {2});".format( - tuple(coord.pos()[0]), self.radius, color_str + tuple(coord.pos()[0]), box.radius, color_str ) - for coord in self.points + for coord in box.points ) add_conversion_fn(Sphere3DBox) -def tube_3d_box(self: Tube3DBox, **options) -> str: - # if not (hasattr(self.graphics, "tube_import_added") and self.tube_import_added): - # self.graphics.tube_import_added = True +def tube_3d_box(box: Tube3DBox, **options) -> str: + # if not (hasattr(box.graphics, "tube_import_added") and box.tube_import_added): + # box.graphics.tube_import_added = True # asy_head = "import tube;\n\n" # else: # asy_head = "" - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = ( # asy_head + "// Tube3DBox\n draw(tube({0}, scale({1})*unitcircle), {2});".format( "--".join( - "({0},{1},{2})".format(*coords.pos()[0]) for coords in self.points + "({0},{1},{2})".format(*coords.pos()[0]) for coords in box.points ), - self.radius, + box.radius, color_str, ) ) @@ -743,16 +744,16 @@ def tube_3d_box(self: Tube3DBox, **options) -> str: add_conversion_fn(Tube3DBox, tube_3d_box) -def uniform_polyhedron_3d_box(self: UniformPolyhedron3DBox, **options) -> str: - # l = self.style.get_line_width(face_element=True) +def uniform_polyhedron_3d_box(box: UniformPolyhedron3DBox, **options) -> str: + # l = box.style.get_line_width(face_element=True) - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) - render_fn = HEDRON_NAME_MAP.get(self.sub_type, unimplimented_polygon) - return f"// {self.sub_type}\n" + "\n".join( - render_fn(tuple(coord.pos()[0]), self.edge_length, color_str) - for coord in self.points + render_fn = HEDRON_NAME_MAP.get(box.sub_type, unimplimented_polygon) + return f"// {box.sub_type}\n" + "\n".join( + render_fn(tuple(coord.pos()[0]), box.edge_length, color_str) + for coord in box.points ) diff --git a/mathics/format/render/json.py b/mathics/format/render/json.py index 5e9310535..93df337c5 100644 --- a/mathics/format/render/json.py +++ b/mathics/format/render/json.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter of Mathics objects as JSON data. +Mathics3 Graphics box rendering to JSON data. -Right now this happens mostly for graphics primitives. +Right now, this happens in graphics primitives. """ import json @@ -50,12 +50,12 @@ def convert_coord_collection( return data -def graphics_3D_elements(self, **options) -> list: - """Iterates over self.elements to convert each item. +def graphics_3D_elements(box: Graphics3DElements, **options) -> list: + """Iterates over box.elements to converting each item. The list of converted items is returned. """ result = [] - for element in self.elements: + for element in box.elements: format_fn = lookup_method(element, "json") result += format_fn(element) @@ -66,13 +66,13 @@ def graphics_3D_elements(self, **options) -> list: add_conversion_fn(Graphics3DElements, graphics_3D_elements) -def arrow_3d_box(self): +def arrow_3d_box(box: Arrow3DBox): """ Compact (lower-level) JSON formatting of a Arrow3DBox. """ # TODO: account for arrow widths and style - color = self.edge_color.to_rgba() - data = convert_coord_collection(self.lines, "arrow", color) + color = box.edge_color.to_rgba() + data = convert_coord_collection(box.lines, "arrow", color) # print("### json Arrow3DBox", data) return data @@ -80,18 +80,18 @@ def arrow_3d_box(self): add_conversion_fn(Arrow3DBox, arrow_3d_box) -def cone_3d_box(self): +def cone_3d_box(box: Cone3DBox): """ Compact (lower-level) JSON formatting of a Cone3DBox. """ - face_color = self.face_color + face_color = box.face_color if face_color is not None: face_color = face_color.to_js() data = convert_coord_collection( - [self.points], + [box.points], "cone", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Cone3DBox", data) return data @@ -100,15 +100,15 @@ def cone_3d_box(self): add_conversion_fn(Cone3DBox, cone_3d_box) -def cuboid_3d_box(self): +def cuboid_3d_box(box: Cuboid3DBox): """ Compact (lower-level) JSON formatting of a Cuboid3DBox. """ - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "cuboid", face_color, ) @@ -119,18 +119,18 @@ def cuboid_3d_box(self): add_conversion_fn(Cuboid3DBox, cuboid_3d_box) -def cylinder_3d_box(self): +def cylinder_3d_box(box: Cylinder3DBox): """ Compact (lower-level) JSON formatting of a Cylinder3DBox. """ - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "cylinder", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Cylinder3DBox", data) return data @@ -139,7 +139,7 @@ def cylinder_3d_box(self): add_conversion_fn(Cylinder3DBox, cylinder_3d_box) -def graphics3d_boxes_to_json(self, content=None, **options): +def graphics3d_boxes(box: Graphics3DBox, content=None, **options): """Turn the Graphics3DBox to into a something JSON like. This can be used to embed in something else like MathML or Javascript. @@ -154,10 +154,10 @@ def graphics3d_boxes_to_json(self, content=None, **options): ticks_style, calc_dimensions, boxscale, - ) = prepare_elements3d(self, self.content, options) + ) = prepare_elements3d(box, box.content, options) background = "rgba(100.0%, 100.0%, 100.0%, 100.0%)" - if self.background_color: - components = self.background_color.to_rgba() + if box.background_color: + components = box.background_color.to_rgba() if len(components) == 3: background = "rgb(" + ", ".join(f"{100*c}%" for c in components) + ")" else: @@ -192,26 +192,26 @@ def graphics3d_boxes_to_json(self, content=None, **options): "zmin": zmin, "zmax": zmax, }, - "lighting": self.lighting, - "viewpoint": self.viewpoint, + "lighting": box.lighting, + "viewpoint": box.viewpoint, "protocol": "1.1", } ) return json_repr -add_conversion_fn(Graphics3DBox, graphics3d_boxes_to_json) +add_conversion_fn(Graphics3DBox, graphics3d_boxes) -def line_3d_box(self): +def line_3d_box(box: Line3DBox): """ Compact (lower-level) JSON formatting of a Line3DBox. """ # TODO: account for line widths and style - color = self.edge_color.to_rgba() - if len(color) < 4 and self.edge_opacity: - color = color + [self.edge_opacity.opacity] - data = convert_coord_collection(self.lines, "line", color) + color = box.edge_color.to_rgba() + if len(color) < 4 and box.edge_opacity: + color = color + [box.edge_opacity.opacity] + data = convert_coord_collection(box.lines, "line", color) # print("### json Line3DBox", data) return data @@ -219,21 +219,21 @@ def line_3d_box(self): add_conversion_fn(Line3DBox, line_3d_box) -def point_3d_box(self) -> list: +def point_3d_box(box: Point3DBox) -> list: """ Compact (lower-level) JSON formatting of a Point3DBox. """ # TODO: account for point size - face_color = self.face_color.to_rgba() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + face_color = box.face_color.to_rgba() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] - point_size, _ = self.style.get_style(PointSize, face_element=False) + point_size, _ = box.style.get_style(PointSize, face_element=False) relative_point_size = 0.01 if point_size is None else point_size.value data = convert_coord_collection( - self.lines, + box.lines, "point", face_color, {"pointSize": relative_point_size * 0.5}, @@ -246,21 +246,21 @@ def point_3d_box(self) -> list: add_conversion_fn(Point3DBox, point_3d_box) -def polygon_3d_box(self) -> list: +def polygon_3d_box(box: Polygon3DBox) -> list: """ Compact (lower-level) JSON formatting of a Polygon3DBox. This format follows an API understood by mathics_threejs_backend. """ # TODO: account for line widths and style - if self.vertex_colors is None: - face_color = self.face_color.to_js() + if box.vertex_colors is None: + face_color = box.face_color.to_js() else: face_color = None - if face_color and len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + if face_color and len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - self.lines, + box.lines, "polygon", face_color, ) @@ -271,15 +271,15 @@ def polygon_3d_box(self) -> list: add_conversion_fn(Polygon3DBox, polygon_3d_box) -def sphere_3d_box(self) -> list: - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] +def sphere_3d_box(box: Sphere3DBox) -> list: + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "sphere", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Sphere3DBox", data) return data @@ -288,15 +288,15 @@ def sphere_3d_box(self) -> list: add_conversion_fn(Sphere3DBox, sphere_3d_box) -def uniform_polyhedron_3d_box(self) -> list: - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.edge_opacity: - face_color = face_color + [self.face_opacity.opacity] +def uniform_polyhedron_3d_box(box: UniformPolyhedron3DBox) -> list: + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.edge_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "uniformPolyhedron", face_color, - {"subType": self.sub_type}, + {"subType": box.sub_type}, ) # print("### json UniformPolyhedron3DBox", data) return data @@ -305,17 +305,17 @@ def uniform_polyhedron_3d_box(self) -> list: add_conversion_fn(UniformPolyhedron3DBox, uniform_polyhedron_3d_box) -def tube_3d_box(self) -> list: - face_color = self.face_color +def tube_3d_box(box: Tube3DBox) -> list: + face_color = box.face_color if face_color is not None: face_color = face_color.to_js() - if len(face_color) < 4 and self.edge_opacity: - face_color = face_color + [self.face_opacity.opacity] + if len(face_color) < 4 and box.edge_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "tube", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Tube3DBox", data) return data diff --git a/mathics/format/render/latex.py b/mathics/format/render/latex.py index 93ea3b1ce..329f987fa 100644 --- a/mathics/format/render/latex.py +++ b/mathics/format/render/latex.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- -"""Lower-level formatter of Mathics objects as (AMS)LaTeX strings. +""" +Mathics3 box rendering to (AMS)LaTeX strings. + +Formatting is usually initiated in Mathics via TeXForm[]. AMS LaTeX is LaTeX with addition mathematical symbols, which we may make use of via the mathics-scanner tables. -LaTeX formatting is usually initiated in Mathics via TeXForm[]. - TeXForm in WMA is slightly vague or misleading since the output is typically LaTeX rather than Plain TeX. In Mathics3, we also assume AMS LaTeX or more specifically that we the additional AMS Mathematical @@ -14,6 +15,7 @@ import re +from mathics.builtin.box.expression import BoxExpression from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox from mathics.builtin.box.layout import ( @@ -189,9 +191,9 @@ def replace(match): return text -def string(self, **options) -> str: +def string(s: String, **options) -> str: """String to LaTeX form""" - text = self.value + text = s.value def render(format, string_, in_text=False): return format % encode_tex(string_, in_text) @@ -236,16 +238,16 @@ def render(format, string_, in_text=False): add_conversion_fn(String, string) -def interpretation_box(self, **options): - return lookup_conversion_method(self.boxes, "latex")(self.boxes, **options) +def interpretation_box(box: InterpretationBox, **options): + return lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) add_conversion_fn(InterpretationBox, interpretation_box) -def pane_box(self, **options): - content = lookup_conversion_method(self.boxes, "latex")(self.boxes, **options) - options = self.box_options +def pane_box(box: PaneBox, **options): + content = lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) + options = box.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() if size == "System`Automatic": @@ -281,27 +283,27 @@ def pane_box(self, **options): add_conversion_fn(PaneBox, pane_box) -def fractionbox(self, **options) -> str: - _options = self.box_options.copy() +def fractionbox(box: FractionBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options return "\\frac{%s}{%s}" % ( - lookup_conversion_method(self.num, "latex")(self.num, **options), - lookup_conversion_method(self.den, "latex")(self.den, **options), + lookup_conversion_method(box.num, "latex")(box.num, **options), + lookup_conversion_method(box.den, "latex")(box.den, **options), ) add_conversion_fn(FractionBox, fractionbox) -def gridbox(self, elements=None, **box_options) -> str: +def gridbox(box: GridBox, elements=None, **box_options) -> str: def boxes_to_tex(box, **options): return lookup_conversion_method(box, "latex")(box, **options) if not elements: - elements = self._elements + elements = box._elements evaluation = box_options.get("evaluation") - items, options = self.get_array(elements, evaluation) + items, options = box.get_array(elements, evaluation) box_options.update(options) box_options["inside_list"] = True column_alignments = box_options["System`ColumnAlignments"].get_name() @@ -338,40 +340,40 @@ def boxes_to_tex(box, **options): add_conversion_fn(GridBox, gridbox) -def sqrtbox(self, **options): - _options = self.box_options.copy() +def sqrtbox(box: SqrtBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - if self.index: + if box.index: return "\\sqrt[%s]{%s}" % ( - lookup_conversion_method(self.radicand, "latex")(self.radicand, **options), - lookup_conversion_method(self.index, "latex")(self.index, **options), + lookup_conversion_method(box.radicand, "latex")(box.radicand, **options), + lookup_conversion_method(box.index, "latex")(box.index, **options), ) - return "\\sqrt{%s}" % lookup_conversion_method(self.radicand, "latex")( - self.radicand, **options + return "\\sqrt{%s}" % lookup_conversion_method(box.radicand, "latex")( + box.radicand, **options ) add_conversion_fn(SqrtBox, sqrtbox) -def superscriptbox(self, **options): - _options = self.box_options.copy() +def superscriptbox(box: SuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - base_to_tex = lookup_conversion_method(self.base, "latex") - tex1 = base_to_tex(self.base, **options) + base_to_tex = lookup_conversion_method(box.base, "latex") + tex1 = base_to_tex(box.base, **options) - sup_string = self.superindex.get_string_value() + sup_string = box.superindex.get_string_value() # Handle derivatives if sup_string == named_characters["Prime"]: return "%s'" % tex1 if sup_string == named_characters["Prime"] * 2: return "%s''" % tex1 - base = self.tex_block(tex1, True) - superidx_to_tex = lookup_conversion_method(self.superindex, "latex") - superindx = self.tex_block(superidx_to_tex(self.superindex, **options), True) - if len(superindx) == 1 and isinstance(self.superindex, (String, StyleBox)): + base = box.tex_block(tex1, True) + superidx_to_tex = lookup_conversion_method(box.superindex, "latex") + superindx = box.tex_block(superidx_to_tex(box.superindex, **options), True) + if len(superindx) == 1 and isinstance(box.superindex, (String, StyleBox)): return "%s^%s" % ( base, superindx, @@ -385,33 +387,33 @@ def superscriptbox(self, **options): add_conversion_fn(SuperscriptBox, superscriptbox) -def subscriptbox(self, **options): - _options = self.box_options.copy() +def subscriptbox(box: SubscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - base_to_tex = lookup_conversion_method(self.base, "latex") - subidx_to_tex = lookup_conversion_method(self.subindex, "latex") + base_to_tex = lookup_conversion_method(box.base, "latex") + subidx_to_tex = lookup_conversion_method(box.subindex, "latex") return "%s_%s" % ( - self.tex_block(base_to_tex(self.base, **options), True), - self.tex_block(subidx_to_tex(self.subindex, **options)), + box.tex_block(base_to_tex(box.base, **options), True), + box.tex_block(subidx_to_tex(box.subindex, **options)), ) add_conversion_fn(SubscriptBox, subscriptbox) -def subsuperscriptbox(self, **options): - _options = self.box_options.copy() +def subsuperscriptbox(box: SubsuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - base_to_tex = lookup_conversion_method(self.base, "latex") - subidx_to_tex = lookup_conversion_method(self.subindex, "latex") - superidx_to_tex = lookup_conversion_method(self.superindex, "latex") + base_to_tex = lookup_conversion_method(box.base, "latex") + subidx_to_tex = lookup_conversion_method(box.subindex, "latex") + superidx_to_tex = lookup_conversion_method(box.superindex, "latex") return "%s_%s^%s" % ( - self.tex_block(base_to_tex(self.base, **options), True), - self.tex_block(subidx_to_tex(self.subindex, **options)), - self.tex_block(superidx_to_tex(self.superindex, **options)), + box.tex_block(base_to_tex(box.base, **options), True), + box.tex_block(subidx_to_tex(box.subindex, **options)), + box.tex_block(superidx_to_tex(box.superindex, **options)), ) @@ -466,11 +468,11 @@ def rowbox_parenthesized(items, **options): return f'{bracket_data["latex_open"]}{contain}{bracket_data["latex_closing"]}' -def rowbox(self, **options) -> str: - _options = self.box_options.copy() +def rowbox(box: RowBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options - items = self.items + items = box.items # Handle special cases if len(items) >= 3: head, *rest = items @@ -489,17 +491,17 @@ def rowbox(self, **options) -> str: add_conversion_fn(RowBox, rowbox) -def stylebox(self, **options) -> str: - _options = self.box_options.copy() +def stylebox(box: StyleBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options - return lookup_conversion_method(self.boxes, "latex")(self.boxes, **options) + return lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) add_conversion_fn(StyleBox, stylebox) -def graphicsbox(self, elements=None, **options) -> str: +def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: """This is the top-level function that converts a Mathics Expression in to something suitable for AMSLaTeX. @@ -509,8 +511,8 @@ def graphicsbox(self, elements=None, **options) -> str: assert elements is None if not elements: - content = self.content - fields = prepare_elements2d(self, content, options, max_width=450) + content = box.content + fields = prepare_elements2d(box, content, options, max_width=450) if len(fields) == 2: elements, calc_dimensions = fields else: @@ -545,8 +547,8 @@ def graphicsbox(self, elements=None, **options) -> str: asy_number(ymax), ) - if self.background_color is not None: - color, opacity = asy_color(self.background_color) + if box.background_color is not None: + color, opacity = asy_color(box.background_color) if opacity is not None: color = color + f"+opacity({opacity})" asy_background = "filldraw(%s, %s);" % (asy_box, color) @@ -576,9 +578,9 @@ def graphicsbox(self, elements=None, **options) -> str: add_conversion_fn(GraphicsBox, graphicsbox) -def graphics3dbox(self, elements=None, **options) -> str: +def graphics3dbox(box: Graphics3DBox, elements=None, **options) -> str: assert elements is None - elements = self.content + elements = box.content ( elements, @@ -587,7 +589,7 @@ def graphics3dbox(self, elements=None, **options) -> str: ticks_style, calc_dimensions, boxscale, - ) = prepare_elements3d(self, elements, options, max_width=450) + ) = prepare_elements3d(box, elements, options, max_width=450) elements._apply_boxscaling(boxscale) @@ -614,7 +616,7 @@ def graphics3dbox(self, elements=None, **options) -> str: # Draw boundbox and axes boundbox_asy = "" - boundbox_lines = get_boundbox_lines3D(self, xmin, xmax, ymin, ymax, zmin, zmax) + boundbox_lines = get_boundbox_lines3D(box, xmin, xmax, ymin, ymax, zmin, zmax) for i, line in enumerate(boundbox_lines): if i in axes_indices: @@ -748,8 +750,8 @@ def graphics3dbox(self, elements=None, **options) -> str: height, width = (400, 400) # TODO: Proper size # Background color - if self.background_color: - bg_color, opacity = asy_color(self.background_color) + if box.background_color: + bg_color, opacity = asy_color(box.background_color) background_directive = "background=" + bg_color + ", " else: background_directive = "" @@ -769,7 +771,7 @@ def graphics3dbox(self, elements=None, **options) -> str: asy_number(width / 60), asy_number(height / 60), # Rescale viewpoint - [vp * max([xmax - xmin, ymax - ymin, zmax - zmin]) for vp in self.viewpoint], + [vp * max([xmax - xmin, ymax - ymin, zmax - zmin]) for vp in box.viewpoint], asy, boundbox_asy, background_directive, @@ -780,8 +782,8 @@ def graphics3dbox(self, elements=None, **options) -> str: add_conversion_fn(Graphics3DBox, graphics3dbox) -def tag_and_form_box(self, **options): - return lookup_conversion_method(self.boxes, "latex")(self.boxes, **options) +def tag_and_form_box(box: BoxExpression, **options): + return lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) add_conversion_fn(FormBox, tag_and_form_box) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 683471d0d..054f7d2cc 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter of Mathics objects as MathML strings. +Mathics3 Graphics3D box rendering to MathML strings. MathML formatting is usually initiated in Mathics via MathMLForm[]. """ @@ -9,6 +9,7 @@ from mathics_scanner.tokeniser import is_symbol_name +from mathics.builtin.box.expression import BoxExpression from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox from mathics.builtin.box.layout import ( @@ -64,14 +65,14 @@ def encode_mathml(text: str) -> str: } -def string(self, **options) -> str: - text = self.value +def string(s: String, **options) -> str: + text = s.value number_as_text = options.get("number_as_text", None) show_string_characters = ( options.get("System`ShowStringCharacters", None) is SymbolTrue ) - if isinstance(self, BoxElementMixin): + if isinstance(s, BoxElementMixin): if number_as_text is None: number_as_text = SymbolFalse @@ -122,16 +123,16 @@ def render(format, string): add_conversion_fn(String, string) -def interpretation_box(self, **options): - return lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) +def interpretation_box(box: InterpretationBox, **options): + return lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) add_conversion_fn(InterpretationBox, interpretation_box) -def pane_box(self, **options): - content = lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) - options = self.box_options +def pane_box(box: PaneBox, **options): + content = lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) + options = box.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() if size is SymbolAutomatic: width = "" @@ -167,27 +168,27 @@ def pane_box(self, **options): add_conversion_fn(PaneBox, pane_box) -def fractionbox(self, **options) -> str: - _options = self.box_options.copy() +def fractionbox(box: FractionBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options return "%s %s" % ( - lookup_conversion_method(self.num, "mathml")(self.num, **options), - lookup_conversion_method(self.den, "mathml")(self.den, **options), + lookup_conversion_method(box.num, "mathml")(box.num, **options), + lookup_conversion_method(box.den, "mathml")(box.den, **options), ) add_conversion_fn(FractionBox, fractionbox) -def gridbox(self, elements=None, **box_options) -> str: +def gridbox(box: GridBox, elements=None, **box_options) -> str: def boxes_to_mathml(box, **options): return lookup_conversion_method(box, "mathml")(box, **options) if not elements: - elements = self._elements + elements = box._elements evaluation = box_options.get("evaluation") - items, options = self.get_array(elements, evaluation) + items, options = box.get_array(elements, evaluation) num_fields = max(len(item) if isinstance(item, tuple) else 1 for item in items) attrs = {} @@ -221,67 +222,67 @@ def boxes_to_mathml(box, **options): add_conversion_fn(GridBox, gridbox) -def sqrtbox(self, **options): - _options = self.box_options.copy() +def sqrtbox(box: SqrtBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - if self.index: + if box.index: return " %s %s " % ( - lookup_conversion_method(self.radicand, "mathml")(self.radicand, **options), - lookup_conversion_method(self.index, "mathml")(self.index, **options), + lookup_conversion_method(box.radicand, "mathml")(box.radicand, **options), + lookup_conversion_method(box.index, "mathml")(box.index, **options), ) - return " %s " % lookup_conversion_method(self.radicand, "mathml")( - self.radicand, **options + return " %s " % lookup_conversion_method(box.radicand, "mathml")( + box.radicand, **options ) add_conversion_fn(SqrtBox, sqrtbox) -def subscriptbox(self, **options): - _options = self.box_options.copy() +def subscriptbox(box: SubscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options return "%s %s" % ( - lookup_conversion_method(self.base, "mathml")(self.base, **options), - lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), + lookup_conversion_method(box.base, "mathml")(box.base, **options), + lookup_conversion_method(box.subindex, "mathml")(box.subindex, **options), ) add_conversion_fn(SubscriptBox, subscriptbox) -def superscriptbox(self, **options): - _options = self.box_options.copy() +def superscriptbox(box: SuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options return "%s %s" % ( - lookup_conversion_method(self.base, "mathml")(self.base, **options), - lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), + lookup_conversion_method(box.base, "mathml")(box.base, **options), + lookup_conversion_method(box.superindex, "mathml")(box.superindex, **options), ) add_conversion_fn(SuperscriptBox, superscriptbox) -def subsuperscriptbox(self, **options): - _options = self.box_options.copy() +def subsuperscriptbox(box: SubsuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options options["inside_row"] = True return "%s %s %s" % ( - lookup_conversion_method(self.base, "mathml")(self.base, **options), - lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), - lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), + lookup_conversion_method(box.base, "mathml")(box.base, **options), + lookup_conversion_method(box.subindex, "mathml")(box.subindex, **options), + lookup_conversion_method(box.superindex, "mathml")(box.superindex, **options), ) add_conversion_fn(SubsuperscriptBox, subsuperscriptbox) -def rowbox(self, **options) -> str: - _options = self.box_options.copy() +def rowbox(box: RowBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options result = [] @@ -296,16 +297,16 @@ def is_list_interior(content): is_list_row = False if ( - len(self.items) >= 3 - and self.items[0].get_string_value() == "{" - and self.items[2].get_string_value() == "}" - and self.items[1].has_form("RowBox", 1, None) + len(box.items) >= 3 + and box.items[0].get_string_value() == "{" + and box.items[2].get_string_value() == "}" + and box.items[1].has_form("RowBox", 1, None) ): - content = self.items[1].items + content = box.items[1].items if is_list_interior(content): is_list_row = True - if not inside_row and is_list_interior(self.items): + if not inside_row and is_list_interior(box.items): is_list_row = True if is_list_row: @@ -313,7 +314,7 @@ def is_list_interior(content): else: options["inside_row"] = True - for element in self.items: + for element in box.items: result.append(lookup_conversion_method(element, "mathml")(element, **options)) # print(f"mrow: {result}") @@ -324,20 +325,20 @@ def is_list_interior(content): add_conversion_fn(RowBox, rowbox) -def stylebox(self, **options) -> str: - _options = self.box_options.copy() +def stylebox(box: StyleBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options - return lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) + return lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) add_conversion_fn(StyleBox, stylebox) -def graphicsbox(self, elements=None, **options) -> str: +def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: # FIXME: SVG is the only thing we can convert MathML into. # Handle other graphics formats. - svg_body = self.boxes_to_format("svg", **options) + svg_body = box.boxes_to_format("svg", **options) # mglyph, which is what we have been using, is bad because MathML standard changed. # metext does not work because the way in which we produce the svg images is also based on this outdated mglyph @@ -349,8 +350,8 @@ def graphicsbox(self, elements=None, **options) -> str: ) # print(svg_body) mathml = template % ( - int(self.boxwidth), - int(self.boxheight), + int(box.boxwidth), + int(box.boxheight), base64.b64encode(svg_body.encode("utf8")).decode("utf8"), ) # print("boxes_to_mathml", mathml) @@ -360,9 +361,9 @@ def graphicsbox(self, elements=None, **options) -> str: add_conversion_fn(GraphicsBox, graphicsbox) -def graphics3dbox(self, elements=None, **options) -> str: +def graphics3dbox(box: GraphicsBox, elements=None, **options) -> str: """Turn the Graphics3DBox into a MathML string""" - result = self.boxes_to_js(**options) + result = box.boxes_to_js(**options) result = f"{result}" return result @@ -370,8 +371,8 @@ def graphics3dbox(self, elements=None, **options) -> str: add_conversion_fn(Graphics3DBox, graphics3dbox) -def tag_and_form_box(self, **options): - return lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) +def tag_and_form_box(box: BoxExpression, **options): + return lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) add_conversion_fn(FormBox, tag_and_form_box) diff --git a/mathics/format/render/svg.py b/mathics/format/render/svg.py index 8ae10b709..fcdb60636 100644 --- a/mathics/format/render/svg.py +++ b/mathics/format/render/svg.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter of Mathics objects as SVG strings. +Mathics3 Graphics box rendering to SVG strings. """ from mathics.builtin.box.graphics import ( + ArcBox, ArrowBox, BezierCurveBox, FilledCurveBox, @@ -13,8 +14,7 @@ PointBox, PolygonBox, RectangleBox, - _ArcBox, - _RoundBox, + RoundBox, ) from mathics.builtin.drawing.graphics3d import Graphics3DElements from mathics.builtin.graphics import DEFAULT_POINT_FACTOR, PointSize, _svg_bezier @@ -92,16 +92,16 @@ def create_css( return "; ".join(css) -def arcbox(self, **options) -> str: +def arcbox(box: ArcBox, **options) -> str: """ SVG formatting for arc of a circle. """ - if self.arc is None: + if box.arc is None: # We have a doughnut graph and this is the inner blank hole of that. # It is an empty circle - return _roundbox(self) + return roundbox(box) - x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() + x, y, rx, ry, sx, sy, ex, ey, large_arc = box._arc_params() def path(closed): if closed: @@ -115,40 +115,40 @@ def path(closed): if closed: yield "Z" - line_width = self.style.get_line_width(face_element=self.face_element) + line_width = box.style.get_line_width(face_element=box.face_element) style = create_css( - self.edge_color, - self.face_color, + box.edge_color, + box.face_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, - face_opacity=self.face_opacity, + edge_opacity=box.edge_opacity, + face_opacity=box.face_opacity, ) - svg = f"" + svg = f"" # print("_Arcbox: ", svg) return svg -add_conversion_fn(_ArcBox, arcbox) +add_conversion_fn(ArcBox, arcbox) -def arrow_box(self, **options) -> str: - width = self.style.get_line_width(face_element=False) +def arrow_box(box: ArrowBox, **options) -> str: + width = box.style.get_line_width(face_element=False) style = create_css( - self.edge_color, stroke_width=width, edge_opacity=self.edge_opacity + box.edge_color, stroke_width=width, edge_opacity=box.edge_opacity ) - polyline = self.curve.make_draw_svg(style) + polyline = box.curve.make_draw_svg(style) - arrow_style = create_css(face_color=self.edge_color, stroke_width=width) + arrow_style = create_css(face_color=box.edge_color, stroke_width=width) def polygon(points): yield '' - extent = self.graphics.view_width or 0 - default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("svg", _SVGTransform) - svg = "\n".join(self._draw(polyline, default_arrow, custom_arrow, extent)) + extent = box.graphics.view_width or 0 + default_arrow = box._default_arrow(polygon) + custom_arrow = box._custom_arrow("svg", _SVGTransform) + svg = "\n".join(box._draw(polyline, default_arrow, custom_arrow, extent)) # print("ArrowBox: ", svg) return svg @@ -156,19 +156,19 @@ def polygon(points): add_conversion_fn(ArrowBox, arrow_box) -def bezier_curve_box(self, **options) -> str: +def bezier_curve_box(box: BezierCurveBox, **options) -> str: """ SVG formatter for BezierCurveBox. """ - line_width = self.style.get_line_width(face_element=False) + line_width = box.style.get_line_width(face_element=False) style = create_css( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, + edge_opacity=box.edge_opacity, ) svg = "\n" - for line in self.lines: - s = "\n".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line]))) + for line in box.lines: + s = "\n".join(_svg_bezier((box.spline_degree, [xy.pos() for xy in line]))) svg += f'' # print("BezierCurveBox: ", svg) return svg @@ -177,7 +177,7 @@ def bezier_curve_box(self, **options) -> str: add_conversion_fn(BezierCurveBox, bezier_curve_box) -def density_plot_box(self, **options): +def density_plot_box(box, **options): """ SVG formatter for DensityPlotBox. """ @@ -202,9 +202,9 @@ def density_plot_box(self, **options): # to go from the center to each of the (square) sides. svg_data = [""] - for index, triangle_coords in enumerate(self.lines): + for index, triangle_coords in enumerate(box.lines): triangle = [coords.pos() for coords in triangle_coords] - colors = [rgb.to_js() for rgb in self.vertex_colors[index]] + colors = [rgb.to_js() for rgb in box.vertex_colors[index]] r = (colors[0][0] + colors[1][0] + colors[2][0]) / 3 g = (colors[0][1] + colors[1][1] + colors[2][1]) / 3 b = (colors[0][2] + colors[1][2] + colors[2][1]) / 3 @@ -221,21 +221,21 @@ def density_plot_box(self, **options): # No add_conversion_fn since this is a hacken-on polygonbox -def filled_curve_box(self, **options): - line_width = self.style.get_line_width(face_element=False) +def filled_curve_box(box: FilledCurveBox, **options): + line_width = box.style.get_line_width(face_element=False) style = create_css( - edge_color=self.edge_color, face_color=self.face_color, stroke_width=line_width + edge_color=box.edge_color, face_color=box.face_color, stroke_width=line_width ) style = create_css( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, - face_opacity=self.edge_opacity, + edge_opacity=box.edge_opacity, + face_opacity=box.edge_opacity, ) def components(): - for component in self.components: + for component in box.components: transformed = [(k, [xy.pos() for xy in p]) for k, p in component] yield " ".join(_svg_bezier(*transformed)) + " Z" @@ -249,7 +249,7 @@ def components(): add_conversion_fn(FilledCurveBox, filled_curve_box) -def graphics_box(self, elements=None, **options: dict) -> str: +def graphics_box(box: GraphicsBox, elements=None, **options: dict) -> str: """ Top-level SVG routine takes ``elements`` and ``options`` and turns this into a SVG string, including the .. tag. @@ -266,7 +266,7 @@ def graphics_box(self, elements=None, **options: dict) -> str: """ assert elements is None - elements = self.content + elements = box.content data = options.get("data", None) assert data is None @@ -277,27 +277,27 @@ def graphics_box(self, elements=None, **options: dict) -> str: xmax, ymin, ymax, - self.boxwidth, - self.boxheight, + box.boxwidth, + box.boxheight, width, height, ) = data else: elements, calc_dimensions = prepare_elements2d( - self, elements, options, neg_y=True + box, elements, options, neg_y=True ) ( xmin, xmax, ymin, ymax, - self.boxwidth, - self.boxheight, + box.boxwidth, + box.boxheight, width, height, ) = calc_dimensions() - elements.view_width = self.boxwidth + elements.view_width = box.boxwidth format_fn = lookup_method(elements, "svg") if format_fn is not None: @@ -305,17 +305,17 @@ def graphics_box(self, elements=None, **options: dict) -> str: else: svg_body = elements.to_svg(**options) - self.boxwidth = options.get("width", self.boxwidth) - self.boxheight = options.get("height", self.boxheight) + box.boxwidth = options.get("width", box.boxwidth) + box.boxheight = options.get("height", box.boxheight) - tooltip_text = self.tooltip_text or "" - if self.background_color is not None: + tooltip_text = box.tooltip_text or "" + if box.background_color is not None: # FIXME: tests don't seem to cover this section of code. # Wrap svg_elements in a rectangle background = "rgba(100%,100%,100%,100%)" - if self.background_color: - components = self.background_color.to_rgba() + if box.background_color: + components = box.background_color.to_rgba() if len(components) == 3: background = "rgb(" + ", ".join(f"{100*c}%" for c in components) + ")" else: @@ -324,8 +324,8 @@ def graphics_box(self, elements=None, **options: dict) -> str: svg_body = f""" {tooltip_text} {svg_body} """ @@ -333,7 +333,7 @@ def graphics_box(self, elements=None, **options: dict) -> str: if options.get("noheader", False): return svg_body - svg_main = wrap_svg_body(self.boxwidth, self.boxheight, xmin, ymin, svg_body) + svg_main = wrap_svg_body(box.boxwidth, box.boxheight, xmin, ymin, svg_body) # print("svg_main", svg_main) return svg_main # , width, height @@ -341,12 +341,12 @@ def graphics_box(self, elements=None, **options: dict) -> str: add_conversion_fn(GraphicsBox, graphics_box) -def graphics_elements(self, **options) -> str: +def graphics_elements(box: GraphicsElements, **options) -> str: """ - SVG formatting on a list of graphics elements. + SVG formatting on a GraphicsElementBox which may contain other GraphicsElementBox's. """ result = [""] - for element in self.elements: + for element in box.elements: try: format_fn = lookup_method(element, "svg") except Exception: @@ -370,45 +370,45 @@ def graphics_elements(self, **options) -> str: add_conversion_fn(Graphics3DElements) -def inset_box(self, **options) -> str: +def inset_box(box: InsetBox, **options) -> str: """ SVG formatting for boxing an Inset in a graphic. """ - x, y = self.pos.pos() + x, y = box.pos.pos() offset = options.get("offset", None) if offset is not None: x = x + offset[0] y = y + offset[1] - if hasattr(self.content, "to_svg"): - content = self.content.to_svg(noheader=True, offset=(x, y)) + if hasattr(box.content, "to_svg"): + content = box.content.to_svg(noheader=True, offset=(x, y)) svg = "\n" + content + "\n" else: css_style = create_css( - font_color=self.color, - edge_color=self.color, - face_color=self.color, + font_color=box.color, + edge_color=box.color, + face_color=box.color, stroke_width=0.2, - opacity=self.opacity.opacity, + opacity=box.opacity.opacity, ) - text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"' + text_pos_opts = f'x="{x}" y="{y}" ox="{box.opos[0]}" oy="{box.opos[1]}"' alignment = " dominant-baseline:hanging;" - if hasattr(self, "alignment"): - if self.alignment == "bottom": + if hasattr(box, "alignment"): + if box.alignment == "bottom": # This is typically done for labels under the x axis. alignment = " dominant-baseline:hanging; text-anchor:middle;" - elif self.alignment == "left": + elif box.alignment == "left": # This is typically done for labels to the left of the y axis. alignment = " dominant-baseline:middle; text-anchor:end;" # FIXME: don't hard code text_style_opts, but allow these to be adjustable. text_style_opts = alignment - content = self.content.boxes_to_text(evaluation=self.graphics.evaluation) + content = box.content.boxes_to_text(evaluation=box.graphics.evaluation) font_size = f'''font-size="{options.get("point_size", "10px")}"''' svg = f'{content}' - # content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation) - # style = create_css(font_color=self.color) + # content = box.content.boxes_to_mathml(evaluation=box.graphics.evaluation) + # style = create_css(font_color=box.color) # svg = ( # '' # "%s") @@ -420,12 +420,12 @@ def inset_box(self, **options) -> str: add_conversion_fn(InsetBox, inset_box) -def line_box(self, **options) -> str: - line_width = self.style.get_line_width(face_element=False) +def line_box(box: LineBox, **options) -> str: + line_width = box.style.get_line_width(face_element=False) style = create_css( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, + edge_opacity=box.edge_opacity, ) # The following line options come from looking at SVG produced from WMA. @@ -434,7 +434,7 @@ def line_box(self, **options) -> str: style += "; stroke-linecap:square; stroke-linejoin:miter; stroke-miterlimit:3.25" svg = "\n" - for line in self.lines: + for line in box.lines: svg += '' % ( " ".join(["%f,%f" % coords.pos() for coords in line]), style, @@ -446,18 +446,18 @@ def line_box(self, **options) -> str: add_conversion_fn(LineBox, line_box) -def pointbox(self, **options) -> str: - point_size, _ = self.style.get_style(PointSize, face_element=False) +def pointbox(box: PointBox, **options) -> str: + point_size, _ = box.style.get_style(PointSize, face_element=False) if point_size is None: - point_size = PointSize(self.graphics, value=DEFAULT_POINT_FACTOR) + point_size = PointSize(box.graphics, value=DEFAULT_POINT_FACTOR) size = point_size.get_absolute_size() style = create_css( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=0, - face_color=self.face_color, - edge_opacity=self.edge_opacity, - face_opacity=self.face_opacity, + face_color=box.face_color, + edge_opacity=box.edge_opacity, + face_opacity=box.face_opacity, ) # The following line options come from looking at SVG produced from WMA. @@ -465,7 +465,7 @@ def pointbox(self, **options) -> str: style += "; fill-rule:even-odd" svg = "" - for line in self.lines: + for line in box.lines: for coords in line: svg += f""" str: add_conversion_fn(PointBox) -def polygonbox(self, **options): +def polygonbox(box: PolygonBox, **options): """ SVG formatter for PolygonBox """ - line_width = self.style.get_line_width(face_element=True) + line_width = box.style.get_line_width(face_element=True) # Hack alert. Currently we encode density plots as a polygon box where # each polygon is a triangle with a color. We know we have this case because - # self.vertex_colors is not empty here. - if self.vertex_colors: - return density_plot_box(self, **options) + # box.vertex_colors is not empty here. + if box.vertex_colors: + return density_plot_box(box, **options) style = create_css( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, - face_opacity=self.face_opacity, + edge_opacity=box.edge_opacity, + face_opacity=box.face_opacity, ) svg = "\n" @@ -505,7 +505,7 @@ def polygonbox(self, **options): # Perhaps one day we will find it useful to have other fill_rules specified as an option. fill_rule = "evenodd" - for line in self.lines: + for line in box.lines: svg += f""" ' - # print("_RoundBox: ", svg) + # print("RoundBox: ", svg) return svg -add_conversion_fn(_RoundBox) +add_conversion_fn(RoundBox) def wrap_svg_body( diff --git a/mathics/format/render/text.py b/mathics/format/render/text.py index 1c2b5b9a5..a62f201cb 100644 --- a/mathics/format/render/text.py +++ b/mathics/format/render/text.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter Mathics objects as plain text. +Mathics3 box rendering to plain text. """ @@ -33,8 +33,8 @@ def boxes_to_text(boxes, **options) -> str: return lookup_method(boxes, "text")(boxes, **options) -def string(self, **options) -> str: - value = self.value +def string(s: String, **options) -> str: + value = s.value show_string_characters = ( options.get("System`ShowStringCharacters", None) is SymbolTrue ) From 3c53f989be1ed1734f8716b964d226c59a7052dc Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 3 Feb 2026 10:41:04 -0500 Subject: [PATCH 18/31] Fix type errors found by mypy Initialize: * BoxExpression.boxes * GraphicsBox.boxwidth * GraphicsBox.boxheight * GrapnicsBox.boxes Make sure to convert sympy.Float to Python float --- mathics/builtin/box/expression.py | 4 ++++ mathics/builtin/box/graphics.py | 3 +++ mathics/format/box/numberform.py | 2 +- mathics/format/render/json.py | 4 ++-- mathics/format/render/mathml.py | 2 +- mathics/format/render/svg.py | 13 +++++++++++-- 6 files changed, 22 insertions(+), 6 deletions(-) diff --git a/mathics/builtin/box/expression.py b/mathics/builtin/box/expression.py index 476e5156d..6180b630f 100644 --- a/mathics/builtin/box/expression.py +++ b/mathics/builtin/box/expression.py @@ -81,6 +81,10 @@ def __new__(cls, *elements, **kwargs): instance._elements = None return instance + def __init(self, *args, **kwargs): + super().__init(args, kwargs) + self.boxes = [] + def do_format(self, evaluation, format): return self diff --git a/mathics/builtin/box/graphics.py b/mathics/builtin/box/graphics.py index c7ce1084a..2e7c4c541 100644 --- a/mathics/builtin/box/graphics.py +++ b/mathics/builtin/box/graphics.py @@ -471,6 +471,9 @@ def init(self, *items, **kwargs): self.background_color = None self.tooltip_text: Optional[str] = None self.evaluation = kwargs.pop("_evaluation", None) + self.boxwidth: int = -1 + self.boxheight: int = -1 + self.boxes: list = [] @property def elements(self): diff --git a/mathics/format/box/numberform.py b/mathics/format/box/numberform.py index e1e7f92b5..2e864eb64 100644 --- a/mathics/format/box/numberform.py +++ b/mathics/format/box/numberform.py @@ -314,7 +314,7 @@ def get_baseform_elements( raise ValueError if isinstance(expr, PrecisionReal): - x = expr.to_sympy() + x = float(expr.to_sympy()) p = int(ceil(expr.get_precision() / LOG2_10) + 1) elif isinstance(expr, MachineReal): x = expr.value diff --git a/mathics/format/render/json.py b/mathics/format/render/json.py index 93df337c5..4d1b81b8e 100644 --- a/mathics/format/render/json.py +++ b/mathics/format/render/json.py @@ -139,7 +139,7 @@ def cylinder_3d_box(box: Cylinder3DBox): add_conversion_fn(Cylinder3DBox, cylinder_3d_box) -def graphics3d_boxes(box: Graphics3DBox, content=None, **options): +def graphics3d_boxes_to_json(box: Graphics3DBox, content=None, **options): """Turn the Graphics3DBox to into a something JSON like. This can be used to embed in something else like MathML or Javascript. @@ -200,7 +200,7 @@ def graphics3d_boxes(box: Graphics3DBox, content=None, **options): return json_repr -add_conversion_fn(Graphics3DBox, graphics3d_boxes) +add_conversion_fn(Graphics3DBox, graphics3d_boxes_to_json) def line_3d_box(box: Line3DBox): diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 054f7d2cc..70c641e4c 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -361,7 +361,7 @@ def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: add_conversion_fn(GraphicsBox, graphicsbox) -def graphics3dbox(box: GraphicsBox, elements=None, **options) -> str: +def graphics3dbox(box, elements=None, **options) -> str: """Turn the Graphics3DBox into a MathML string""" result = box.boxes_to_js(**options) result = f"{result}" diff --git a/mathics/format/render/svg.py b/mathics/format/render/svg.py index fcdb60636..2c7c9ddb2 100644 --- a/mathics/format/render/svg.py +++ b/mathics/format/render/svg.py @@ -305,8 +305,17 @@ def graphics_box(box: GraphicsBox, elements=None, **options: dict) -> str: else: svg_body = elements.to_svg(**options) - box.boxwidth = options.get("width", box.boxwidth) - box.boxheight = options.get("height", box.boxheight) + boxwidth = options.get("width", box.boxwidth) + boxheight = options.get("height", box.boxheight) + + assert isinstance( + boxwidth, int + ), f"boxwidth {boxwidth} should be 'int'. is {type(boxwidth)}" + assert isinstance( + boxheight, int + ), f"boxwidth {boxheight} should be 'int'. is {type(boxheight)}" + box.boxwidth = boxwidth + box.boxheight = boxheight tooltip_text = box.tooltip_text or "" if box.background_color is not None: From 942752a1839053e2c5ff05052534e20966f15085 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 3 Feb 2026 10:56:53 -0500 Subject: [PATCH 19/31] More mypy/type work. --- mathics/format/box/numberform.py | 2 +- mathics/format/render/svg.py | 8 ++++---- test/builtin/test_forms.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mathics/format/box/numberform.py b/mathics/format/box/numberform.py index 2e864eb64..e1e7f92b5 100644 --- a/mathics/format/box/numberform.py +++ b/mathics/format/box/numberform.py @@ -314,7 +314,7 @@ def get_baseform_elements( raise ValueError if isinstance(expr, PrecisionReal): - x = float(expr.to_sympy()) + x = expr.to_sympy() p = int(ceil(expr.get_precision() / LOG2_10) + 1) elif isinstance(expr, MachineReal): x = expr.value diff --git a/mathics/format/render/svg.py b/mathics/format/render/svg.py index 2c7c9ddb2..bd48f4df4 100644 --- a/mathics/format/render/svg.py +++ b/mathics/format/render/svg.py @@ -309,11 +309,11 @@ def graphics_box(box: GraphicsBox, elements=None, **options: dict) -> str: boxheight = options.get("height", box.boxheight) assert isinstance( - boxwidth, int - ), f"boxwidth {boxwidth} should be 'int'. is {type(boxwidth)}" + boxwidth, (int, float) + ), f"boxwidth {boxwidth} should be 'int' or 'float'. is {type(boxwidth)}" assert isinstance( - boxheight, int - ), f"boxwidth {boxheight} should be 'int'. is {type(boxheight)}" + boxheight, (int, float) + ), f"boxwidth {boxheight} should be 'int' or 'float'. is {type(boxheight)}" box.boxwidth = boxwidth box.boxheight = boxheight diff --git a/test/builtin/test_forms.py b/test/builtin/test_forms.py index 7bc9e5f59..890c988c4 100644 --- a/test/builtin/test_forms.py +++ b/test/builtin/test_forms.py @@ -403,7 +403,7 @@ def test_makeboxes_form(expr, form, head, subhead): ), ], ) -def test_private_doctests_output(str_expr, msgs, str_expected, fail_msg): +def test_output(str_expr, msgs, str_expected, fail_msg): """ """ check_evaluation( str_expr, From cc1afded8798a52a145a194b9d108b3d36e63383 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 6 Feb 2026 11:42:57 -0300 Subject: [PATCH 20/31] merge --- mathics/format/render/mathml.py | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index d47216597..06e4430b1 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -130,6 +130,25 @@ def fractionbox(box: FractionBox, **options) -> str: add_conversion_fn(FractionBox, fractionbox) +def graphics3dbox(box, elements=None, **options) -> str: + """Turn the Graphics3DBox into a MathML string""" + indent_level = options.get("_indent_level", 0) + indent_spaces = " " * indent_level + result = box.box_to_js(**options) + result = ( + f"{indent_spaces}\n" + f"\n" + f"{indent_spaces} \n" + f"{indent_spaces} {result}\n" + f"{indent_spaces} \n\n" + f"{indent_spaces}" + ) + return result + + +add_conversion_fn(Graphics3DBox, graphics3dbox) + + def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: # FIXME: SVG is the only thing we can convert MathML into. # Handle other graphics formats. @@ -160,25 +179,6 @@ def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: add_conversion_fn(GraphicsBox, graphicsbox) -def graphics3dbox(box, elements=None, **options) -> str: - """Turn the Graphics3DBox into a MathML string""" - indent_level = options.get("_indent_level", 0) - indent_spaces = " " * indent_level - result = box.box_to_js(**options) - result = ( - f"{indent_spaces}\n" - f"\n" - f"{indent_spaces} \n" - f"{indent_spaces} {result}\n" - f"{indent_spaces} \n\n" - f"{indent_spaces}" - ) - return result - - -add_conversion_fn(Graphics3DBox, graphics3dbox) - - def gridbox(box: GridBox, elements=None, **options) -> str: def boxes_to_mathml(box, **options): return lookup_conversion_method(box, "mathml")(box, **options) From 262b6286a8a91faf5a388529d01db5d000abd823 Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 6 Feb 2026 14:21:55 -0300 Subject: [PATCH 21/31] merge --- mathics/format/render/mathml.py | 112 ++++++++++++++++---------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 2f05495ba..aee4b0a92 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -102,6 +102,16 @@ def fractionbox(box: FractionBox, **options) -> str: add_conversion_fn(FractionBox, fractionbox) +def graphics3dbox(box, elements=None, **options) -> str: + """Turn the Graphics3DBox into a MathML string""" + result = box.boxes_to_js(**options) + result = f"\n\n\n{result}\n\n\n" + return result + + +add_conversion_fn(Graphics3DBox, graphics3dbox) + + def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: # FIXME: SVG is the only thing we can convert MathML into. # Handle other graphics formats. @@ -128,16 +138,6 @@ def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: add_conversion_fn(GraphicsBox, graphicsbox) -def graphics3dbox(box, elements=None, **options) -> str: - """Turn the Graphics3DBox into a MathML string""" - result = box.boxes_to_js(**options) - result = f"\n\n\n{result}\n\n\n" - return result - - -add_conversion_fn(Graphics3DBox, graphics3dbox) - - def gridbox(box: GridBox, elements=None, **box_options) -> str: def boxes_to_mathml(box, **options): return lookup_conversion_method(box, "mathml")(box, **options) @@ -308,48 +308,6 @@ def sqrtbox(box: SqrtBox, **options): add_conversion_fn(SqrtBox, sqrtbox) -def subscriptbox(box: SubscriptBox, **options): - # Note: values set in `options` take precedence over `box_options` - child_options = {**options, **box.box_options} - return "\n%s\n%s\n" % ( - lookup_conversion_method(box.base, "mathml")(box.base, **child_options), - lookup_conversion_method(box.subindex, "mathml")(box.subindex, **child_options), - ) - - -add_conversion_fn(SubscriptBox, subscriptbox) - - -def subsuperscriptbox(box: SubsuperscriptBox, **options): - # Note: values set in `options` take precedence over `box_options` - child_options = {**box.box_options, **options} - box.base.inside_row = box.subindex.inside_row = box.superindex.inside_row = True - return "\n%s\n%s\n%s\n" % ( - lookup_conversion_method(box.base, "mathml")(box.base, **child_options), - lookup_conversion_method(box.subindex, "mathml")(box.subindex, **child_options), - lookup_conversion_method(box.superindex, "mathml")( - box.superindex, **child_options - ), - ) - - -add_conversion_fn(SubsuperscriptBox, subsuperscriptbox) - - -def superscriptbox(box: SuperscriptBox, **options): - # Note: values set in `options` take precedence over `box_options` - child_options = {**options, **box.box_options} - return "\n%s\n%s\n" % ( - lookup_conversion_method(box.base, "mathml")(box.base, **child_options), - lookup_conversion_method(box.superindex, "mathml")( - box.superindex, **child_options - ), - ) - - -add_conversion_fn(SuperscriptBox, superscriptbox) - - def string(s: String, **options) -> str: text = s.value @@ -404,14 +362,46 @@ def render(format, string): add_conversion_fn(String, string) -def stylebox(box: StyleBox, **options) -> str: +def subscriptbox(box: SubscriptBox, **options): + # Note: values set in `options` take precedence over `box_options` child_options = {**options, **box.box_options} - return lookup_conversion_method(box.inner_box, "mathml")( - box.inner_box, **child_options + return "\n%s\n%s\n" % ( + lookup_conversion_method(box.base, "mathml")(box.base, **child_options), + lookup_conversion_method(box.subindex, "mathml")(box.subindex, **child_options), ) -add_conversion_fn(StyleBox, stylebox) +add_conversion_fn(SubscriptBox, subscriptbox) + + +def subsuperscriptbox(box: SubsuperscriptBox, **options): + # Note: values set in `options` take precedence over `box_options` + child_options = {**box.box_options, **options} + box.base.inside_row = box.subindex.inside_row = box.superindex.inside_row = True + return "\n%s\n%s\n%s\n" % ( + lookup_conversion_method(box.base, "mathml")(box.base, **child_options), + lookup_conversion_method(box.subindex, "mathml")(box.subindex, **child_options), + lookup_conversion_method(box.superindex, "mathml")( + box.superindex, **child_options + ), + ) + + +add_conversion_fn(SubsuperscriptBox, subsuperscriptbox) + + +def superscriptbox(box: SuperscriptBox, **options): + # Note: values set in `options` take precedence over `box_options` + child_options = {**options, **box.box_options} + return "\n%s\n%s\n" % ( + lookup_conversion_method(box.base, "mathml")(box.base, **child_options), + lookup_conversion_method(box.superindex, "mathml")( + box.superindex, **child_options + ), + ) + + +add_conversion_fn(SuperscriptBox, superscriptbox) def tag_and_form_box(box: BoxExpression, **options): @@ -420,3 +410,13 @@ def tag_and_form_box(box: BoxExpression, **options): add_conversion_fn(FormBox, tag_and_form_box) add_conversion_fn(TagBox, tag_and_form_box) + + +def stylebox(box: StyleBox, **options) -> str: + child_options = {**options, **box.box_options} + return lookup_conversion_method(box.inner_box, "mathml")( + box.inner_box, **child_options + ) + + +add_conversion_fn(StyleBox, stylebox) From 5488a1324a16433e53c64b54bc575784b15adbfe Mon Sep 17 00:00:00 2001 From: mmatera Date: Fri, 6 Feb 2026 16:00:30 -0300 Subject: [PATCH 22/31] fix mypy --- mathics/format/render/mathml.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index a88b1469b..a113f85ba 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -33,7 +33,6 @@ from mathics_scanner.tokeniser import is_symbol_name -from mathics.builtin.box.expression import BoxExpression from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox from mathics.builtin.box.layout import ( @@ -197,7 +196,7 @@ def interpretation_box(box: InterpretationBox, **options): if origin.has_form("InputForm", None): # InputForm produce outputs of the form # InterpretationBox[Style[_String, ...], origin_InputForm, opts___] - assert isinstance(box, StyleBox), f"boxes={boxes} are not a StyleBox" + assert isinstance(box, StyleBox), f"boxes={box} is not a StyleBox." box = box.inner_box child_options["System`ShowStringCharacters"] = SymbolTrue assert isinstance(box, String) From 5be081a82bab0ece2aa515fe49e4bdb0b02451b4 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 6 Feb 2026 18:27:22 -0300 Subject: [PATCH 23/31] merge --- mathics/core/element.py | 3 - mathics/core/formatter.py | 108 ++++++++++---------------------- mathics/format/render/latex.py | 45 +++++++------ mathics/format/render/mathml.py | 49 +++++++-------- 4 files changed, 76 insertions(+), 129 deletions(-) diff --git a/mathics/core/element.py b/mathics/core/element.py index 231e4ce77..17c531320 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -417,13 +417,10 @@ def box_to_format(self, format: str, **options) -> str: return box_to_format(self, format, **options) def boxes_to_mathml(self, **options) -> str: - """For compatibility deprecated""" return self.box_to_format("mathml", **options) def boxes_to_tex(self, **options) -> str: - """For compatibility deprecated""" return self.box_to_format("latex", **options) def boxes_to_text(self, **options) -> str: - """For compatibility deprecated""" return self.box_to_format("text", **options) diff --git a/mathics/core/formatter.py b/mathics/core/formatter.py index 392c6a6cd..0fcf62474 100644 --- a/mathics/core/formatter.py +++ b/mathics/core/formatter.py @@ -1,5 +1,4 @@ import inspect -import re from typing import Callable from mathics.core.element import BoxElementMixin @@ -15,43 +14,40 @@ def encode_mathml(text: str) -> str: return text -TEX_REPLACE = { - "{": r"\{", - "}": r"\}", - "_": r"\_", - "$": r"\$", - "%": r"\%", - "#": r"\#", - "&": r"\&", - "\\": r"\backslash{}", - "^": r"{}^{\wedge}", - "~": r"\sim{}", - "|": r"\vert{}", -} -TEX_TEXT_REPLACE = TEX_REPLACE.copy() -TEX_TEXT_REPLACE.update( - { - "<": r"$<$", - ">": r"$>$", - "~": r"$\sim$", - "|": r"$\vert$", - "\\": r"$\backslash$", - "^": r"${}^{\wedge}$", - } -) -TEX_REPLACE_RE = re.compile("([" + "".join([re.escape(c) for c in TEX_REPLACE]) + "])") - - -def encode_tex(text: str, in_text=False) -> str: - def replace(match): - c = match.group(1) - repl = TEX_TEXT_REPLACE if in_text else TEX_REPLACE - # return TEX_REPLACE[c] - return repl.get(c, c) - - text = TEX_REPLACE_RE.sub(replace, text) - text = text.replace("\n", "\\newline\n") - return text +def add_conversion_fn(cls, module_fn_name=None) -> None: + """Add to `format2fn` a mapping from a conversion type and builtin-class + to a conversion method. + + The conversion type is determined form the module name. + For example, in module mathics.format.render.svg the conversion + type is "svg". + + The conversion method is assumed to be a method in the caller's + module, and is derived from lowercasing `cls`. + + For example function arrowbox in module mathics.format.render.svg would be + the SVG conversion routine for class ArrowBox. + + We use frame introspection to get all of this done. + """ + fr = inspect.currentframe() + assert fr is not None + fr = fr.f_back + assert fr is not None + module_dict = fr.f_globals + + # The last part of the module name is expected to be the conversion routine. + conversion_type = module_dict["__name__"].split(".")[-1] + + # Derive the conversion function from the passed-in class argument, + # unless it is already set. + if module_fn_name is None: + module_fn_name = cls.__name__.lower() + elif hasattr(module_fn_name, "__name__"): + module_fn_name = module_fn_name.__name__ + + # Finally register the mapping: (Builtin-class, conversion name) -> conversion_function. + format2fn[(conversion_type, cls)] = module_dict[module_fn_name] def box_to_format(box, format: str, **options) -> str: # Maybe Union[str, bytearray] @@ -103,39 +99,3 @@ def ret_fn(box, elements=None, **opts): error_msg = f"Can't find formatter {format} for {type(self).__name__} ({self})" raise RuntimeError(error_msg) - - -def add_conversion_fn(cls, module_fn_name=None) -> None: - """Add to `format2fn` a mapping from a conversion type and builtin-class - to a conversion method. - - The conversion type is determined form the module name. - For example, in module mathics.format.render.svg the conversion - type is "svg". - - The conversion method is assumed to be a method in the caller's - module, and is derived from lowercasing `cls`. - - For example function arrowbox in module mathics.format.render.svg would be - the SVG conversion routine for class ArrowBox. - - We use frame introspection to get all of this done. - """ - fr = inspect.currentframe() - assert fr is not None - fr = fr.f_back - assert fr is not None - module_dict = fr.f_globals - - # The last part of the module name is expected to be the conversion routine. - conversion_type = module_dict["__name__"].split(".")[-1] - - # Derive the conversion function from the passed-in class argument, - # unless it is already set. - if module_fn_name is None: - module_fn_name = cls.__name__.lower() - elif hasattr(module_fn_name, "__name__"): - module_fn_name = module_fn_name.__name__ - - # Finally register the mapping: (Builtin-class, conversion name) -> conversion_function. - format2fn[(conversion_type, cls)] = module_dict[module_fn_name] diff --git a/mathics/format/render/latex.py b/mathics/format/render/latex.py index 5f152b55c..dba37bd8b 100644 --- a/mathics/format/render/latex.py +++ b/mathics/format/render/latex.py @@ -576,11 +576,28 @@ def pane_box(box: PaneBox, **options): add_conversion_fn(PaneBox, pane_box) +def rowbox_parenthesized(items, **options): + if len(items) < 2: + return None + key = ( + items[0], + items[-1], + ) + items = items[1:-1] + try: + bracket_data = BRACKET_INFO[key] + except KeyError: + return None + + contain = rowbox_sequence(items, **options) if len(items) > 0 else "" + + if any(item.is_multiline for item in items): + return f'{bracket_data["latex_open_large"]}{contain}{bracket_data["latex_closing_large"]}' + return f'{bracket_data["latex_open"]}{contain}{bracket_data["latex_closing"]}' + + def rowbox_sequence(items, **options): - parts_str = [ - lookup_conversion_method(element, "latex")(element, **options) - for element in items - ] + parts_str = [convert_box_to_format(element, **options) for element in items] if len(parts_str) == 0: return "" if len(parts_str) == 1: @@ -604,26 +621,6 @@ def rowbox_sequence(items, **options): return result -def rowbox_parenthesized(items, **options): - if len(items) < 2: - return None - key = ( - items[0], - items[-1], - ) - items = items[1:-1] - try: - bracket_data = BRACKET_INFO[key] - except KeyError: - return None - - contain = rowbox_sequence(items, **options) if len(items) > 0 else "" - - if any(item.is_multiline for item in items): - return f'{bracket_data["latex_open_large"]}{contain}{bracket_data["latex_closing_large"]}' - return f'{bracket_data["latex_open"]}{contain}{bracket_data["latex_closing"]}' - - def rowbox(box: RowBox, **options) -> str: # Note: values set in `options` take precedence over `box_options` child_options = {**box.box_options, **options} diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index e1d951838..eecc16514 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -58,7 +58,6 @@ add_conversion_fn, convert_box_to_format, convert_inner_box_field, - lookup_method as lookup_conversion_method, ) from mathics.core.load_builtin import display_operators_set as operators from mathics.core.symbols import SymbolFalse, SymbolTrue @@ -119,7 +118,7 @@ def graphics3dbox(box: Graphics3DBox, elements=None, **options) -> str: """Turn the Graphics3DBox into a MathML string""" indent_level = options.get("_indent_level", 0) indent_spaces = " " * indent_level - result = box.boxes_to_js(**options) + result = box.box_to_js(**options) result = ( f"{indent_spaces}\n" f"\n" @@ -156,7 +155,7 @@ def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: indent_level = options.get("_indent_level", 0) if indent_level: mathml = " " * indent_level + mathml - # print("boxes_to_mathml", mathml) + # print("box_to_mathml", mathml) return mathml @@ -164,7 +163,7 @@ def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: def gridbox(box: GridBox, elements=None, **super_options) -> str: - def boxes_to_mathml(box, **options): + def __box_to_mathml(box, **options): return lookup_conversion_method(box, "mathml")(box, **options) if not elements: @@ -216,27 +215,26 @@ def boxes_to_mathml(box, **options): def interpretation_box(box: InterpretationBox, **options): - boxes = box.inner_box origin = box.expr - child_options = options + child_options = {**options, **box.box_options} + box = box.inner_box if origin.has_form("InputForm", None): # InputForm produce outputs of the form # InterpretationBox[Style[_String, ...], origin_InputForm, opts___] - assert isinstance(boxes, StyleBox), f"boxes={boxes} are not a StyleBox" - boxes = boxes.inner_box - child_options = {**options} + assert isinstance(box, StyleBox), f"box={box} are not a StyleBox" + box = box.inner_box child_options["System`ShowStringCharacters"] = SymbolTrue - assert isinstance(boxes, String) + assert isinstance(box, String) elif origin.has_form("OutputForm", None): # OutputForm produce outputs of the form # InterpretationBox[PaneBox[_String, ...], origin_OutputForm, opts___] - assert boxes.has_form("PaneBox", 1, None) - boxes = boxes.inner_box - assert isinstance(boxes, String) + assert box.has_form("PaneBox", 1, None) + box = box.inner_box + assert isinstance(box, String) # Remove the outer quotes - boxes = String(boxes.value) + box = String(box.value) - return lookup_conversion_method(boxes, "mathml")(boxes, **child_options) + return convert_box_to_format(box, **child_options) add_conversion_fn(InterpretationBox, interpretation_box) @@ -405,8 +403,6 @@ def render(format, string): add_conversion_fn(String, string) -add_conversion_fn(StyleBox, convert_inner_box) - def subscriptbox(box: SubscriptBox, **options): # Note: values set in `options` take precedence over `box_options` @@ -415,8 +411,8 @@ def subscriptbox(box: SubscriptBox, **options): child_options = {**options, **box.box_options} child_options["_indent_level"] = indent_level + 1 return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( - lookup_conversion_method(box.base, "mathml")(box.base, **child_options), - lookup_conversion_method(box.subindex, "mathml")(box.subindex, **child_options), + convert_inner_box_field(box, "base", **child_options), + convert_inner_box_field(box, "subindex", **child_options), ) @@ -431,11 +427,9 @@ def subsuperscriptbox(box: SubsuperscriptBox, **options): child_options["_indent_level"] = indent_level + 1 box.base.inside_row = box.subindex.inside_row = box.superindex.inside_row = True return f"{indent_spaces}\n%s\n%s\n%s\n{indent_spaces}" % ( - lookup_conversion_method(box.base, "mathml")(box.base, **child_options), - lookup_conversion_method(box.subindex, "mathml")(box.subindex, **child_options), - lookup_conversion_method(box.superindex, "mathml")( - box.superindex, **child_options - ), + convert_inner_box_field(box, "base", **child_options), + convert_inner_box_field(box, "subindex", **child_options), + convert_inner_box_field(box, "superindex", **child_options), ) @@ -449,12 +443,11 @@ def superscriptbox(box: SuperscriptBox, **options): child_options = {**options, **box.box_options} child_options["_indent_level"] = indent_level + 1 return f"{indent_spaces}\n%s\n%s\n{indent_spaces}" % ( - lookup_conversion_method(box.base, "mathml")(box.base, **child_options), - lookup_conversion_method(box.superindex, "mathml")( - box.superindex, **child_options - ), + convert_inner_box_field(box, "base", **child_options), + convert_inner_box_field(box, "superindex", **child_options), ) add_conversion_fn(SuperscriptBox, superscriptbox) +add_conversion_fn(StyleBox, convert_inner_box) add_conversion_fn(TagBox, convert_inner_box) From a07cac9d1c653b42d7bf25c32b06e13bbf943a89 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 6 Feb 2026 18:28:07 -0300 Subject: [PATCH 24/31] removing unused inner function --- mathics/format/render/mathml.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index eecc16514..9640a83ce 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -163,9 +163,6 @@ def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: def gridbox(box: GridBox, elements=None, **super_options) -> str: - def __box_to_mathml(box, **options): - return lookup_conversion_method(box, "mathml")(box, **options) - if not elements: elements = box._elements evaluation = super_options.get("evaluation") From e5207214a95ca9f2b3f8d55066f51140608474cc Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 6 Feb 2026 18:37:15 -0300 Subject: [PATCH 25/31] ruff --- test/builtin/box/test_custom_boxexpression.py | 2 +- test/builtin/test_system.py | 1 - test/format/test_boxexpressions.py | 2 +- test/format/test_svg.py | 2 -- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/test/builtin/box/test_custom_boxexpression.py b/test/builtin/box/test_custom_boxexpression.py index aaac0e8d2..c33447cea 100644 --- a/test/builtin/box/test_custom_boxexpression.py +++ b/test/builtin/box/test_custom_boxexpression.py @@ -6,7 +6,7 @@ from mathics.core.builtin import Predefined from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.rules import BaseRule, FunctionApplyRule, Rule +from mathics.core.rules import FunctionApplyRule from mathics.core.symbols import Symbol SymbolCustomGraphicsBox = Symbol("CustomGraphicsBox") diff --git a/test/builtin/test_system.py b/test/builtin/test_system.py index 9868cba7f..0008f2615 100644 --- a/test/builtin/test_system.py +++ b/test/builtin/test_system.py @@ -4,7 +4,6 @@ """ -import os from test.helper import check_evaluation import pytest diff --git a/test/format/test_boxexpressions.py b/test/format/test_boxexpressions.py index 53f4ff6a8..4f4597fd2 100644 --- a/test/format/test_boxexpressions.py +++ b/test/format/test_boxexpressions.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from test.helper import check_evaluation, session +from test.helper import session import pytest diff --git a/test/format/test_svg.py b/test/format/test_svg.py index 395b6cead..0665a2008 100644 --- a/test/format/test_svg.py +++ b/test/format/test_svg.py @@ -57,8 +57,6 @@ def extract_svg_body(svg): def get_svg(expression): - from mathics.format.box.graphics import prepare_elements as prepare_elements2d - options = {} boxes = MakeBoxes(expression).evaluate(evaluation) format_fn = lookup_method(boxes, "svg") From 684e47449f220c1e1212e3ac7af81b85b5866618 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Fri, 6 Feb 2026 18:39:55 -0300 Subject: [PATCH 26/31] ruff --- mathics/doc/doc_entries.py | 1 - test/builtin/box/test_custom_boxexpression.py | 2 +- test/builtin/test_system.py | 1 - test/format/test_svg.py | 2 -- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/mathics/doc/doc_entries.py b/mathics/doc/doc_entries.py index a16f2acf2..11b777947 100644 --- a/mathics/doc/doc_entries.py +++ b/mathics/doc/doc_entries.py @@ -10,7 +10,6 @@ import logging import re from abc import ABC -from os import getenv from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Sequence, Tuple from mathics.core.evaluation import Message, Print, _Out diff --git a/test/builtin/box/test_custom_boxexpression.py b/test/builtin/box/test_custom_boxexpression.py index aaac0e8d2..c33447cea 100644 --- a/test/builtin/box/test_custom_boxexpression.py +++ b/test/builtin/box/test_custom_boxexpression.py @@ -6,7 +6,7 @@ from mathics.core.builtin import Predefined from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.rules import BaseRule, FunctionApplyRule, Rule +from mathics.core.rules import FunctionApplyRule from mathics.core.symbols import Symbol SymbolCustomGraphicsBox = Symbol("CustomGraphicsBox") diff --git a/test/builtin/test_system.py b/test/builtin/test_system.py index 9868cba7f..0008f2615 100644 --- a/test/builtin/test_system.py +++ b/test/builtin/test_system.py @@ -4,7 +4,6 @@ """ -import os from test.helper import check_evaluation import pytest diff --git a/test/format/test_svg.py b/test/format/test_svg.py index 395b6cead..0665a2008 100644 --- a/test/format/test_svg.py +++ b/test/format/test_svg.py @@ -57,8 +57,6 @@ def extract_svg_body(svg): def get_svg(expression): - from mathics.format.box.graphics import prepare_elements as prepare_elements2d - options = {} boxes = MakeBoxes(expression).evaluate(evaluation) format_fn = lookup_method(boxes, "svg") From 351eee1e41775d9c6623f16eadd82a148a563c23 Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 7 Feb 2026 10:07:02 -0300 Subject: [PATCH 27/31] Update mathics/format/render/mathml.py Co-authored-by: R. Bernstein --- mathics/format/render/mathml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 29d64e6a4..21a0da46c 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -345,7 +345,7 @@ def render(format, string): # Mathics-Django: if text == "": return "" - if text == "\u2146": # DifferentialD + if text == named_characters["DifferentialD"] return render( '%s', text ) From 18abb932f325d8d4efdba74ed3652acbb0d11b7f Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 7 Feb 2026 10:07:22 -0300 Subject: [PATCH 28/31] Update mathics/format/render/mathml.py Co-authored-by: R. Bernstein --- mathics/format/render/mathml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 21a0da46c..42439d7a6 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -349,7 +349,7 @@ def render(format, string): return render( '%s', text ) - if text == "\u2062": # InvisibleTimes + if text == named_characters["InvisibleTimes"] return render( '%s', text ) From 8e17b7449979cec4bca76e2c193d120fceb0b438 Mon Sep 17 00:00:00 2001 From: rocky Date: Sat, 7 Feb 2026 09:36:35 -0500 Subject: [PATCH 29/31] fix syntactic typo --- mathics/format/render/mathml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 42439d7a6..1ac643d62 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -345,11 +345,11 @@ def render(format, string): # Mathics-Django: if text == "": return "" - if text == named_characters["DifferentialD"] + if text == named_characters["DifferentialD"]: return render( '%s', text ) - if text == named_characters["InvisibleTimes"] + if text == named_characters["InvisibleTimes"]: return render( '%s', text ) From 42c6226248c42e44237edd332cd6d657e75f9844 Mon Sep 17 00:00:00 2001 From: "R. Bernstein" Date: Sat, 7 Feb 2026 10:11:54 -0500 Subject: [PATCH 30/31] Apply suggestion from @rocky This code does not follow the comment about how this turns into invisible space. When this was \u2146, that might have been true, but we use for DifferentialD the value U0001D451 --- mathics/format/render/mathml.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index 1ac643d62..aaea565f5 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -345,10 +345,6 @@ def render(format, string): # Mathics-Django: if text == "": return "" - if text == named_characters["DifferentialD"]: - return render( - '%s', text - ) if text == named_characters["InvisibleTimes"]: return render( '%s', text From 0d4c08c38173812a730a5d4a96a378dcd44bab4c Mon Sep 17 00:00:00 2001 From: Juan Mauricio Matera Date: Sat, 7 Feb 2026 14:31:31 -0300 Subject: [PATCH 31/31] Update mathics/format/render/mathml.py Co-authored-by: R. Bernstein --- mathics/format/render/mathml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index c3da620ff..2c1d5f004 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -217,7 +217,7 @@ def interpretation_box(box: InterpretationBox, **options): if origin.has_form("InputForm", None): # InputForm produce outputs of the form # InterpretationBox[Style[_String, ...], origin_InputForm, opts___] - assert isinstance(box, StyleBox), f"box={box} are not a StyleBox" + assert isinstance(box, StyleBox), f"box={box} is not a StyleBox" box = box.inner_box child_options["System`ShowStringCharacters"] = SymbolTrue assert isinstance(box, String)