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