diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py
index 27f300298..c96e1b416 100644
--- a/mathics/builtin/arithfns/basic.py
+++ b/mathics/builtin/arithfns/basic.py
@@ -15,6 +15,7 @@
IntegerM1,
Number,
RationalOneHalf,
+ String,
)
from mathics.core.attributes import (
A_FLAT,
@@ -34,11 +35,21 @@
)
from mathics.core.evaluation import Evaluation
from mathics.core.expression import Expression
-from mathics.core.symbols import Symbol, SymbolNull, SymbolPower, SymbolTimes
+from mathics.core.list import ListExpression
+from mathics.core.symbols import (
+ Symbol,
+ SymbolHoldForm,
+ SymbolNull,
+ SymbolPower,
+ SymbolTimes,
+ SymbolTrue,
+)
from mathics.core.systemsymbols import (
SymbolBlank,
SymbolComplexInfinity,
SymbolIndeterminate,
+ SymbolInfix,
+ SymbolLeft,
SymbolPattern,
SymbolSequence,
)
@@ -144,7 +155,7 @@ class Divide(InfixOperator):
expected_args = 2
formats = {
- (("InputForm", "OutputForm"), "Divide[x_, y_]"): (
+ ("InputForm", "Divide[x_, y_]"): (
'Infix[{HoldForm[x], HoldForm[y]}, "/", 400, Left]'
),
}
@@ -160,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("/"),
+ Integer(400),
+ SymbolLeft,
+ )
+ return None
+
class Minus(PrefixOperator):
"""
@@ -350,10 +379,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/forms/print.py b/mathics/builtin/forms/print.py
index 2e47e2fe9..93ff4c51f 100644
--- a/mathics/builtin/forms/print.py
+++ b/mathics/builtin/forms/print.py
@@ -20,7 +20,7 @@
from mathics.core.symbols import SymbolFalse, SymbolTrue
from mathics.core.systemsymbols import SymbolInputForm, SymbolOutputForm
from mathics.format.box.makeboxes import is_print_form_callback
-from mathics.format.form import render_input_form, render_output_form
+from mathics.format.form import render_input_form
sort_order = "mathics.builtin.forms.general-purpose-forms"
@@ -258,7 +258,21 @@ def eval_makeboxes_outputform(expr: BaseElement, evaluation: Evaluation, **kwarg
Build a 2D representation of the expression using only keyboard characters.
"""
- text_outputform = str(render_output_form(expr, evaluation, **kwargs))
+ from mathics.builtin.box.layout import InterpretationBox
+ from mathics.format.form.outputform import render_output_form
+ from mathics.format.render.prettyprint import render_2d_text
+
+ if (
+ evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace
+ is SymbolTrue
+ ):
+ text_outputform = str(render_2d_text(expr, evaluation, **{"2d": True}))
+
+ if "\n" in text_outputform:
+ text_outputform = "\n" + text_outputform
+ else: # 1D
+ text_outputform = str(render_output_form(expr, evaluation, **kwargs))
+
pane = PaneBox(String('"' + text_outputform + '"'))
return InterpretationBox(
pane, Expression(SymbolOutputForm, expr), **{"System`Editable": SymbolFalse}
diff --git a/mathics/builtin/forms/variables.py b/mathics/builtin/forms/variables.py
index 720b9a24d..e7022fbbb 100644
--- a/mathics/builtin/forms/variables.py
+++ b/mathics/builtin/forms/variables.py
@@ -5,7 +5,12 @@
"""
-from mathics.core.attributes import A_LOCKED, A_PROTECTED, A_READ_PROTECTED
+from mathics.core.attributes import (
+ A_LOCKED,
+ A_NO_ATTRIBUTES,
+ A_PROTECTED,
+ A_READ_PROTECTED,
+)
from mathics.core.builtin import Builtin, Predefined
from mathics.core.list import ListExpression
@@ -179,3 +184,35 @@ class PrintForms_(Predefined):
def evaluate(self, evaluation):
return ListExpression(*evaluation.definitions.printforms)
+
+
+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
+
+ 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"
diff --git a/mathics/doc/doc_entries.py b/mathics/doc/doc_entries.py
index a16f2acf2..11b777947 100644
--- a/mathics/doc/doc_entries.py
+++ b/mathics/doc/doc_entries.py
@@ -10,7 +10,6 @@
import logging
import re
from abc import ABC
-from os import getenv
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Sequence, Tuple
from mathics.core.evaluation import Message, Print, _Out
diff --git a/mathics/format/render/pane_text.py b/mathics/format/render/pane_text.py
new file mode 100644
index 000000000..288c1c379
--- /dev/null
+++ b/mathics/format/render/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 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/render/prettyprint.py b/mathics/format/render/prettyprint.py
new file mode 100644
index 000000000..8b42bbb15
--- /dev/null
+++ b/mathics/format/render/prettyprint.py
@@ -0,0 +1,864 @@
+"""
+This module builts the 2D string associated to the OutputForm
+"""
+
+from typing import Callable, Dict, List, Union
+
+from mathics.core.atoms import (
+ Integer,
+ Integer1,
+ Integer2,
+ IntegerM1,
+ PrecisionReal,
+ 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.number import dps
+from mathics.core.symbols import Atom, Symbol, SymbolTimes
+from mathics.core.systemsymbols import (
+ SymbolDerivative,
+ SymbolInfix,
+ SymbolNone,
+ SymbolOutputForm,
+ SymbolPower,
+ SymbolStandardForm,
+ SymbolTraditionalForm,
+)
+from mathics.format.box import compare_precedence, do_format # , format_element
+from mathics.format.box.numberform import numberform_to_boxes
+from mathics.format.render.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 render_2d_text(expr: BaseElement, evaluation: Evaluation, **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, **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_render_2d_text(format_expr, evaluation, **kwargs)
+
+
+def _default_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ """
+ Default representation of a function
+ """
+ expr_head = expr.head
+ head = render_2d_text(expr_head, evaluation, **kwargs)
+ comma = join_blocks(TEXTBLOCK_COMMA, TEXTBLOCK_SPACE)
+ elements = [render_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 kwargs.get("_Form", SymbolStandardForm) is SymbolTraditionalForm:
+ return join_blocks(head, parenthesize(result))
+ return join_blocks(head, bracket(result))
+
+
+def _divide(num, den, evaluation, **kwargs):
+ if kwargs.get("2d", False):
+ return fraction(
+ render_2d_text(num, evaluation, **kwargs),
+ render_2d_text(den, evaluation, **kwargs),
+ )
+ infix_form = Expression(
+ SymbolInfix, ListExpression(num, den), String("/"), Integer(400), SymbolLeft
+ )
+ return render_2d_text(infix_form, evaluation, **kwargs)
+
+
+def _strip_1_parm_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ if len(expr.elements) != 1:
+ raise _WrongFormattedExpression
+ return render_2d_text(expr.elements[0], evaluation, **kwargs)
+
+
+expr_to_2d_text_map["System`HoldForm"] = _strip_1_parm_render_2d_text
+expr_to_2d_text_map["System`InputForm"] = _strip_1_parm_render_2d_text
+
+
+def derivative_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ """Derivative operator"""
+ head = expr.get_head()
+ if head is SymbolDerivative:
+ return _default_render_2d_text(expr, evaluation, **kwargs)
+ super_head = head.get_head()
+ if super_head is SymbolDerivative:
+ expr_elements = expr.elements
+ if len(expr_elements) != 1:
+ return _default_render_2d_text(expr, evaluation, **kwargs)
+ function_head = render_2d_text(expr_elements[0], evaluation, **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_render_2d_text(expr, evaluation, **kwargs)
+
+ comma = TEXTBLOCK_COMMA
+ superscript_tb, *rest_derivatives = (
+ render_2d_text(order, evaluation, **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_render_2d_text(expr, evaluation, **kwargs)
+
+
+expr_to_2d_text_map["System`Derivative"] = derivative_render_2d_text
+
+
+def divide_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ if len(expr.elements) != 2:
+ raise _WrongFormattedExpression
+ num, den = expr.elements
+ return _divide(num, den, evaluation, **kwargs)
+
+
+expr_to_2d_text_map["System`Divide"] = divide_render_2d_text
+
+
+def graphics(expr: Expression, evaluation: Evaluation, **kwargs) -> TextBlock:
+ return TEXTBLOCK_GRAPHICS
+
+
+expr_to_2d_text_map["System`Graphics"] = graphics
+
+
+def graphics3d(expr: Expression, evaluation: Evaluation, **kwargs) -> TextBlock:
+ return TEXTBLOCK_GRAPHICS3D
+
+
+expr_to_2d_text_map["System`Graphics3D"] = graphics3d
+
+
+def grid_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **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(
+ [
+ render_2d_text(item_elem, evaluation, **kwargs)
+ for item_elem in item.elements
+ ]
+ )
+ else:
+ rows.append(render_2d_text(item, evaluation, **kwargs))
+
+ return grid(rows)
+
+
+expr_to_2d_text_map["System`Grid"] = grid_render_2d_text
+
+
+def integer_render_2d_text(n: Integer, evaluation: Evaluation, **kwargs):
+ return TextBlock(str(n.value))
+
+
+expr_to_2d_text_map["System`Integer"] = integer_render_2d_text
+
+
+def integrate_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ elems = list(expr.elements)
+ if len(elems) > 2 or not kwargs.get("2d", False):
+ raise _WrongFormattedExpression
+
+ integrand = elems.pop(0)
+ result = render_2d_text(integrand, evaluation, **kwargs)
+ while elems:
+ var = elems.pop(0)
+ if var.has_form("List", 3):
+ var_txt, a, b = (
+ render_2d_text(item, evaluation, **kwargs) for item in var.elements
+ )
+ result = integral_definite(result, var_txt, a, b)
+ elif isinstance(var, Symbol):
+ var_txt = render_2d_text(var, evaluation, **kwargs)
+ result = integral_indefinite(result, var_txt)
+ else:
+ break
+ return result
+
+
+expr_to_2d_text_map["System`Integrate"] = integrate_render_2d_text
+
+
+def list_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ result, *rest_elems = (
+ render_2d_text(elem, evaluation, **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_render_2d_text
+
+
+def mathmlform_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ # boxes = format_element(expr.elements[0], evaluation)
+ 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_render_2d_text
+
+
+def matrixform_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ # return parenthesize(tableform_render_2d_text(expr, evaluation, **kwargs))
+ return tableform_render_2d_text(expr, evaluation, **kwargs)
+
+
+expr_to_2d_text_map["System`MatrixForm"] = matrixform_render_2d_text
+
+
+def plus_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **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,
+ render_2d_text(
+ Expression(SymbolTimes, *elem.elements[1:]),
+ evaluation,
+ **kwargs,
+ ),
+ )
+ continue
+ elif first.value < 0:
+ result = join_blocks(
+ result,
+ TEXTBLOCK_SPACE,
+ render_2d_text(elem, evaluation, **kwargs),
+ )
+ continue
+ elif isinstance(first, Real):
+ if first.value < 0:
+ result = join_blocks(
+ result,
+ TEXTBLOCK_SPACE,
+ render_2d_text(elem, evaluation, **kwargs),
+ )
+ continue
+ result = join_blocks(
+ result, tb_plus, render_2d_text(elem, evaluation, **kwargs)
+ )
+ ## TODO: handle complex numbers?
+ else:
+ elem_txt = render_2d_text(elem, evaluation, **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,
+ render_2d_text(elem, evaluation, **kwargs),
+ )
+ return result
+
+
+expr_to_2d_text_map["System`Plus"] = plus_render_2d_text
+
+
+def power_render_2d_text(expr: Expression, evaluation: Evaluation, **kwargs):
+ if len(expr.elements) != 2:
+ raise _WrongFormattedExpression
+ if kwargs.get("2d", False):
+ base, exponent = (
+ render_2d_text(elem, evaluation, **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 render_2d_text(infix_form, evaluation, **kwargs)
+
+
+expr_to_2d_text_map["System`Power"] = power_render_2d_text
+
+
+def pre_pos_fix_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **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 = [render_2d_text(ops, evaluation, **kwargs)]
+ else:
+ if head is SymbolPrefix:
+ default_symb = TEXTBLOCK_ARROBA
+ ops_txt = join_blocks(
+ render_2d_text(head, evaluation, **kwargs), default_symb
+ )
+ else: # head is SymbolPostfix:
+ default_symb = TEXTBLOCK_DOUBLESLASH
+ ops_txt = join_blocks(
+ default_symb, render_2d_text(head, evaluation, **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 = render_2d_text(operand, evaluation, **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_render_2d_text
+expr_to_2d_text_map["System`Postfix"] = pre_pos_fix_render_2d_text
+
+
+def infix_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **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 = [
+ render_2d_text(op, evaluation, **kwargs) for op in ops.elements
+ ]
+ else:
+ ops_lst = [render_2d_text(ops, evaluation, **kwargs)]
+ elif head in (SymbolPrefix, SymbolPostfix):
+ ops_txt = [render_2d_text(ops, evaluation, **kwargs)]
+ else:
+ num_ops = 1
+ default_symb = join_blocks(TEXTBLOCK_SPACE, TEXTBLOCK_TILDE, TEXTBLOCK_SPACE)
+ ops_lst = [
+ join_blocks(
+ default_symb,
+ render_2d_text(head, evaluation, **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 = render_2d_text(operand, evaluation, **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_render_2d_text
+
+
+def precedenceform_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ if len(expr.elements) == 2:
+ return render_2d_text(expr.elements[0], evaluation, **kwargs)
+ raise _WrongFormattedExpression
+
+
+expr_to_2d_text_map["System`PrecedenceForm"] = precedenceform_render_2d_text
+
+
+def rational_render_2d_text(
+ n: Union[Rational, Expression], evaluation: Evaluation, **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, **kwargs)
+
+
+expr_to_2d_text_map["System`Rational"] = rational_render_2d_text
+
+
+def real_render_2d_text(n: Real, evaluation: Evaluation, **kwargs):
+ if not isinstance(n, Real):
+ raise _WrongFormattedExpression
+ py_digits, py_options = kwargs.setdefault(
+ "_numberform_args",
+ (
+ (None, None),
+ {},
+ ),
+ )
+ py_options["_Form"] = "System`OutputForm"
+ digits, padding = py_digits
+ if digits is None:
+ digits = dps(n.get_precision()) if isinstance(n, PrecisionReal) else 6
+
+ result = numberform_to_boxes(n, digits, padding, evaluation, py_options)
+ if isinstance(result, String):
+ return result.value
+ return result.boxes_to_text()
+
+
+expr_to_2d_text_map["System`Real"] = real_render_2d_text
+
+
+def sqrt_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ if not 1 <= len(expr.elements) <= 2:
+ raise _WrongFormattedExpression
+ if kwargs.get("2d", False):
+ return sqrt_block(
+ *(render_2d_text(item, evaluation, **kwargs) for item in expr.elements)
+ )
+ raise _WrongFormattedExpression
+
+
+expr_to_2d_text_map["System`Sqrt"] = sqrt_render_2d_text
+
+
+def subscript_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ if len(expr.elements) != 2:
+ raise _WrongFormattedExpression
+ if kwargs.get("2d", False):
+ return subscript(
+ *(render_2d_text(item, evaluation, **kwargs) for item in expr.elements)
+ )
+ raise _WrongFormattedExpression
+
+
+expr_to_2d_text_map["System`Subscript"] = subscript_render_2d_text
+
+
+def subsuperscript_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ if len(expr.elements) != 3:
+ raise _WrongFormattedExpression
+ if kwargs.get("2d", False):
+ return subsuperscript(
+ *(render_2d_text(item, evaluation, **kwargs) for item in expr.elements)
+ )
+ raise _WrongFormattedExpression
+
+
+expr_to_2d_text_map["System`Subsuperscript"] = subsuperscript_render_2d_text
+
+
+def string_render_2d_text(expr: String, evaluation: Evaluation, **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_render_2d_text
+
+
+def stringform_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ strform = expr.elements[0]
+ if not isinstance(strform, String):
+ raise _WrongFormattedExpression
+
+ items = list(
+ render_2d_text(item, evaluation, **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_render_2d_text
+
+
+def superscript_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ elements = expr.elements
+ if len(elements) != 2:
+ raise _WrongFormattedExpression
+ if kwargs.get("2d", False):
+ base, exponent = elements
+ base_tb, exponent_tb = (
+ render_2d_text(item, evaluation, **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 render_2d_text(infix_form, evaluation, **kwargs)
+
+
+expr_to_2d_text_map["System`Superscript"] = superscript_render_2d_text
+
+
+def symbol_render_2d_text(symb: Symbol, evaluation: Evaluation, **kwargs):
+ return TextBlock(evaluation.definitions.shorten_name(symb.name))
+
+
+expr_to_2d_text_map["System`Symbol"] = symbol_render_2d_text
+
+
+def tableform_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ return grid_render_2d_text(expr, evaluation)
+
+
+expr_to_2d_text_map["System`TableForm"] = tableform_render_2d_text
+
+
+def texform_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **kwargs
+) -> TextBlock:
+ # boxes = format_element(expr.elements[0], evaluation)
+ 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_render_2d_text
+
+
+def times_render_2d_text(
+ expr: Expression, evaluation: Evaluation, **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, **kwargs)
+
+ # there are no integer negative powers:
+ if len(num) == 1:
+ return render_2d_text(num[0], evaluation, **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 = render_2d_text(elem, evaluation, **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_render_2d_text
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