diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index c26b66080..924e83314 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -14,6 +14,7 @@ Integer1, Integer3, Integer310, + Integer400, IntegerM1, Number, Rational, @@ -49,6 +50,7 @@ SymbolNull, SymbolPower, SymbolTimes, + SymbolTrue, ) from mathics.core.systemsymbols import ( SymbolBlank, @@ -153,7 +155,7 @@ class Divide(InfixOperator): default_formats = False formats = { - (("InputForm", "OutputForm"), "Divide[x_, y_]"): ( + ("InputForm", "Divide[x_, y_]"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]' ), } @@ -169,6 +171,24 @@ class Divide(InfixOperator): summary_text = "divide a number" + def format_outputform(self, x, y, evaluation): + "(OutputForm,): Divide[x_, y_]" + use_2d = ( + evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace + is SymbolTrue + ) + if not use_2d: + return Expression( + SymbolInfix, + ListExpression( + Expression(SymbolHoldForm, x), Expression(SymbolHoldForm, y) + ), + String("/"), + Integer400, + SymbolLeft, + ) + return None + class Minus(PrefixOperator): """ @@ -402,10 +422,21 @@ class Power(InfixOperator, MPMathFunction): Expression(SymbolPattern, Symbol("x"), Expression(SymbolBlank)), RationalOneHalf, ): "HoldForm[Sqrt[x]]", - (("InputForm", "OutputForm"), "x_ ^ y_"): ( + (("InputForm",), "x_ ^ y_"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]' ), - ("", "x_ ^ y_"): ( + (("OutputForm",), "x_ ^ y_"): ( + "If[$Use2DOutputForm, " + "Superscript[HoldForm[x], HoldForm[y]], " + 'Infix[{HoldForm[x], HoldForm[y]}, "^", 590, Right]]' + ), + ( + ( + "StandardForm", + "TraditionalForm", + ), + "x_ ^ y_", + ): ( "PrecedenceForm[Superscript[PrecedenceForm[HoldForm[x], 590]," " HoldForm[y]], 590]" ), @@ -430,6 +461,7 @@ class Power(InfixOperator, MPMathFunction): rules = { "Power[]": "1", "Power[x_]": "x", + "MakeBoxes[x_^y_, fmt_]": "SuperscriptBox[MakeBoxes[x, fmt],MakeBoxes[y, fmt]]", } summary_text = "exponentiate a number" diff --git a/mathics/builtin/box/expression.py b/mathics/builtin/box/expression.py index 142b1a533..4c6a69831 100644 --- a/mathics/builtin/box/expression.py +++ b/mathics/builtin/box/expression.py @@ -81,6 +81,12 @@ def __new__(cls, *elements, **kwargs): instance._elements = tuple(elements) return instance + def __repr__(self): + result = str(type(self)) + for elem in self._elements: + result += " * " + repr(elem) + return result + def do_format(self, evaluation, format): return self diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 680215b27..97650571c 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -188,6 +188,33 @@ def eval_display(boxexpr, evaluation): return boxexpr.elements[0] +class PaneBox(BoxExpression): + """ + + :WMA link: + https://reference.wolfram.com/language/ref/InterpretationBox.html + +
+
'PaneBox[expr]' +
is a low-level box construct, used in OutputForm. +
+ + """ + + attributes = A_HOLD_ALL_COMPLETE | A_PROTECTED | A_READ_PROTECTED + summary_text = "box associated to panel" + + def apply_display(boxexpr, evaluation, expression): + """ToExpression[boxexpr_PaneBox, form_]""" + return Expression(expression.head, boxexpr.elements[0], form).evaluate( + evaluation + ) + + def apply_display(boxexpr, evaluation): + """DisplayForm[boxexpr_PaneBox]""" + return boxexpr.elements[0] + + class RowBox(BoxExpression): """ diff --git a/mathics/builtin/forms/output.py b/mathics/builtin/forms/output.py index 021440fc4..3d7436de7 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -14,7 +14,13 @@ """ from typing import Optional -from mathics.builtin.box.layout import RowBox +from mathics.builtin.box.layout import ( + GridBox, + InterpretationBox, + PaneBox, + RowBox, + to_boxes, +) from mathics.builtin.forms.base import FormBaseClass from mathics.core.atoms import Integer, Real, String, StringFromPython from mathics.core.builtin import Builtin @@ -28,8 +34,11 @@ SymbolInfinity, SymbolMakeBoxes, SymbolNumberForm, + SymbolOutputForm, SymbolRowBox, SymbolRuleDelayed, + SymbolStandardForm, + SymbolSubscriptBox, SymbolSuperscriptBox, ) from mathics.eval.makeboxes import ( @@ -40,7 +49,10 @@ eval_mathmlform, eval_tableform, eval_texform, + format_element, ) +from mathics.eval.testing_expressions import expr_min +from mathics.format.prettyprint import expression_to_2d_text class BaseForm(FormBaseClass): @@ -490,8 +502,18 @@ class OutputForm(FormBaseClass): = -Graphics- """ + formats = {"OutputForm[s_String]": "s"} summary_text = "plain-text output format" + def eval_makeboxes(self, expr, form, evaluation): + """MakeBoxes[OutputForm[expr_], form_]""" + print(" eval Makeboxes outputform") + text2d = expression_to_2d_text(expr, evaluation, form).text + elem1 = PaneBox(String(text2d)) + elem2 = Expression(SymbolOutputForm, expr) + result = InterpretationBox(elem1, elem2) + return result + class PythonForm(FormBaseClass): """ diff --git a/mathics/builtin/forms/variables.py b/mathics/builtin/forms/variables.py index 853f904d0..11d24bf84 100644 --- a/mathics/builtin/forms/variables.py +++ b/mathics/builtin/forms/variables.py @@ -3,11 +3,47 @@ """ -from mathics.core.attributes import A_LOCKED, A_PROTECTED +from mathics.core.attributes import A_LOCKED, A_NO_ATTRIBUTES, A_PROTECTED from mathics.core.builtin import Predefined from mathics.core.list import ListExpression +class Use2DOutputForm_(Predefined): + r""" +
+
'$Use2DOutputForm' +
internal variable that controls if 'OutputForm[expr]' is shown \ + in one line (standard Mathics behavior) or \ + or in a prettyform-like multiline output (the standard way in WMA). + The default value is 'False', keeping the standard Mathics behavior. +
+ + >> $Use2DOutputForm + = False + >> OutputForm[a^b] + = a ^ b + >> $Use2DOutputForm = True; OutputForm[a ^ b] + = + . b + . a + + Notice that without the 'OutputForm' wrapper, we fall back to the normal + behavior: + >> a ^ b + = Superscript[a, b] + Setting the variable back to False go back to the normal behavior: + >> $Use2DOutputForm = False; OutputForm[a ^ b] + = a ^ b + """ + + attributes = A_NO_ATTRIBUTES + name = "$Use2DOutputForm" + rules = { + "$Use2DOutputForm": "False", + } + summary_text = "use the 2D OutputForm" + + class PrintForms_(Predefined): r"""
diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index cfcdf65c1..3691d48ac 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -110,7 +110,7 @@ class MakeBoxes(Builtin): def eval_general(self, expr, f, evaluation): """MakeBoxes[expr_, f:TraditionalForm|StandardForm|OutputForm|InputForm|FullForm]""" - return eval_generic_makeboxes(self, expr, f, evaluation) + return eval_generic_makeboxes(expr, f, evaluation) def eval_outerprecedenceform(self, expr, precedence, form, evaluation): """MakeBoxes[PrecedenceForm[expr_, precedence_], diff --git a/mathics/core/atoms.py b/mathics/core/atoms.py index 03e99a523..46727b67c 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -356,6 +356,7 @@ def user_hash(self, update): Integer3 = Integer(3) Integer4 = Integer(4) Integer310 = Integer(310) +Integer400 = Integer(400) Integer10 = Integer(10) IntegerM1 = Integer(-1) diff --git a/mathics/eval/makeboxes/__init__.py b/mathics/eval/makeboxes/__init__.py index a6cb2a6e7..ebec6618a 100644 --- a/mathics/eval/makeboxes/__init__.py +++ b/mathics/eval/makeboxes/__init__.py @@ -18,7 +18,11 @@ eval_tableform, eval_texform, ) -from mathics.eval.makeboxes.precedence import builtins_precedence, parenthesize +from mathics.eval.makeboxes.precedence import ( + builtins_precedence, + compare_precedence, + parenthesize, +) __all__ = [ "NumberForm_to_String", diff --git a/mathics/eval/makeboxes/formatvalues.py b/mathics/eval/makeboxes/formatvalues.py index d6d4b0ded..185b6e802 100644 --- a/mathics/eval/makeboxes/formatvalues.py +++ b/mathics/eval/makeboxes/formatvalues.py @@ -29,8 +29,25 @@ SymbolRepeated, SymbolRepeatedNull, SymbolTimes, + SymbolTrue, ) -from mathics.core.systemsymbols import SymbolComplex, SymbolMinus, SymbolRational +from mathics.core.systemsymbols import ( + SymbolComplex, + SymbolMinus, + SymbolOutputForm, + SymbolRational, + SymbolStandardForm, +) + +# An operator precedence value that will ensure that whatever operator +# this is attached to does not have parenthesis surrounding it. +# Operator precedence values are integers; If if an operator +# "op" is greater than the surrounding precedence, then "op" +# will be surrounded by parenthesis, e.g. ... (...op...) ... +# In named-characters.yml of mathics-scanner we start at 0. +# However, negative values would also work. +NEVER_ADD_PARENTHESIS = 0 + # These Strings are used in Boxing output StringElipsis = String("...") @@ -47,6 +64,170 @@ ] = {} +# this temporarily replaces the _BoxedString class +def _boxed_string(string: str, **options): + from mathics.builtin.box.layout import StyleBox + + return StyleBox(String(string), **options) + + +def compare_precedence( + element: BaseElement, precedence: Optional[int] = None +) -> Optional[int]: + """ + compare the precedence of the element regarding a precedence value. + If both precedences are equal, return 0. If precedence of the + first element is higher, return 1, otherwise -1. + If precedences cannot be compared, return None. + """ + while element.has_form("HoldForm", 1): + element = element.elements[0] + + if precedence is None: + return None + if element.has_form(("Infix", "Prefix", "Postfix"), 3, None): + element_prec = element.elements[2].value + elif element.has_form("PrecedenceForm", 2): + element_prec = element.elements[1].value + # For negative values, ensure that the element_precedence is at least the precedence. (Fixes #332) + elif isinstance(element, (Integer, Real)) and element.value < 0: + element_prec = precedence + else: + element_prec = builtins_precedence.get(element.get_head_name()) + + if element_prec is None: + return None + return 0 if element_prec == precedence else (1 if element_prec > precedence else -1) + + +# 640 = sys.int_info.str_digits_check_threshold. +# Someday when 3.11 is the minimum version of Python supported, +# we can replace the magic value 640 below with sys.int.str_digits_check_threshold. +def int_to_string_shorter_repr(value: int, form: Symbol, max_digits=640): + """Convert value to a String, restricted to max_digits characters. + + if value has an n-digit decimal representation, + value = d_1 *10^{n-1} d_2 * 10^{n-2} + d_3 10^{n-3} + ..... + + d_{n-2}*100 +d_{n-1}*10 + d_{n} + is represented as the string + + "d_1d_2d_3...d_{k}<>d_{n-k-1}...d_{n-2}d_{n-1}d_{n}" + + where n-2k digits are replaced by a placeholder. + """ + if max_digits == 0: + return String(str(value)) + + # Normalize to positive quantities + is_negative = value < 0 + if is_negative: + value = -value + max_digits = max_digits - 1 + + # Estimate the number of decimal digits + num_digits = int(value.bit_length() * 0.3) + + # If the estimated number is below the threshold, + # return it as it is. + if num_digits <= max_digits: + if is_negative: + return String("-" + str(value)) + return String(str(value)) + + # estimate the size of the placeholder + size_placeholder = len(str(num_digits)) + 6 + # Estimate the number of available decimal places + avaliable_digits = max(max_digits - size_placeholder, 0) + # how many most significative digits include + len_msd = (avaliable_digits + 1) // 2 + # how many least significative digits to include: + len_lsd = avaliable_digits - len_msd + # Compute the msd. + msd = str(value // 10 ** (num_digits - len_msd)) + if msd == "0": + msd = "" + + # If msd has more digits than the expected, it means that + # num_digits was wrong. + extra_msd_digits = len(msd) - len_msd + if extra_msd_digits > 0: + # Remove the extra digit and fix the real + # number of digits. + msd = msd[:len_msd] + num_digits = num_digits + 1 + + lsd = "" + if len_lsd > 0: + lsd = str(value % 10 ** (len_lsd)) + # complete decimal positions in the lsd: + lsd = (len_lsd - len(lsd)) * "0" + lsd + + # Now, compute the true number of hiding + # decimal places, and built the placeholder + remaining = num_digits - len_lsd - len_msd + placeholder = f" <<{remaining}>> " + # Check if the shorten string is actually + # shorter than the full string representation: + if len(placeholder) < remaining: + value_str = f"{msd}{placeholder}{lsd}" + else: + value_str = str(value) + + if is_negative: + value_str = "-" + value_str + return String(value_str) + + +def eval_fullform_makeboxes( + self, expr, evaluation: Evaluation, form=SymbolStandardForm +) -> Optional[BaseElement]: + """ + This function takes the definitions provided by the evaluation + object, and produces a boxed form for expr. + + Basically: MakeBoxes[expr // FullForm] + """ + # This is going to be reimplemented. + expr = Expression(SymbolFullForm, expr) + return Expression(SymbolMakeBoxes, expr, form).evaluate(evaluation) + + +def eval_makeboxes( + expr, evaluation: Evaluation, form=SymbolStandardForm +) -> Optional[BaseElement]: + """ + This function takes the definitions provided by the evaluation + object, and produces a boxed fullform for expr. + + Basically: MakeBoxes[expr // form] + """ + # This is going to be reimplemented. + return Expression(SymbolMakeBoxes, expr, form).evaluate(evaluation) + + +def make_output_form(expr, evaluation, form): + """ + Build a 2D text representation of the expression. + """ + from mathics.builtin.box.layout import InterpretationBox, PaneBox + from mathics.format.prettyprint import expression_to_2d_text + + use_2d = ( + evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace + is SymbolTrue + ) + text2d = expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d}).text + + if "\n" in text2d: + text2d = "\n" + text2d + elem1 = PaneBox(String(text2d)) + elem2 = Expression(SymbolOutputForm, expr) + return InterpretationBox(elem1, elem2) + + +# do_format_* + + def do_format( element: BaseElement, evaluation: Evaluation, form: Symbol ) -> Optional[BaseElement]: diff --git a/mathics/eval/makeboxes/makeboxes.py b/mathics/eval/makeboxes/makeboxes.py index 47038d1a0..fb5fc0baf 100644 --- a/mathics/eval/makeboxes/makeboxes.py +++ b/mathics/eval/makeboxes/makeboxes.py @@ -14,7 +14,7 @@ from mathics.core.expression import Expression from mathics.core.symbols import Atom, Symbol, SymbolFullForm, SymbolMakeBoxes from mathics.core.systemsymbols import SymbolStandardForm -from mathics.eval.makeboxes.formatvalues import do_format +from mathics.eval.makeboxes.formatvalues import do_format, make_output_form from mathics.eval.makeboxes.precedence import parenthesize @@ -140,7 +140,7 @@ def eval_fullform_makeboxes( return Expression(SymbolMakeBoxes, expr, form).evaluate(evaluation) -def eval_generic_makeboxes(self, expr, f, evaluation): +def eval_generic_makeboxes(expr, f, evaluation): """MakeBoxes[expr_, f:TraditionalForm|StandardForm|OutputForm|InputForm|FullForm]""" from mathics.builtin.box.layout import RowBox @@ -215,11 +215,17 @@ def eval_makeboxes( def format_element( element: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs -) -> Optional[Union[BoxElementMixin, BaseElement]]: +) -> Optional[BaseElement]: """ Applies formats associated to the expression, and then calls Makeboxes """ evaluation.is_boxing = True + if element.has_form("FullForm", 1): + return eval_generic_makeboxes(element.elements[0], element.head, evaluation) + + if element.has_form("OutputForm", 1): + return make_output_form(element.elements[0], evaluation, form) + expr = do_format(element, evaluation, form) if expr is None: return None diff --git a/mathics/eval/makeboxes/outputforms.py b/mathics/eval/makeboxes/outputforms.py index 38bebabff..ee2b6aafd 100644 --- a/mathics/eval/makeboxes/outputforms.py +++ b/mathics/eval/makeboxes/outputforms.py @@ -4,7 +4,7 @@ from mathics.core.expression import BoxError, Expression from mathics.core.list import ListExpression from mathics.core.symbols import SymbolFullForm, SymbolList -from mathics.core.systemsymbols import SymbolMakeBoxes, SymbolRowBox +from mathics.core.systemsymbols import SymbolMakeBoxes, SymbolRowBox, SymbolStandardForm from mathics.eval.makeboxes.makeboxes import format_element from mathics.eval.testing_expressions import expr_min diff --git a/mathics/format/latex.py b/mathics/format/latex.py index 86218e1c3..e1cf20444 100644 --- a/mathics/format/latex.py +++ b/mathics/format/latex.py @@ -19,6 +19,8 @@ from mathics.builtin.box.layout import ( FractionBox, GridBox, + InterpretationBox, + PaneBox, RowBox, SqrtBox, StyleBox, @@ -133,6 +135,16 @@ def render(format, string, in_text=False): add_conversion_fn(String, string) +def interpretation_panebox(self, **options): + return lookup_conversion_method(self.elements[0], "latex")( + self.elements[0], **options + ) + + +add_conversion_fn(InterpretationBox, interpretation_panebox) +add_conversion_fn(PaneBox, interpretation_panebox) + + def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) diff --git a/mathics/format/mathml.py b/mathics/format/mathml.py index 7bbdd65da..d8ac94922 100644 --- a/mathics/format/mathml.py +++ b/mathics/format/mathml.py @@ -15,6 +15,8 @@ from mathics.builtin.box.layout import ( FractionBox, GridBox, + InterpretationBox, + PaneBox, RowBox, SqrtBox, StyleBox, @@ -110,6 +112,16 @@ def render(format, string): add_conversion_fn(String, string) +def interpretation_panebox(self, **options): + return lookup_conversion_method(self.elements[0], "latex")( + self.elements[0], **options + ) + + +add_conversion_fn(InterpretationBox, interpretation_panebox) +add_conversion_fn(PaneBox, interpretation_panebox) + + def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) diff --git a/mathics/format/pane_text.py b/mathics/format/pane_text.py new file mode 100644 index 000000000..d8b693530 --- /dev/null +++ b/mathics/format/pane_text.py @@ -0,0 +1,465 @@ +""" +This module produces a "pretty-print" inspired 2d text representation. + +This code is completely independent from Mathics objects, so it could live +alone in a different package. +""" + +from typing import List, Optional, Union + + +class TextBlock: + lines: List[str] + width: int + height: int + base: int + + @staticmethod + def _build_attributes(lines, width=0, height=0, base=0): + width = max(width, max(len(line) for line in lines)) if lines else 0 + + # complete lines: + lines = [ + line if len(line) == width else (line + (width - len(line)) * " ") + for line in lines + ] + + if base < 0: + height = height - base + empty_line = width * " " + lines = (-base) * [empty_line] + lines + base = -base + if height > len(lines): + empty_line = width * " " + lines = lines + (height - len(lines)) * [empty_line] + else: + height = len(lines) + + return (lines, width, height, base) + + def __init__(self, text, padding=0, base=0, height=1, width=0): + if isinstance(text, str): + if text == "": + lines = [] + else: + lines = text.split("\n") + else: + lines = sum((line.split("\n") for line in text), []) + if padding: + padding_spaces = padding * " " + lines = [padding_spaces + line.replace("\t", " ") for line in lines] + else: + lines = [line.replace("\t", " ") for line in lines] + + self.lines, self.width, self.height, self.base = self._build_attributes( + lines, width, height, base + ) + + @property + def text(self): + return "\n".join(self.lines) + + @text.setter + def text(self, value): + raise TypeError("TextBlock is inmutable") + + def __repr__(self): + return self.text + + def __add__(self, tb): + result = TextBlock("") + result += self + result += tb + return result + + def __iadd__(self, tb): + """In-place addition""" + if isinstance(tb, str): + tb = TextBlock(tb) + base = self.base + other_base = tb.base + left_lines = self.lines + right_lines = tb.lines + offset = other_base - base + if offset > 0: + left_lines = left_lines + offset * [self.width * " "] + base = other_base + elif offset < 0: + offset = -offset + right_lines = right_lines + offset * [tb.width * " "] + + offset = len(right_lines) - len(left_lines) + if offset > 0: + left_lines = offset * [self.width * " "] + left_lines + elif offset < 0: + right_lines = (-offset) * [tb.width * " "] + right_lines + + return TextBlock( + list(left + right for left, right in zip(left_lines, right_lines)), + base=base, + ) + + def ajust_base(self, base: int): + """ + if base is larger than self.base, + adds lines at the bottom of the text + and update self.base + """ + if base > self.base: + diff = base - self.base + result = TextBlock( + self.lines + diff * [" "], self.width, self.height, self.base + ) + + return result + + def ajust_width(self, width: int, align: str = "c"): + def padding(lines, diff): + if diff > 0: + if align == "c": + left_pad = int(diff / 2) + right_pad = diff - left_pad + lines = [ + (left_pad * " " + line + right_pad * " ") for line in lines + ] + elif align == "r": + lines = [(diff * " " + line) for line in lines] + else: + lines = [(line + diff * " ") for line in lines] + return lines + + diff_width = width - self.width + if diff_width <= 0: + return self + + new_lines = padding(self.lines, diff_width) + return TextBlock(new_lines, base=self.base) + + def box(self): + top = "+" + self.width * "-" + "+" + out = "\n".join("|" + line + "|" for line in self.lines) + out = top + "\n" + out + "\n" + top + return TextBlock(out, self.base + 1) + + def join(self, iterable): + result = TextBlock("") + for i, item in enumerate(iterable): + if i == 0: + result = item + else: + result = result + self + item + return result + + def stack(self, top, align: str = "c"): + if isinstance(top, str): + top = TextBlock(top) + + bottom = self + bottom_width, top_width = bottom.width, top.width + + if bottom_width > top_width: + top = top.ajust_width(bottom_width, align=align) + elif bottom_width < top_width: + bottom = bottom.ajust_width(top_width, align=align) + + return TextBlock(top.lines + bottom.lines, base=self.base) # type: ignore[union-attr] + + +def _draw_integral_symbol(height: int) -> TextBlock: + return TextBlock( + (" /+ \n" + "\n".join(height * [" | "]) + "\n+/ "), base=int((height + 1) / 2) + ) + + +def bracket(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("["), TextBlock("]") + else: + left_br = TextBlock( + "+-\n" + "\n".join((height) * ["| "]) + "\n+-", base=inner.base + 1 + ) + right_br = TextBlock( + "-+ \n" + "\n".join((height) * [" |"]) + "\n-+", base=inner.base + 1 + ) + return left_br + inner + right_br + + +def curly_braces(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("{"), TextBlock("}") + else: + half_height = max(1, int((height - 3) / 2)) + half_line = "\n".join(half_height * [" |"]) + left_br = TextBlock( + "\n".join([" /", half_line, "< ", half_line, " \\"]), base=half_height + 1 + ) + half_line = "\n".join(half_height * ["| "]) + right_br = TextBlock( + "\n".join(["\\ ", half_line, " >", half_line, "/ "]), base=half_height + 1 + ) + + return left_br + inner + right_br + + +def draw_vertical( + pen: str, height, base=0, left_padding=0, right_padding=0 +) -> TextBlock: + """ + build a TextBlock with a vertical line of height `height` + using the string `pen`. If paddings are given, + spaces are added to the sides. + For example, `draw_vertical("=", 3)` produces + TextBlock(("=\n" + "=\n" + "=", base=base + ) + """ + pen = (left_padding * " ") + str(pen) + (right_padding * " ") + return TextBlock("\n".join(height * [pen]), base=base) + + +def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: + """ + A TextBlock representation of + a Fraction + """ + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + width = max(b.width, a.width) + frac_bar = TextBlock(width * "-") + result = frac_bar.stack(a) + result = b.stack(result) + result.base = b.height + return result + + +def grid(items: list, **options) -> TextBlock: + """ + Process items and build a TextBlock + """ + result: TextBlock = TextBlock("") + + if not items: + return result + + # Ensure that items is a list + items = list(items) + # Ensure that all are TextBlock or list + items = [TextBlock(item) if isinstance(item, str) else item for item in items] + + # options + col_border = options.get("col_border", False) + row_border = options.get("row_border", False) + + # normalize widths: + widths: list = [1] + try: + widths = [1] * max( + len(item) for item in items if isinstance(item, (tuple, list)) + ) + except ValueError: + pass + + full_width: int = 0 + for row in items: + if isinstance(row, TextBlock): + full_width = max(full_width, row.width) + else: + for index, item in enumerate(row): + widths[index] = max(widths[index], item.width) + + total_width: int = sum(widths) + max(0, len(widths) - 1) * 3 + + if full_width > total_width: + widths[-1] = widths[-1] + full_width - total_width + total_width = full_width + + # Set the borders + + if row_border: + if col_border: + interline = TextBlock("+" + "+".join((w + 2) * "-" for w in widths) + "+") + else: + interline = TextBlock((sum(w + 3 for w in widths) - 2) * "-") + full_width = interline.width - 4 + else: + if col_border: + interline = ( + TextBlock("|") + + TextBlock("|".join((w + 2) * " " for w in widths)) + + TextBlock("|") + ) + full_width = max(0, interline.width - 4) + else: + interline = TextBlock((sum(w + 3 for w in widths) - 3) * " ") + full_width = max(0, interline.width - 4) + + def normalize_widths(row): + if isinstance(row, TextBlock): + return [row.ajust_width(max(0, full_width), align="l")] + return [item.ajust_width(widths[i]) for i, item in enumerate(row)] + + items = [normalize_widths(row) for row in items] + + if col_border: + for i, row in enumerate(items): + row_height: int = max(item.height for item in row) + row_base: int = max(item.base for item in row) + col_sep = draw_vertical( + "|", height=row_height, base=row_base, left_padding=1, right_padding=1 + ) + + new_row_txt = col_sep.join(row) + new_row_txt = ( + draw_vertical("|", row_height, base=row_base, right_padding=1) + + new_row_txt + + draw_vertical("|", row_height, base=row_base, left_padding=1) + ) + if i == 0: + if row_border: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt + else: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt.stack(result, align="l") + else: + for i, row in enumerate(items): + new_row_txt = TextBlock(" ").join(row) + if i == 0: + if row_border: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt + else: + new_row_txt = new_row_txt.stack(interline, align="l") + result = new_row_txt.stack(result, align="l") + + if row_border: + result = interline.stack(result, align="l") + + result.base = int(result.height / 2) + return result + + +def integral_indefinite( + integrand: Union[TextBlock, str], var: Union[TextBlock, str] +) -> TextBlock: + # TODO: handle list of vars + # TODO: use utf as an option + if isinstance(var, str): + var = TextBlock(var) + + if isinstance(integrand, str): + integrand = TextBlock(integrand) + + int_symb: TextBlock = _draw_integral_symbol(integrand.height) + return int_symb + integrand + " d" + var + + +def integral_definite( + integrand: Union[TextBlock, str], + var: Union[TextBlock, str], + a: Union[TextBlock, str], + b: Union[TextBlock, str], +) -> TextBlock: + # TODO: handle list of vars + # TODO: use utf as an option + if isinstance(var, str): + var = TextBlock(var) + if isinstance(integrand, str): + integrand = TextBlock(integrand) + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + + int_symb = _draw_integral_symbol(integrand.height) + return subsuperscript(int_symb, a, b) + " " + integrand + " d" + var + + +def parenthesize(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + height = inner.height + if height == 1: + left_br, right_br = TextBlock("("), TextBlock(")") + else: + left_br = TextBlock( + "/ \n" + "\n".join((height - 2) * ["| "]) + "\n\\ ", base=inner.base + ) + right_br = TextBlock( + " \\ \n" + "\n".join((height - 2) * [" |"]) + "\n /", base=inner.base + ) + return left_br + inner + right_br + + +def sqrt_block( + a: Union[TextBlock, str], index: Optional[Union[TextBlock, str]] = None +) -> TextBlock: + """ + Sqrt Text Block + """ + if isinstance(a, str): + a = TextBlock(a) + if isinstance(index, str): + index = TextBlock(index) + + a_height = a.height + result_2 = TextBlock( + "\n".join("|" + line for line in a.text.split("\n")), base=a.base + ) + result_2 = result_2.stack((a.width + 1) * "_", align="l") + half_height = int(a_height / 2 + 1) + + result_1 = TextBlock( + "\n".join( + [ + (int(i) * " " + "\\" + int((half_height - i - 1)) * " ") + for i in range(half_height) + ] + ), + base=a.base, + ) + if index is not None: + result_1 = result_1.stack(index, align="c") + return result_1 + result_2 + + +def subscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + if isinstance(a, str): + a = TextBlock(a) + if isinstance(base, str): + base = TextBlock(base) + + text2 = a.stack(TextBlock(base.height * [""], base=base.base), align="l") + text2.base = base.base + a.height + return base + text2 + + +def subsuperscript( + base: Union[TextBlock, str], a: Union[TextBlock, str], b: Union[TextBlock, str] +) -> TextBlock: + if isinstance(base, str): + base = TextBlock(base) + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + + text2 = a.stack((base.height - 1) * "\n", align="l").stack(b, align="l") + text2.base = base.base + a.height + return base + text2 + + +def superscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + if isinstance(base, str): + base = TextBlock(base) + text2 = TextBlock((base.height - 1) * "\n", base=base.base).stack(a, align="l") + return base + text2 diff --git a/mathics/format/prettyprint.py b/mathics/format/prettyprint.py new file mode 100644 index 000000000..4ee8cb47f --- /dev/null +++ b/mathics/format/prettyprint.py @@ -0,0 +1,808 @@ +""" +This module builts the 2D string associated to the OutputForm +""" + +from typing import Any, Callable, Dict, List, Optional, Union + +from mathics.core.atoms import ( + Integer, + Integer1, + Integer2, + IntegerM1, + Rational, + Real, + String, +) +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import Atom, Symbol, SymbolTimes +from mathics.core.systemsymbols import ( + SymbolDerivative, + SymbolInfix, + SymbolNone, + SymbolOutputForm, + SymbolPower, + SymbolStandardForm, + SymbolTraditionalForm, +) +from mathics.eval.makeboxes import compare_precedence, do_format # , format_element +from mathics.format.pane_text import ( + TextBlock, + bracket, + fraction, + grid, + integral_definite, + integral_indefinite, + parenthesize, + sqrt_block, + subscript, + subsuperscript, + superscript, +) + +SymbolNonAssociative = Symbol("System`NonAssociative") +SymbolPostfix = Symbol("System`Postfix") +SymbolPrefix = Symbol("System`Prefix") +SymbolRight = Symbol("System`Right") +SymbolLeft = Symbol("System`Left") + +#### Functions that convert Expressions in TextBlock + + +expr_to_2d_text_map: Dict[str, Callable] = {} + + +# This Exception if the expression should +# be processed by the default routine +class _WrongFormattedExpression(Exception): + pass + + +class IsNotGrid(Exception): + pass + + +class IsNot2DArray(Exception): + pass + + +def expression_to_2d_text( + expr: BaseElement, evaluation: Evaluation, form=SymbolStandardForm, **kwargs +): + """ + Build a 2d text from an `Expression` + """ + ## TODO: format the expression + format_expr: Expression = do_format(expr, evaluation, SymbolOutputForm) # type: ignore + + # Strip HoldForm + while format_expr.has_form("HoldForm", 1): # type: ignore + format_expr = format_expr.elements[0] + + lookup_name = format_expr.get_head().get_lookup_name() + try: + return expr_to_2d_text_map[lookup_name](format_expr, evaluation, form, **kwargs) + except _WrongFormattedExpression: + # If the key is not present, or the execution fails for any reason, use + # the default + pass + except KeyError: + pass + return _default_expression_to_2d_text(format_expr, evaluation, form, **kwargs) + + +def _default_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + """ + Default representation of a function + """ + expr_head = expr.head + head = expression_to_2d_text(expr_head, evaluation, form, **kwargs) + comma = TextBlock(", ") + elements = [expression_to_2d_text(elem, evaluation) for elem in expr.elements] + result = elements.pop(0) if elements else TextBlock(" ") + while elements: + result = result + comma + elements.pop(0) + + if form is SymbolTraditionalForm: + return head + parenthesize(result) + return head + bracket(result) + + +def _divide(num, den, evaluation, form, **kwargs): + if kwargs.get("2d", False): + return fraction( + expression_to_2d_text(num, evaluation, form, **kwargs), + expression_to_2d_text(den, evaluation, form, **kwargs), + ) + infix_form = Expression( + SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft + ) + return expression_to_2d_text(infix_form, evaluation, form, **kwargs) + + +def _strip_1_parm_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + if len(expr.elements) != 1: + raise _WrongFormattedExpression + return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`HoldForm"] = _strip_1_parm_expression_to_2d_text +expr_to_2d_text_map["System`InputForm"] = _strip_1_parm_expression_to_2d_text + + +def derivative_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + """Derivative operator""" + head = expr.get_head() + if head is SymbolDerivative: + return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) + super_head = head.get_head() + if super_head is SymbolDerivative: + expr_elements = expr.elements + if len(expr_elements) != 1: + return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) + function_head = expression_to_2d_text( + expr_elements[0], evaluation, form, **kwargs + ) + derivatives = head.elements + if len(derivatives) == 1: + order_iv = derivatives[0] + if order_iv == Integer1: + return function_head + "'" + elif order_iv == Integer2: + return function_head + "''" + + if not kwargs["2d"]: + return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + superscript_tb = TextBlock(",").join( + expression_to_2d_text(order, evaluation, form, **kwargs) + for order in derivatives + ) + superscript_tb = parenthesize(superscript_tb) + return superscript(function_head, superscript_tb) + + # Full Function with arguments: delegate to the default conversion. + # It will call us again with the head + return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Derivative"] = derivative_expression_to_2d_text + + +def divide_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + if len(expr.elements) != 2: + raise _WrongFormattedExpression + num, den = expr.elements + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Divide"] = divide_expression_to_2d_text + + +def graphics( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + return TextBlock("-Graphics-") + + +expr_to_2d_text_map["System`Graphics"] = graphics + + +def graphics3d( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + return TextBlock("-Graphics3D-") + + +expr_to_2d_text_map["System`Graphics3D"] = graphics3d + + +def grid_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + if len(expr.elements) == 0: + raise IsNotGrid + if len(expr.elements) > 1 and not expr.elements[1].has_form( + ["Rule", "RuleDelayed"], 2 + ): + raise IsNotGrid + if not expr.elements[0].has_form("List", None): + raise IsNotGrid + + elements = expr.elements[0].elements + rows = [] + for idx, item in enumerate(elements): + if item.has_form("List", None): + rows.append( + [ + expression_to_2d_text(item_elem, evaluation, form, **kwargs) + for item_elem in item.elements + ] + ) + else: + rows.append(expression_to_2d_text(item, evaluation, form, **kwargs)) + + return grid(rows) + + +expr_to_2d_text_map["System`Grid"] = grid_expression_to_2d_text + + +def integer_expression_to_2d_text( + n: Integer, evaluation: Evaluation, form: Symbol, **kwargs +): + return TextBlock(str(n.value)) + + +expr_to_2d_text_map["System`Integer"] = integer_expression_to_2d_text + + +def integrate_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + elems = list(expr.elements) + if len(elems) > 2 or not kwargs.get("2d", False): + raise _WrongFormattedExpression + + integrand = elems.pop(0) + result = expression_to_2d_text(integrand, evaluation, form, **kwargs) + while elems: + var = elems.pop(0) + if var.has_form("List", 3): + var_txt, a, b = ( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in var.elements + ) + result = integral_definite(result, var_txt, a, b) + elif isinstance(var, Symbol): + var_txt = expression_to_2d_text(var, evaluation, form, **kwargs) + result = integral_indefinite(result, var_txt) + else: + break + return result + + +expr_to_2d_text_map["System`Integrate"] = integrate_expression_to_2d_text + + +def list_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + return ( + TextBlock("{") + + TextBlock(", ").join( + [ + expression_to_2d_text(elem, evaluation, form, **kwargs) + for elem in expr.elements + ] + ) + + TextBlock("}") + ) + + +expr_to_2d_text_map["System`List"] = list_expression_to_2d_text + + +def mathmlform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return TextBlock(boxes.boxes_to_mathml()) # type: ignore[union-attr] + + +expr_to_2d_text_map["System`MathMLForm"] = mathmlform_expression_to_2d_text + + +def matrixform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + # return parenthesize(tableform_expression_to_2d_text(expr, evaluation, form, **kwargs)) + return tableform_expression_to_2d_text(expr, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`MatrixForm"] = matrixform_expression_to_2d_text + + +def plus_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + elements = expr.elements + result = TextBlock("") + for i, elem in enumerate(elements): + if elem.has_form("Times", None): + # If the first element is -1, remove it and use + # a minus sign. Otherwise, if negative, do not add a sign. + first = elem.elements[0] + if isinstance(first, Integer): + if first.value == -1: + result = ( + result + + " - " + + expression_to_2d_text( + Expression(SymbolTimes, *elem.elements[1:]), + evaluation, + form, + **kwargs, + ) + ) + continue + elif first.value < 0: + result = ( + result + + " " + + expression_to_2d_text(elem, evaluation, form, **kwargs) + ) + continue + elif isinstance(first, Real): + if first.value < 0: + result = ( + result + + " " + + expression_to_2d_text(elem, evaluation, form, **kwargs) + ) + continue + result = ( + result + " + " + expression_to_2d_text(elem, evaluation, form, **kwargs) + ) + ## TODO: handle complex numbers? + else: + elem_txt = expression_to_2d_text(elem, evaluation, form, **kwargs) + if (compare_precedence(elem, 310) or -1) < 0: + elem_txt = parenthesize(elem_txt) + result = result + " + " + elem_txt + elif i == 0 or ( + (isinstance(elem, Integer) and elem.value < 0) + or (isinstance(elem, Real) and elem.value < 0) + ): + result = result + elem_txt + else: + result = ( + result + + " + " + + expression_to_2d_text(elem, evaluation, form, **kwargs) + ) + return result + + +expr_to_2d_text_map["System`Plus"] = plus_expression_to_2d_text + + +def power_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +): + if len(expr.elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + base, exponent = ( + expression_to_2d_text(elem, evaluation, form, **kwargs) + for elem in expr.elements + ) + if (compare_precedence(expr.elements[0], 590) or 1) == -1: + base = parenthesize(base) + return superscript(base, exponent) + + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return expression_to_2d_text(infix_form, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Power"] = power_expression_to_2d_text + + +def pre_pos_infix_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + + if head in (SymbolPrefix, SymbolPostfix): + if len(operands) != 1: + raise _WrongFormattedExpression + elif head is SymbolInfix: + if len(operands) < 2: + raise _WrongFormattedExpression + else: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + if head is SymbolInfix: + # This is not the WMA behaviour, but the Mathics current implementation requires it: + num_ops = 1 + if ops.has_form("List", None): + num_ops = len(ops.elements) + ops_lst = [ + expression_to_2d_text(op, evaluation, form, **kwargs) + for op in ops.elements + ] + else: + ops_lst = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + elif head in (SymbolPrefix, SymbolPostfix): + ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolInfix: + num_ops = 1 + default_symb = TextBlock(" ~ ") + ops_lst = [ + default_symb + + expression_to_2d_text(head, evaluation, form, **kwargs) + + default_symb + ] + elif head is SymbolPrefix: + default_symb = TextBlock(" @ ") + ops_txt = ( + expression_to_2d_text(head, evaluation, form, **kwargs) + default_symb + ) + elif head is SymbolPostfix: + default_symb = TextBlock(" // ") + ops_txt = default_symb + expression_to_2d_text( + head, evaluation, form, **kwargs + ) + + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: + raise _WrongFormattedExpression + + # Processing the forth argument, if it is there: + if len(elements) > 3: + group = elements[3] + if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): + raise _WrongFormattedExpression + if group is SymbolNone: + group = None + + if head is SymbolPrefix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return ops_txt[0] + target_txt + if head is SymbolPostfix: + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + return target_txt + ops_txt[0] + else: # Infix + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + for index, operand in enumerate(operands): + operand_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) + cmp_precedence = compare_precedence(operand, precedence) + if cmp_precedence is not None and ( + cmp_precedence == -1 or (cmp_precedence == 0 and parenthesized) + ): + operand_txt = parenthesize(operand_txt) + + if index == 0: + result = operand_txt + # After the first element, for lateral + # associativity, parenthesized is flipped: + if group in (SymbolLeft, SymbolRight): + parenthesized = not parenthesized + else: + if ops_lst[index % num_ops].text != " ": + result = result + " " + ops_lst[index % num_ops] + " " + operand_txt + else: + result = result + " " + operand_txt + + return result + + +expr_to_2d_text_map["System`Prefix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Postfix"] = pre_pos_infix_expression_to_2d_text +expr_to_2d_text_map["System`Infix"] = pre_pos_infix_expression_to_2d_text + + +def precedenceform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + if len(expr.elements) == 2: + return expression_to_2d_text(expr.elements[0], evaluation, form, **kwargs) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`PrecedenceForm"] = precedenceform_expression_to_2d_text + + +def rational_expression_to_2d_text( + n: Union[Rational, Expression], evaluation: Evaluation, form: Symbol, **kwargs +): + if n.has_form("Rational", 2): + num, den = n.elements # type: ignore[union-attr] + else: + num, den = n.numerator(), n.denominator() # type: ignore[union-attr] + return _divide(num, den, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Rational"] = rational_expression_to_2d_text + + +def real_expression_to_2d_text(n: Real, evaluation: Evaluation, form: Symbol, **kwargs): + str_n = n.make_boxes("System`OutputForm").boxes_to_text() # type: ignore[attr-defined] + return TextBlock(str(str_n)) + + +expr_to_2d_text_map["System`Real"] = real_expression_to_2d_text + + +def sqrt_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + if not 1 <= len(expr.elements) <= 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return sqrt_block( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`Sqrt"] = sqrt_expression_to_2d_text + + +def subscript_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + if len(expr.elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return subscript( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`Subscript"] = subscript_expression_to_2d_text + + +def subsuperscript_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + if len(expr.elements) != 3: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + return subsuperscript( + *( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements + ) + ) + raise _WrongFormattedExpression + + +expr_to_2d_text_map["System`Subsuperscript"] = subsuperscript_expression_to_2d_text + + +def string_expression_to_2d_text( + expr: String, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + return TextBlock(expr.value) + + +expr_to_2d_text_map["System`String"] = string_expression_to_2d_text + + +def stringform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + strform = expr.elements[0] + if not isinstance(strform, String): + raise _WrongFormattedExpression + + items = list( + expression_to_2d_text(item, evaluation, form, **kwargs) + for item in expr.elements[1:] + ) + + curr_indx = 0 + parts = strform.value.split("`") + result = TextBlock(parts[0]) + if len(parts) == 1: + return result + + quote_open = True + remaining = len(parts) - 1 + + for part in parts[1:]: + remaining -= 1 + if quote_open: + if remaining == 0: + result = result + "`" + part + quote_open = False + continue + if len(part) == 0: + result = result + items[curr_indx] + continue + try: + idx = int(part) + except ValueError: + idx = None + if idx is not None and str(idx) == part: + curr_indx = idx - 1 + result = result + items[curr_indx] + quote_open = False + continue + else: + result = result + "`" + part + "`" + quote_open = False + continue + else: + result = result + part + quote_open = True + + return result + + +expr_to_2d_text_map["System`StringForm"] = stringform_expression_to_2d_text + + +def superscript_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + elements = expr.elements + if len(elements) != 2: + raise _WrongFormattedExpression + if kwargs.get("2d", False): + base, exponent = elements + base_tb, exponent_tb = ( + expression_to_2d_text(item, evaluation, form, **kwargs) for item in elements + ) + precedence = compare_precedence(base, 590) or 1 + if precedence < 0: + base_tb = parenthesize(base_tb) + return superscript(base_tb, exponent_tb) + infix_form = Expression( + SymbolInfix, + ListExpression(*(expr.elements)), + String("^"), + Integer(590), + SymbolRight, + ) + return expression_to_2d_text(infix_form, evaluation, form, **kwargs) + + +expr_to_2d_text_map["System`Superscript"] = superscript_expression_to_2d_text + + +def symbol_expression_to_2d_text( + symb: Symbol, evaluation: Evaluation, form: Symbol, **kwargs +): + return TextBlock(evaluation.definitions.shorten_name(symb.name)) + + +expr_to_2d_text_map["System`Symbol"] = symbol_expression_to_2d_text + + +def tableform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + return grid_expression_to_2d_text(expr, evaluation, form) + + +expr_to_2d_text_map["System`TableForm"] = tableform_expression_to_2d_text + + +def texform_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + # boxes = format_element(expr.elements[0], evaluation, form) + boxes = Expression( + Symbol("System`MakeBoxes"), expr.elements[0], SymbolStandardForm + ).evaluate(evaluation) + return TextBlock(boxes.boxes_to_tex()) # type: ignore + + +expr_to_2d_text_map["System`TeXForm"] = texform_expression_to_2d_text + + +def times_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + elements = expr.elements + num: List[BaseElement] = [] + den: List[BaseElement] = [] + # First, split factors with integer, negative powers: + for elem in elements: + if elem.has_form("Power", 2): + base, exponent = elem.elements + if isinstance(exponent, Integer): + if exponent.value == -1: + den.append(base) + continue + elif exponent.value < 0: + den.append(Expression(SymbolPower, base, Integer(-exponent.value))) + continue + elif isinstance(elem, Rational): + num.append(elem.numerator()) + den.append(elem.denominator()) + continue + elif elem.has_form("Rational", 2): + elem_elements = elem.elements + num.append(elem_elements[0]) + den.append(elem_elements[1]) + continue + + num.append(elem) + + # If there are integer, negative powers, process as a fraction: + if den: + den_expr = den[0] if len(den) == 1 else Expression(SymbolTimes, *den) + num_expr = ( + Expression(SymbolTimes, *num) + if len(num) > 1 + else num[0] + if len(num) == 1 + else Integer1 + ) + return _divide(num_expr, den_expr, evaluation, form, **kwargs) + + # there are no integer negative powers: + if len(num) == 1: + return expression_to_2d_text(num[0], evaluation, form, **kwargs) + + prefactor = 1 + result: TextBlock = TextBlock("") + for i, elem in enumerate(num): + if elem is IntegerM1: + prefactor *= -1 + continue + if isinstance(elem, Integer): + prefactor *= -1 + elem = Integer(-elem.value) + + elem_txt = expression_to_2d_text(elem, evaluation, form, **kwargs) + if compare_precedence(elem, 400): + elem_txt = parenthesize(elem_txt) + if i == 0: + result = elem_txt + else: + result = result + " " + elem_txt + if result.text == "": + result = TextBlock("1") + if prefactor == -1: + result = TextBlock("-") + result + return result + + +expr_to_2d_text_map["System`Times"] = times_expression_to_2d_text diff --git a/mathics/format/text.py b/mathics/format/text.py index 422ce940a..7d6f7c733 100644 --- a/mathics/format/text.py +++ b/mathics/format/text.py @@ -9,6 +9,8 @@ from mathics.builtin.box.layout import ( FractionBox, GridBox, + InterpretationBox, + PaneBox, RowBox, SqrtBox, StyleBox, @@ -40,6 +42,14 @@ def string(self, **options) -> str: add_conversion_fn(String, string) +def interpretation_panebox(self, **options): + return boxes_to_text(self.elements[0], **options) + + +add_conversion_fn(InterpretationBox, interpretation_panebox) +add_conversion_fn(PaneBox, interpretation_panebox) + + def fractionbox(self, **options) -> str: _options = self.box_options.copy() _options.update(options) diff --git a/test/format/test_2d.py b/test/format/test_2d.py new file mode 100644 index 000000000..14a774a39 --- /dev/null +++ b/test/format/test_2d.py @@ -0,0 +1,44 @@ +""" +Test 2d Output form +""" + +from test.helper import session + +import pytest + + +@pytest.mark.parametrize( + ("str_expr", "str_expected", "msg"), + [ + ("$Use2DOutputForm=True;", "Null", "Set the 2D form"), + ( + '"Hola\nCómo estás?"', + ("\n" "Hola \n" "Cómo estás?"), + "String", + ), + ("a^b", ("\n" " b\n" "a "), "power"), + ("(-a)^b", ("\n" " b\n" "(-a) "), "power of negative"), + ("(a+b)^c", ("\n" " c\n" "(a + b) "), "power with composite basis"), + ("Derivative[1][f][x]", "f'[x]", "first derivative"), + ("Derivative[2][f][x]", "f''[x]", "second derivative"), + ("Derivative[3][f][x]", ("\n" " (3) \n" "f [x]"), "Third derivative"), + ( + "Derivative[0,2][f][x]", + ("\n" " (0,2) \n" "f [x]"), + "partial derivative", + ), + ( + "Integrate[f[x]^2,x]", + ("\n" " /+ \n" " | 2 \n" " | f[x] dx\n" "+/ "), + "Indefinite integral", + ), + ("$Use2DOutputForm=False;", "Null", "Go back to the standard behavior."), + ], +) +def test_Output2D(str_expr: str, str_expected: str, msg: str): + test_expr = f"OutputForm[{str_expr}]" + result = session.evaluate_as_in_cli(test_expr).result + if msg: + assert result == str_expected, msg + else: + assert result == str_expected