From d92fffba902aabb370d1ecb03198197d505e7097 Mon Sep 17 00:00:00 2001 From: mmatera Date: Sun, 10 Nov 2024 17:48:36 -0300 Subject: [PATCH] build expressions using sympy.printing.pretty.stringpict --- mathics/eval/makeboxes.py | 2 +- mathics/format/pane_text.py | 162 +++++++++++++++++++++------------- mathics/format/prettyprint.py | 130 +++++++++++++++++++++------ test/format/test_2d.py | 2 +- 4 files changed, 207 insertions(+), 89 deletions(-) diff --git a/mathics/eval/makeboxes.py b/mathics/eval/makeboxes.py index bd6b46ed8..aa79443f6 100644 --- a/mathics/eval/makeboxes.py +++ b/mathics/eval/makeboxes.py @@ -217,7 +217,7 @@ def make_output_form(expr, evaluation, form): evaluation.definitions.get_ownvalues("System`$Use2DOutputForm")[0].replace is SymbolTrue ) - text2d = expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d}).text + text2d = str(expression_to_2d_text(expr, evaluation, form, **{"2d": use_2d})) if "\n" in text2d: text2d = "\n" + text2d diff --git a/mathics/format/pane_text.py b/mathics/format/pane_text.py index d8b693530..6a643937b 100644 --- a/mathics/format/pane_text.py +++ b/mathics/format/pane_text.py @@ -7,8 +7,50 @@ from typing import List, Optional, Union +from sympy.printing.pretty.pretty_symbology import line_width, vobj +from sympy.printing.pretty.stringpict import prettyForm, stringPict -class TextBlock: + +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 + + 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 @@ -37,7 +79,7 @@ def _build_attributes(lines, width=0, height=0, base=0): return (lines, width, height, base) - def __init__(self, text, padding=0, base=0, height=1, width=0): + def __init__(self, text, base=0, padding=0, height=1, width=0): if isinstance(text, str): if text == "": lines = [] @@ -63,6 +105,9 @@ def text(self): def text(self, value): raise TypeError("TextBlock is inmutable") + def __str__(self): + return self.text + def __repr__(self): return self.text @@ -166,45 +211,23 @@ def stack(self, top, align: str = "c"): def _draw_integral_symbol(height: int) -> TextBlock: - return TextBlock( - (" /+ \n" + "\n".join(height * [" | "]) + "\n+/ "), base=int((height + 1) / 2) - ) + 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) - 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 + + return TextBlock(*inner.parens("[", "]")) 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 + return TextBlock(*inner.parens("{", "}")) def draw_vertical( @@ -233,11 +256,7 @@ def fraction(a: Union[TextBlock, str], b: Union[TextBlock, str]) -> TextBlock: 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 a / b return result @@ -359,8 +378,8 @@ def integral_indefinite( if isinstance(integrand, str): integrand = TextBlock(integrand) - int_symb: TextBlock = _draw_integral_symbol(integrand.height) - return int_symb + integrand + " d" + var + int_symb: TextBlock = _draw_integral_symbol(integrand.height()) + return TextBlock(*TextBlock.next(int_symb, integrand, TextBlock(" d"), var)) def integral_definite( @@ -380,24 +399,20 @@ def integral_definite( if isinstance(b, str): b = TextBlock(b) - int_symb = _draw_integral_symbol(integrand.height) - return subsuperscript(int_symb, a, b) + " " + integrand + " d" + var + 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) - 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 + + return TextBlock(*inner.parens()) def sqrt_block( @@ -408,9 +423,13 @@ def sqrt_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 @@ -433,19 +452,26 @@ def sqrt_block( 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) - text2 = a.stack(TextBlock(base.height * [""], base=base.base), align="l") - text2.base = base.base + a.height - return base + text2 + 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): @@ -453,13 +479,31 @@ def subsuperscript( 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 + # 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) - text2 = TextBlock((base.height - 1) * "\n", base=base.base).stack(a, align="l") - return base + text2 + + 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 diff --git a/mathics/format/prettyprint.py b/mathics/format/prettyprint.py index 4ee8cb47f..21d0dda63 100644 --- a/mathics/format/prettyprint.py +++ b/mathics/format/prettyprint.py @@ -31,6 +31,7 @@ from mathics.format.pane_text import ( TextBlock, bracket, + curly_braces, fraction, grid, integral_definite, @@ -83,7 +84,10 @@ def expression_to_2d_text( lookup_name = format_expr.get_head().get_lookup_name() try: - return expr_to_2d_text_map[lookup_name](format_expr, evaluation, form, **kwargs) + result = expr_to_2d_text_map[lookup_name]( + format_expr, evaluation, form, **kwargs + ) + return result except _WrongFormattedExpression: # If the key is not present, or the execution fails for any reason, use # the default @@ -109,7 +113,7 @@ def _default_expression_to_2d_text( if form is SymbolTraditionalForm: return head + parenthesize(result) - return head + bracket(result) + return TextBlock(*TextBlock.next(head, bracket(result))) def _divide(num, den, evaluation, form, **kwargs): @@ -155,17 +159,21 @@ def derivative_expression_to_2d_text( if len(derivatives) == 1: order_iv = derivatives[0] if order_iv == Integer1: - return function_head + "'" + return TextBlock(*TextBlock.next(function_head, TextBlock("'"))) elif order_iv == Integer2: - return function_head + "''" + return TextBlock(*TextBlock.next(function_head, TextBlock("''"))) if not kwargs["2d"]: return _default_expression_to_2d_text(expr, evaluation, form, **kwargs) - superscript_tb = TextBlock(",").join( + comma = TextBlock(",") + superscript_tb, *rest_derivatives = ( expression_to_2d_text(order, evaluation, form, **kwargs) for order in derivatives ) + for order in rest_derivatives: + superscript_tb = TextBlock(*TextBlock.next(superscript_tb, comma, order)) + superscript_tb = parenthesize(superscript_tb) return superscript(function_head, superscript_tb) @@ -278,16 +286,14 @@ def 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("}") + result, *rest_elems = ( + expression_to_2d_text(elem, evaluation, form, **kwargs) + for elem in expr.elements ) + comma_tb = TextBlock(", ") + for next_elem in rest_elems: + result = TextBlock(*TextBlock.next(result, comma_tb, next_elem)) + return curly_braces(result) expr_to_2d_text_map["System`List"] = list_expression_to_2d_text @@ -407,7 +413,7 @@ def power_expression_to_2d_text( expr_to_2d_text_map["System`Power"] = power_expression_to_2d_text -def pre_pos_infix_expression_to_2d_text( +def pre_pos_fix_expression_to_2d_text( expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: elements = expr.elements @@ -423,14 +429,74 @@ def pre_pos_infix_expression_to_2d_text( raise _WrongFormattedExpression operands = list(target.elements) + if len(operands) != 1: + raise _WrongFormattedExpression + + # Processing the second argument, if it is there: + if len(elements) > 1: + ops = elements[1] + ops_txt = [expression_to_2d_text(ops, evaluation, form, **kwargs)] + else: + if head is SymbolPrefix: + default_symb = TextBlock(" @ ") + 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 + ) - if head in (SymbolPrefix, SymbolPostfix): - if len(operands) != 1: + # Processing the third argument, if it is there: + if len(elements) > 2: + if isinstance(elements[2], Integer): + precedence = elements[2].value + else: raise _WrongFormattedExpression - elif head is SymbolInfix: - if len(operands) < 2: + + # 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 - else: + if group is SymbolNone: + group = None + + operand = operands[0] + cmp_precedence = compare_precedence(operand, precedence) + target_txt = expression_to_2d_text(operand, evaluation, form, **kwargs) + if cmp_precedence is not None and cmp_precedence != -1: + target_txt = parenthesize(target_txt) + + if head is SymbolPrefix: + return TextBlock(*TextBlock.next(ops_txt[0], target_txt)) + if head is SymbolPostfix: + return TextBlock(*TextBlock.next(target_txt, ops_txt[0])) + + +expr_to_2d_text_map["System`Prefix"] = pre_pos_fix_expression_to_2d_text +expr_to_2d_text_map["System`Postfix"] = pre_pos_fix_expression_to_2d_text + + +def infix_expression_to_2d_text( + expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs +) -> TextBlock: + elements = expr.elements + if not (0 <= len(elements) <= 4): + raise _WrongFormattedExpression + + group = None + precedence = 670 + # Processing the first argument: + head = expr.get_head() + target = expr.elements[0] + if isinstance(target, Atom): + raise _WrongFormattedExpression + + operands = list(target.elements) + + if len(operands) < 2: raise _WrongFormattedExpression # Processing the second argument, if it is there: @@ -515,17 +581,22 @@ def pre_pos_infix_expression_to_2d_text( 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 + space = TextBlock(" ") + if str(ops_lst[index % num_ops]) != " ": + result_lst = ( + result, + space, + ops_lst[index % num_ops], + space, + operand_txt, + ) else: - result = result + " " + operand_txt + result_lst = (result, space, operand_txt) - return result + return TextBlock(*TextBlock.next(*result_lst)) -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 +expr_to_2d_text_map["System`Infix"] = infix_expression_to_2d_text def precedenceform_expression_to_2d_text( @@ -617,7 +688,10 @@ def subsuperscript_expression_to_2d_text( def string_expression_to_2d_text( expr: String, evaluation: Evaluation, form: Symbol, **kwargs ) -> TextBlock: - return TextBlock(expr.value) + lines = expr.value.split("\n") + max_len = max([len(line) for line in lines]) + lines = [line + (max_len - len(line)) * " " for line in lines] + return TextBlock("\n".join(lines)) expr_to_2d_text_map["System`String"] = string_expression_to_2d_text diff --git a/test/format/test_2d.py b/test/format/test_2d.py index 14a774a39..1165415a2 100644 --- a/test/format/test_2d.py +++ b/test/format/test_2d.py @@ -29,7 +29,7 @@ ), ( "Integrate[f[x]^2,x]", - ("\n" " /+ \n" " | 2 \n" " | f[x] dx\n" "+/ "), + ("\n" "⌠ 2 \n" "⎮f[x] dx\n" "⌡ "), "Indefinite integral", ), ("$Use2DOutputForm=False;", "Null", "Go back to the standard behavior."),