diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index bd6dd2d7b..aadcef82b 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -14,6 +14,7 @@ Integer1, Integer3, Integer310, + Integer400, IntegerM1, Number, Rational, @@ -48,6 +49,7 @@ SymbolNull, SymbolPower, SymbolTimes, + SymbolTrue, ) from mathics.core.systemsymbols import ( SymbolBlank, @@ -152,7 +154,7 @@ class Divide(InfixOperator): default_formats = False formats = { - (("InputForm", "OutputForm"), "Divide[x_, y_]"): ( + ("InputForm", "Divide[x_, y_]"): ( 'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]' ), } @@ -166,9 +168,26 @@ class Divide(InfixOperator): "FractionBox[MakeBoxes[x, f], MakeBoxes[y, f]]" ), } - summary_text = "divide" + 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): """ @@ -406,10 +425,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]" ), diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 9908fcc74..cc5cfab5f 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -146,6 +146,7 @@ class GridBox(BoxExpression): # >> MathMLForm[TableForm[{{a,b},{c,d}}]] # = ... """ + options = {"ColumnAlignments": "Center"} summary_text = "low-level representation of an arbitrary 2D layout" @@ -211,6 +212,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 cde81ebf1..b1e58ecc5 100644 --- a/mathics/builtin/forms/output.py +++ b/mathics/builtin/forms/output.py @@ -16,7 +16,13 @@ from math import ceil from typing import Optional -from mathics.builtin.box.layout import GridBox, RowBox, to_boxes +from mathics.builtin.box.layout import ( + GridBox, + InterpretationBox, + PaneBox, + RowBox, + to_boxes, +) from mathics.builtin.forms.base import FormBaseClass from mathics.builtin.makeboxes import MakeBoxes, NumberForm_to_String from mathics.builtin.tensors import get_dimensions @@ -54,11 +60,13 @@ SymbolOutputForm, SymbolRowBox, SymbolRuleDelayed, + SymbolStandardForm, SymbolSubscriptBox, SymbolSuperscriptBox, ) from mathics.eval.makeboxes import StringLParen, StringRParen, format_element from mathics.eval.testing_expressions import expr_min +from mathics.format.prettyprint import expression_to_2d_text MULTI_NEWLINE_RE = re.compile(r"\n{2,}") @@ -561,8 +569,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 = str(expression_to_2d_text(expr, evaluation, form)) + elem1 = PaneBox(String(text2d)) + elem2 = Expression(SymbolOutputForm, expr) + result = InterpretationBox(elem1, elem2) + return result + class PythonForm(FormBaseClass): """ @@ -707,7 +725,7 @@ class TeXForm(FormBaseClass): def eval_tex(self, expr, evaluation) -> Expression: "MakeBoxes[expr_, TeXForm]" - boxes = MakeBoxes(expr).evaluate(evaluation) + boxes = format_element(expr, evaluation, SymbolStandardForm) try: # Here we set ``show_string_characters`` to False, to reproduce # the standard behaviour in WMA. Remove this parameter to recover the diff --git a/mathics/builtin/forms/variables.py b/mathics/builtin/forms/variables.py index b02500f0d..8b7e8e81e 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/core/atoms.py b/mathics/core/atoms.py index ad98756bc..c1aa0a4df 100644 --- a/mathics/core/atoms.py +++ b/mathics/core/atoms.py @@ -330,6 +330,7 @@ def is_zero(self) -> bool: Integer2 = Integer(2) Integer3 = Integer(3) Integer310 = Integer(310) +Integer400 = Integer(400) Integer10 = Integer(10) IntegerM1 = Integer(-1) diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index 5437d039d..aa79443f6 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -30,13 +30,16 @@ SymbolRepeated, SymbolRepeatedNull, SymbolTimes, + SymbolTrue, ) from mathics.core.systemsymbols import ( SymbolComplex, SymbolMinus, + SymbolOutputForm, SymbolRational, SymbolRowBox, SymbolStandardForm, + SymbolTraditionalForm, ) # An operator precedence value that will ensure that whatever operator @@ -203,12 +206,35 @@ def eval_makeboxes( 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 = str(expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d})) + + if "\n" in text2d: + text2d = "\n" + text2d + elem1 = PaneBox(String(text2d)) + elem2 = Expression(SymbolOutputForm, expr) + return InterpretationBox(elem1, elem2) + + def format_element( element: BaseElement, evaluation: Evaluation, form: Symbol, **kwargs ) -> Optional[BaseElement]: """ Applies formats associated to the expression, and then calls Makeboxes """ + 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/format/latex.py b/mathics/format/latex.py index 6aee6d034..d258f1826 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, @@ -142,6 +144,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 6f6602a14..dddea2e59 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..7081955f7 --- /dev/null +++ b/mathics/format/pane_text.py @@ -0,0 +1,576 @@ +""" +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 + +from sympy.printing.pretty.pretty_symbology import line_width, vobj +from sympy.printing.pretty.stringpict import prettyForm, stringPict + + +class TextBlock(prettyForm): + def __init__(self, text, base=0, padding=0, height=1, width=0): + super().__init__(text, base) + assert padding == 0 + assert height == 1 + assert width == 0 + + @staticmethod + def stack(self, *args, align="c"): + if align == "c": + return super.stack(*args) + max_width = max((block.width() for block in args)) + if align == "l": + new_args = [] + for block in args: + block_width = block.width() + if block_width == max_width: + new_args.append(block) + else: + fill_block = TextBlock((max_width - block_width) * " ") + new_block = TextBlock(*TextBlock.next(block, fill_block)) + new_args.append(new_block) + return super.stack(*args) + else: # align=="r" + new_args = [] + for block in args: + block_width = block.width() + if block_width == max_width: + new_args.append(block) + else: + fill_block = TextBlock((max_width - block_width) * " ") + new_block = TextBlock( + *TextBlock.next( + fill_block, + block, + ) + ) + new_args.append(new_block) + return super.stack(*args) + + def root(self, n=None): + """Produce a nice root symbol. + Produces ugly results for big n inserts. + """ + # XXX not used anywhere + # XXX duplicate of root drawing in pretty.py + # put line over expression + result = TextBlock(*self.above("_" * self.width())) + # construct right half of root symbol + height = self.height() + slash = "\n".join(" " * (height - i - 1) + "/" + " " * i for i in range(height)) + slash = stringPict(slash, height - 1) + # left half of root symbol + if height > 2: + downline = stringPict("\\ \n \\", 1) + else: + downline = stringPict("\\") + # put n on top, as low as possible + if n is not None and n.width() > downline.width(): + downline = downline.left(" " * (n.width() - downline.width())) + downline = downline.above(n) + # build root symbol + root = TextBlock(*downline.right(slash)) + # glue it on at the proper height + # normally, the root symbel is as high as self + # which is one less than result + # this moves the root symbol one down + # if the root became higher, the baseline has to grow too + root.baseline = result.baseline - result.height() + root.height() + return result.left(root) + + +class OldTextBlock: + 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, base=0, padding=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.baseline = 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 __str__(self): + return self.text + + 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: + if height % 2 == 0: + height = height + 1 + result = TextBlock(vobj("int", height), (height - 1) // 2) + return result + + +def bracket(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + + return TextBlock(*inner.parens("[", "]")) + + +def curly_braces(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + return TextBlock(*inner.parens("{", "}")) + + +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) + return a / b + + +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 + ) + + if row: + field, *rest_row_txt = row + new_row_txt = field + for field in rest_row_txt: + new_row_txt = TextBlock( + *TextBlock.next(new_row_txt, col_sep, field) + ) + else: + new_row_txt = TextBlock("") + vertical_line = draw_vertical( + "|", row_height, base=row_base, left_padding=1 + ) + new_row_txt = TextBlock( + *TextBlock.next(vertical_line, new_row_txt, vertical_line) + ) + 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): + separator = TextBlock(" ") + if row: + field, *rest = row + new_row_txt = field + for field in rest: + new_row_txt = TextBlock( + *TextBlock.next(new_row_txt, separator, field) + ) + else: + new_row_txt = TextBlock("") + 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.baseline = 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 TextBlock(*TextBlock.next(int_symb, integrand, TextBlock(" 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) + + h_int = integrand.height() + symbol_height = h_int + # for ascii, symbol_height +=2 + int_symb = _draw_integral_symbol(symbol_height) + orig_baseline = int_symb.baseline + int_symb = subsuperscript(int_symb, a, b) + return TextBlock(*TextBlock.next(int_symb, integrand, TextBlock(" d"), var)) + + +def parenthesize(inner: Union[str, TextBlock]) -> TextBlock: + if isinstance(inner, str): + inner = TextBlock(inner) + + return TextBlock(*inner.parens()) + + +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 index is None: + index = "" + if isinstance(index, str): + index = TextBlock(index) + + return TextBlock(*a.root(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: + """ + Join b with a as a subscript. + """ + if isinstance(a, str): + a = TextBlock(a) + if isinstance(base, str): + base = TextBlock(base) + + a = TextBlock(*TextBlock.next(TextBlock(base.width() * " "), a)) + base = TextBlock(*TextBlock.next(base, TextBlock(a.width() * " "))) + result = TextBlock(*TextBlock.below(base, a)) + return result + + +def subsuperscript( + base: Union[TextBlock, str], a: Union[TextBlock, str], b: Union[TextBlock, str] +) -> TextBlock: + """ + Join base with a as a superscript and b as a subscript + """ + if isinstance(base, str): + base = TextBlock(base) + if isinstance(a, str): + a = TextBlock(a) + if isinstance(b, str): + b = TextBlock(b) + + # Ensure that a and b have the same width + width_diff = a.width() - b.width() + if width_diff < 0: + a = TextBlock(*TextBlock.next(a, TextBlock((-width_diff) * " "))) + elif width_diff > 0: + b = TextBlock(*TextBlock.next(b, TextBlock((width_diff) * " "))) + + indx_spaces = b.width() * " " + base_spaces = base.width() * " " + a = TextBlock(*TextBlock.next(TextBlock(base_spaces), a)) + b = TextBlock(*TextBlock.next(TextBlock(base_spaces), b)) + base = TextBlock(*TextBlock.next(base, TextBlock(base_spaces))) + result = TextBlock(*TextBlock.below(base, a)) + result = TextBlock(*TextBlock.above(result, b)) + return result + + +def superscript(base: Union[TextBlock, str], a: Union[TextBlock, str]) -> TextBlock: + if isinstance(a, str): + a = TextBlock(a) + if isinstance(base, str): + base = TextBlock(base) + + base_width, a_width = base.width(), a.width() + a = TextBlock(*TextBlock.next(TextBlock(base_width * " "), a)) + base = TextBlock(*TextBlock.next(base, TextBlock(a_width * " "))) + result = TextBlock(*TextBlock.above(base, a)) + return result + + +def join_blocks(*blocks) -> TextBlock: + """ + Concatenate blocks. + The same that the idiom + TextBlock(*TextBlock.next(*blocks)) + """ + return TextBlock(*TextBlock.next(*blocks)) + + +TEXTBLOCK_COMMA = TextBlock(",") +TEXTBLOCK_MINUS = TextBlock("-") +TEXTBLOCK_NULL = TextBlock("") +TEXTBLOCK_PLUS = TextBlock("+") +TEXTBLOCK_QUOTE = TextBlock("'") +TEXTBLOCK_SPACE = TextBlock(" ") diff --git a/mathics/format/prettyprint.py b/mathics/format/prettyprint.py new file mode 100644 index 000000000..068f1bbca --- /dev/null +++ b/mathics/format/prettyprint.py @@ -0,0 +1,879 @@ +""" +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_COMMA, + TEXTBLOCK_MINUS, + TEXTBLOCK_NULL, + TEXTBLOCK_PLUS, + TEXTBLOCK_QUOTE, + TEXTBLOCK_SPACE, + TextBlock, + bracket, + curly_braces, + fraction, + grid, + integral_definite, + integral_indefinite, + join_blocks, + 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") + + +TEXTBLOCK_ARROBA = TextBlock("@") +TEXTBLOCK_BACKQUOTE = TextBlock("`") +TEXTBLOCK_DOUBLESLASH = TextBlock("//") +TEXTBLOCK_GRAPHICS = TextBlock("-Graphics-") +TEXTBLOCK_GRAPHICS3D = TextBlock("-Graphics3D-") +TEXTBLOCK_ONE = TextBlock("1") +TEXTBLOCK_TILDE = TextBlock("~") + +#### 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: + result = expr_to_2d_text_map[lookup_name]( + format_expr, evaluation, form, **kwargs + ) + return result + 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 = join_blocks(TEXTBLOCK_COMMA, TEXTBLOCK_SPACE) + elements = [expression_to_2d_text(elem, evaluation) for elem in expr.elements] + result = elements.pop(0) if elements else TEXTBLOCK_SPACE + while elements: + result = join_blocks(result, comma, elements.pop(0)) + + if form is SymbolTraditionalForm: + return join_blocks(head, parenthesize(result)) + return join_blocks(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 join_blocks(function_head, TEXTBLOCK_QUOTE) + elif order_iv == Integer2: + return join_blocks(function_head, TEXTBLOCK_QUOTE, TEXTBLOCK_QUOTE) + + if not kwargs["2d"]: + return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) + + comma = TEXTBLOCK_COMMA + superscript_tb, *rest_derivatives = ( + expression_to_2d_text(order, evaluation, form, **kwargs) + for order in derivatives + ) + for order in rest_derivatives: + superscript_tb = join_blocks(superscript_tb, comma, order) + + 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: + result, *rest_elems = ( + expression_to_2d_text(elem, evaluation, form, **kwargs) + for elem in expr.elements + ) + comma_tb = join_blocks(TEXTBLOCK_COMMA, TEXTBLOCK_SPACE) + for next_elem in rest_elems: + result = TextBlock(*TextBlock.next(result, comma_tb, next_elem)) + return curly_braces(result) + + +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_NULL + tb_minus = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_MINUS, TEXTBLOCK_SPACE) + tb_plus = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_PLUS, TEXTBLOCK_SPACE) + 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 = join_blocks( + result, + tb_minus, + expression_to_2d_text( + Expression(SymbolTimes, *elem.elements[1:]), + evaluation, + form, + **kwargs, + ), + ) + continue + elif first.value < 0: + result = join_blocks( + result, + TEXTBLOCK_SPACE, + expression_to_2d_text(elem, evaluation, form, **kwargs), + ) + continue + elif isinstance(first, Real): + if first.value < 0: + result = join_blocks( + result, + TEXTBLOCK_SPACE, + expression_to_2d_text(elem, evaluation, form, **kwargs), + ) + continue + result = join_blocks( + result, tb_plus, 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 = join_blocks(result, tb_plus, elem_txt) + elif i == 0 or ( + (isinstance(elem, Integer) and elem.value < 0) + or (isinstance(elem, Real) and elem.value < 0) + ): + result = join_blocks(result, elem_txt) + else: + result = join_blocks( + result, + tb_plus, + 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_fix_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 len(operands) != 1: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolPrefix: + default_symb = TEXTBLOCK_ARROBA + ops_txt = join_blocks( + expression_to_2d_text(head, evaluation, form, **kwargs), default_symb + ) + else: # head is SymbolPostfix: + default_symb = TEXTBLOCK_DOUBLESLASH + ops_txt = join_blocks( + 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 + + 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 ( + join_blocks(ops_txt[0], target_txt) + if head is SymbolPrefix + else join_blocks(target_txt, ops_txt[0]) + ) + + +expr_to_2d_text_map["System`Prefix"] = pre_pos_fix_expression_to_2d_text +expr_to_2d_text_map["System`Postfix"] = pre_pos_fix_expression_to_2d_text + + +def 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 len(operands) < 2: + 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: + num_ops = 1 + default_symb = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_TILDE, TEXTBLOCK_SPACE) + ops_lst = [ + join_blocks( + default_symb, + expression_to_2d_text(head, evaluation, form, **kwargs), + default_symb, + ) + ] + + # 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 + + 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: + space = TEXTBLOCK_SPACE + if str(ops_lst[index % num_ops]) != " ": + result_lst = [ + result, + space, + ops_lst[index % num_ops], + space, + operand_txt, + ] + else: + result_lst = [result, space, operand_txt] + + return join_blocks(*result_lst) + + +expr_to_2d_text_map["System`Infix"] = 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: + lines = expr.value.split("\n") + max_len = max([len(line) for line in lines]) + lines = [line + (max_len - len(line)) * " " for line in lines] + return TextBlock("\n".join(lines)) + + +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 = join_blocks( + result, TEXTBLOCK_BACKQUOTE, part, TEXTBLOCK_BACKQUOTE + ) + quote_open = False + continue + else: + result = join_blocks(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_NULL + 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 = join_blocks(result, TEXTBLOCK_SPACE, elem_txt) + if str(result) == "": + result = TEXTBLOCK_ONE + if prefactor == -1: + result = join_blocks(TEXTBLOCK_MINUS, 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..1165415a2 --- /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" "⌠ 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