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