diff --git a/.github/workflows/consistency-checks.yml b/.github/workflows/consistency-checks.yml index f3fdc5d19..988109e9f 100644 --- a/.github/workflows/consistency-checks.yml +++ b/.github/workflows/consistency-checks.yml @@ -23,7 +23,7 @@ jobs: sudo apt update -qq && sudo apt install llvm-dev remake python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" pip install -e . - name: Install Mathics with minimum dependencies diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7aaed5a8c..d0b486530 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,7 +30,7 @@ jobs: run: | # We can comment out after next Mathics-Scanner release # python -m pip install Mathics-Scanner[full] - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" pip install -e . remake -x develop-full - name: Test Mathics3 diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 9d7da44a1..ff8f437b9 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -25,7 +25,7 @@ jobs: run: | python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" - name: Run Mathics3 Combinatorica tests run: | git submodule init diff --git a/.github/workflows/plot-tests.yml b/.github/workflows/plot-tests.yml index 2dad1867e..7c93fece9 100644 --- a/.github/workflows/plot-tests.yml +++ b/.github/workflows/plot-tests.yml @@ -25,7 +25,7 @@ jobs: run: | python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" git clone --depth 1 https://github.com/Mathics3/mathics-scanner.git cd mathics-scanner/ pip install -e . diff --git a/.github/workflows/pyodide.yml b/.github/workflows/pyodide.yml index dfea4b728..52b0eba21 100644 --- a/.github/workflows/pyodide.yml +++ b/.github/workflows/pyodide.yml @@ -58,5 +58,5 @@ jobs: python -m pip install --no-build-isolation -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner pip install --no-build-isolation -e . - make mathics/data/op-tables.json mathics/data/operator-tables.json + make mathics/data/character-tables.json mathics/data/operator-tables.json make -j3 check diff --git a/.github/workflows/ubuntu-bench.yml b/.github/workflows/ubuntu-bench.yml index a958f7a02..631f674be 100644 --- a/.github/workflows/ubuntu-bench.yml +++ b/.github/workflows/ubuntu-bench.yml @@ -26,7 +26,7 @@ jobs: python -m pip install --upgrade pip python -m pip install pytest-benchmark # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" # python -m pip install Mathics-Scanner[full] pip install -e . remake -x develop diff --git a/.github/workflows/ubuntu-cython.yml b/.github/workflows/ubuntu-cython.yml index 17d83064a..e704774e4 100644 --- a/.github/workflows/ubuntu-cython.yml +++ b/.github/workflows/ubuntu-cython.yml @@ -25,7 +25,7 @@ jobs: sudo apt-get update -qq && sudo apt-get install -qq liblapack-dev llvm-dev tesseract-ocr python -m pip install --upgrade pip # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" pip install -e . cd .. diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index da2a005bf..425650a5a 100755 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -34,7 +34,7 @@ jobs: - name: Install Mathics3 with Python dependencies run: | # We can comment out after next Mathics-Scanner release - python -m pip install -e git+https://github.com/Mathics3/mathics-scanner#egg=Mathics-Scanner[full] + python -m pip install -e "Mathics-Scanner[full] @ git+https://github.com/Mathics3/mathics-scanner" pip install -e . # python -m pip install Mathics-Scanner[full] diff --git a/Makefile b/Makefile index ffad84fd6..1bd9e3e6a 100644 --- a/Makefile +++ b/Makefile @@ -44,13 +44,13 @@ MATHICS3_MODULE_OPTION ?= --load-module pymathics.graph,pymathics.natlang test \ texdoc -SANDBOX ?= +MATHICS3_SANDBOX ?= ifeq ($(OS),Windows_NT) - SANDBOX = t + MATHICS3_SANDBOX = t else UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) - SANDBOX = t + MATHICS3_SANDBOX = t endif endif @@ -69,17 +69,17 @@ build: # because pip install doesn't handle # INSTALL_REQUIRES properly #: Set up to run from the source tree -develop: mathics/data/op-tables.json mathics/data/operator-tables.json +develop: mathics/data/character-tables.json mathics/data/operator-tables.json $(PIP) install -e .[dev] # See note above on ./setup.py #: Set up to run from the source tree with full dependencies -develop-full: mathics/data/op-tables.json mathics/data/operators.json +develop-full: mathics/data/character-tables.json mathics/data/operators.json $(PIP) install -e .[dev,full] # See note above on ./setup.py #: Set up to run from the source tree with full dependencies and Cython -develop-full-cython: mathics/data/op-tables.json mathics/data/operators.json +develop-full-cython: mathics/data/character-tables.json mathics/data/operators.json $(PIP) install -e .[dev,full,cython] @@ -126,7 +126,7 @@ clean: clean-cython clean-cache ($(MAKE) -C "$$dir" clean); \ done; \ rm -f factorials || true; \ - rm -f mathics/data/op-tables || true; \ + rm -f mathics/data/character-tables.json || true; \ rm -rf build || true mypy: @@ -155,7 +155,7 @@ doctest-data: mathics/builtin/*.py mathics/doc/documentation/*.mdoc mathics/doc/ #: Run tests that appear in docstring in the code. Use environment variable "DOCTEST_OPTIONS" for doctest options doctest: - MATHICS_CHARACTER_ENCODING="ASCII" SANDBOX=$(SANDBOX) $(PYTHON) mathics/docpipeline.py $(DOCTEST_OPTIONS) + MATHICS_CHARACTER_ENCODING="ASCII" MATHICS3_SANDBOX=$(MATHICS3_SANDBOX) $(PYTHON) mathics/docpipeline.py $(DOCTEST_OPTIONS) #: Run tests that appear in docstring in the code, stopping on the first error. doctest-x: @@ -166,7 +166,7 @@ latexdoc texdoc doc: (cd mathics/doc/latex && $(MAKE) doc) #: Build JSON ASCII to unicode opcode table and operator table -mathics/data/operator-tables.json mathics/data/op-tables.json mathics/data/operators.json: +mathics/data/operator-tables.json mathics/data/character-tables.json mathics/data/operators.json: $(BASH) ./admin-tools/make-JSON-tables.sh #: Remove ChangeLog diff --git a/SYMBOLS_MANIFEST.txt b/SYMBOLS_MANIFEST.txt index 35e37ac65..f4e4c96dc 100644 --- a/SYMBOLS_MANIFEST.txt +++ b/SYMBOLS_MANIFEST.txt @@ -12,6 +12,7 @@ ImportExport`RegisterExport ImportExport`RegisterImport Internal`RealValuedNumberQ Internal`RealValuedNumericQ +JSON`Import`JSONImport System`$Aborted System`$Assumptions System`$BaseDirectory @@ -486,6 +487,7 @@ System`FoldList System`FontColor System`For System`Format +System`FormatType System`FormatValues System`FractionBox System`FractionalPart diff --git a/admin-tools/make-JSON-tables.sh b/admin-tools/make-JSON-tables.sh index 8a166d9da..a9d802a8d 100755 --- a/admin-tools/make-JSON-tables.sh +++ b/admin-tools/make-JSON-tables.sh @@ -5,14 +5,7 @@ mydir=$(dirname $bs) PYTHON=${PYTHON:-python} cd $mydir/../mathics/data -mathics3-generate-json-table \ - --field=ascii-operator-to-symbol \ - --field=ascii-operator-to-unicode \ - --field=ascii-operator-to-wl-unicode \ - --field=operator-to-ascii \ - --field=operator-to-amslatex \ - --field=operator-to-unicode \ - -o op-tables.json +mathics3-generate-json-table -o character-tables.json mathics3-generate-operator-json-table -o operator-tables.json # tokenizer looks for the table in the default place... mathics3-generate-operator-json-table diff --git a/mathics/__main__.py b/mathics/__main__.py index b24825ebd..28ac37327 100755 --- a/mathics/__main__.py +++ b/mathics/__main__.py @@ -357,7 +357,10 @@ def interactive_eval_loop(shell, full_form: bool, strict_wl_output: bool): show_echo(source_code, evaluation) if len(source_code) and source_code[0] == "!": - subprocess.run(source_code[1:], shell=True) + if not settings.ENABLE_SYSTEM_COMMANDS: + evaluation.message("Run", "dis") + else: + subprocess.run(source_code[1:], shell=True) shell.definitions.increment_line_no(1) continue if query is None: diff --git a/mathics/builtin/arithfns/basic.py b/mathics/builtin/arithfns/basic.py index 0e3820823..27f300298 100644 --- a/mathics/builtin/arithfns/basic.py +++ b/mathics/builtin/arithfns/basic.py @@ -7,19 +7,14 @@ """ -from mathics.builtin.arithmetic import create_infix from mathics.core.atoms import ( Complex, Integer, Integer1, Integer3, - Integer310, IntegerM1, Number, - Rational, RationalOneHalf, - Real, - String, ) from mathics.core.attributes import ( A_FLAT, @@ -37,32 +32,20 @@ PrefixOperator, SympyFunction, ) -from mathics.core.convert.expression import to_expression -from mathics.core.convert.sympy import from_sympy from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.list import ListExpression -from mathics.core.symbols import ( - Symbol, - SymbolDivide, - SymbolHoldForm, - SymbolNull, - SymbolPower, - SymbolTimes, -) +from mathics.core.symbols import Symbol, SymbolNull, SymbolPower, SymbolTimes from mathics.core.systemsymbols import ( SymbolBlank, SymbolComplexInfinity, SymbolIndeterminate, - SymbolInfix, - SymbolLeft, - SymbolMinus, SymbolPattern, SymbolSequence, ) from mathics.eval.arithfns.basic import eval_Plus, eval_Times from mathics.eval.nevaluator import eval_N from mathics.eval.numerify import numerify +from mathics.format.form_rule.arithfns import format_plus, format_times class CubeRoot(Builtin): @@ -303,54 +286,7 @@ def eval(self, elements, evaluation: Evaluation): def format_plus(self, items, evaluation: Evaluation): "Plus[items__]" - - def negate(item): # -> Expression (see FIXME below) - if item.has_form("Times", 2, None): - if isinstance(item.elements[0], Number): - first, *rest = item.elements - first = -first - if first.sameQ(Integer1): - if len(rest) == 1: - return rest[0] - return Expression(SymbolTimes, *rest) - - return Expression(SymbolTimes, first, *rest) - else: - return Expression(SymbolTimes, IntegerM1, *item.elements) - elif isinstance(item, Number): - return from_sympy(-item.to_sympy()) - else: - return Expression(SymbolTimes, IntegerM1, item) - - def is_negative(value) -> bool: - if isinstance(value, Complex): - real, imag = value.to_sympy().as_real_imag() - if real <= 0 and imag <= 0: - return True - elif isinstance(value, Number) and value.to_sympy() < 0: - return True - return False - - elements = items.get_sequence() - values = [to_expression(SymbolHoldForm, element) for element in elements[:1]] - ops = [] - for element in elements[1:]: - if ( - element.has_form("Times", 1, None) and is_negative(element.elements[0]) - ) or is_negative(element): - element = negate(element) - op = "-" - else: - op = "+" - values.append(Expression(SymbolHoldForm, element)) - ops.append(String(op)) - return Expression( - SymbolInfix, - ListExpression(*values), - ListExpression(*ops), - Integer310, - SymbolLeft, - ) + return format_plus(items, evaluation) class Power(InfixOperator, MPMathFunction): @@ -645,74 +581,16 @@ def eval(self, elements, evaluation: Evaluation): def format_times(self, items, evaluation: Evaluation, op="\u2062"): "Times[items__]" - - def inverse(item): - if item.has_form("Power", 2) and isinstance( # noqa - item.elements[1], (Integer, Rational, Real) - ): - neg = -item.elements[1] - if neg.sameQ(Integer1): - return item.elements[0] - else: - return Expression(SymbolPower, item.elements[0], neg) - else: - return item - - items = items.get_sequence() - if len(items) < 2: - return - positive = [] - negative = [] - for item in items: - if ( - item.has_form("Power", 2) - and isinstance(item.elements[1], (Integer, Rational, Real)) - and item.elements[1].to_sympy() < 0 - ): # nopep8 - negative.append(inverse(item)) - elif isinstance(item, Rational): - numerator = item.numerator() - if not numerator.sameQ(Integer1): - positive.append(numerator) - negative.append(item.denominator()) - else: - positive.append(item) - - if positive and hasattr(positive[0], "value") and positive[0].value == -1: - del positive[0] - minus = True - else: - minus = False - positive = [Expression(SymbolHoldForm, item) for item in positive] - negative = [Expression(SymbolHoldForm, item) for item in negative] - if positive: - positive = create_infix(positive, op, 400, "Left") - else: - positive = Integer1 - if negative: - negative = create_infix(negative, op, 400, "Left") - result = Expression( - SymbolDivide, - Expression(SymbolHoldForm, positive), - Expression(SymbolHoldForm, negative), - ) - else: - result = positive - if minus: - result = Expression( - SymbolMinus, result - ) # Expression('PrecedenceForm', result, 481)) - result = Expression(SymbolHoldForm, result) - return result + return format_times(items, evaluation, op) def format_inputform(self, items, evaluation): "(InputForm,): Times[items__]" - return self.format_times(items, evaluation, op="*") + return format_times(items, evaluation, op="*") def format_standardform(self, items, evaluation): "(StandardForm,): Times[items__]" - return self.format_times(items, evaluation, op=" ") + return format_times(items, evaluation, op=" ") def format_outputform(self, items, evaluation): "(OutputForm,): Times[items__]" - return self.format_times(items, evaluation, op=" ") + return format_times(items, evaluation, op=" ") diff --git a/mathics/builtin/assignments/types.py b/mathics/builtin/assignments/types.py index 1f2b7f105..f40ddb99d 100644 --- a/mathics/builtin/assignments/types.py +++ b/mathics/builtin/assignments/types.py @@ -155,7 +155,7 @@ class SubValues(Builtin): >> SubValues[f] = {HoldPattern[f[2][x_]] :> x ^ 2, HoldPattern[f[1][x_]] :> x} >> Definition[f] - = f[2][x_] = x ^ 2 + = f[2][x_] = x^2 . . f[1][x_] = x """ diff --git a/mathics/builtin/atomic/strings.py b/mathics/builtin/atomic/strings.py index 2c2326e7a..ad4c42fc0 100644 --- a/mathics/builtin/atomic/strings.py +++ b/mathics/builtin/atomic/strings.py @@ -876,7 +876,7 @@ class ToString(Builtin): >> "U" <> ToString[2] = U2 >> ToString[Integrate[f[x],x], TeXForm] - = \\int f\\left(x\\right) \\, dx + = \\int f(x) \\, dx """ diff --git a/mathics/builtin/atomic/symbols.py b/mathics/builtin/atomic/symbols.py index eb0acafff..5af79bb99 100644 --- a/mathics/builtin/atomic/symbols.py +++ b/mathics/builtin/atomic/symbols.py @@ -62,6 +62,11 @@ def gather_and_format_definition_rules( """Return a list of lines describing the definition of `symbol`""" lines = [] + def rhs_format(expr): + if expr.has_form("Infix", None): + expr = Expression(Expression(SymbolHoldForm, expr.head), *expr.elements) + return expr + def format_rule( rule: Rule, up: bool = False, @@ -73,17 +78,17 @@ def format_rule( """ evaluation.check_stopped() if isinstance(rule, Rule): - r = rhs( + lhs_pat = Expression(SymbolInputForm, lhs(rule.pattern.expr)) + repl_expr = rhs( rule.replace.replace_vars( {"System`Definition": Expression(SymbolHoldForm, SymbolDefinition)} ) ) + repl_expr = Expression(SymbolInputForm, repl_expr) lines.append( Expression( SymbolHoldForm, - Expression( - up and SymbolUpSet or SymbolSet, lhs(rule.pattern.expr), r - ), + Expression(up and SymbolUpSet or SymbolSet, lhs_pat, repl_expr), ) ) @@ -103,20 +108,13 @@ def gather_rules(definition: Definition): for rule in definition.nvalues: format_rule(rule) formats = sorted(definition.formatvalues.items()) - for format, rules in formats: + for form_name, rules in formats: for rule in rules: - def lhs(expr): - return Expression(SymbolFormat, expr, Symbol(format)) + def lhs_format(expr): + return Expression(SymbolFormat, expr, Symbol(form_name)) - def rhs(expr): - if expr.has_form("Infix", None): - expr = Expression( - Expression(SymbolHoldForm, expr.head), *expr.elements - ) - return Expression(SymbolInputForm, expr) - - format_rule(rule, lhs=lhs, rhs=rhs) + format_rule(rule, lhs=lhs_format, rhs=rhs_format) name = symbol.get_name() if not name: @@ -235,13 +233,13 @@ class Definition(Builtin): >> f[x_] := x ^ 2 >> g[f] ^:= 2 >> Definition[f] - = f[x_] = x ^ 2 + = f[x_] = x^2 . . g[f] ^= 2 Definition of a rather evolved (though meaningless) symbol: >> Attributes[r] := {Orderless} - >> Format[r[args___]] := Infix[{args}, "~"] + >> Format[r[args___]] := Infix[{args}, "#"] >> N[r] := 3.5 >> Default[r, 1] := 2 >> r::msg := "My message" @@ -250,7 +248,7 @@ class Definition(Builtin): Some usage: >> r[z, x, y] - = x ~ y ~ z + = x # y # z >> N[r] = 3.5 >> r[] @@ -262,23 +260,24 @@ class Definition(Builtin): >> Definition[r] = Attributes[r] = {Orderless} . - . arg_. ~ OptionsPattern[r] = {arg, OptionValue[Opt]} + . r[(arg_.), OptionsPattern[r]] = {arg, OptionValue[Opt]} . . N[r, MachinePrecision] = 3.5 . - . Format[args___, MathMLForm] = Infix[{args}, "~"] + . Format[r[args___], MathMLForm] = Infix[{args}, "#"] . - . Format[args___, OutputForm] = Infix[{args}, "~"] + . Format[r[args___], OutputForm] = Infix[{args}, "#"] . - . Format[args___, StandardForm] = Infix[{args}, "~"] + . Format[r[args___], StandardForm] = Infix[{args}, "#"] . - . Format[args___, TeXForm] = Infix[{args}, "~"] + . Format[r[args___], TeXForm] = Infix[{args}, "#"] . - . Format[args___, TraditionalForm] = Infix[{args}, "~"] + . Format[r[args___], TraditionalForm] = Infix[{args}, "#"] . . Default[r, 1] = 2 . - . Options[r] = {Opt -> 3} + .Options[r] = {Opt -> 3} + . For 'ReadProtected' symbols, 'Definition' just prints attributes, default values and options: >> SetAttributes[r, ReadProtected] @@ -408,12 +407,21 @@ class FormatValues(Builtin): :WMA link:https://reference.wolfram.com/language/tutorial/PatternsAndTransformationRules.html#6025
'FormatValues'[$symbol$] -
gives the list of formatvalues associated with $symbol$. +
gives the list of format rules associated with $symbol$.
+ First, use 'Format' to set a formatting rule for a form: + >> Format[F[x_], OutputForm]:= Subscript[x, F] + + Now, to see the rules, we can use 'FormatValues': + >> FormatValues[F] - = {HoldPattern[Format[Subscript[x_, F], OutputForm]] :> Subscript[x, F]} + = {HoldPattern[Subscript[x_, F]] :> Subscript[x, F]} + + The replacment pattern on the right in the delayed rule is formatted according to the top-level form. To see the rule input, we can use 'InputForm': + >> FormatValues[F] //InputForm + = {HoldPattern[Format[F[x_], OutputForm]] :> Subscript[x, F]} """ summary_text = ( diff --git a/mathics/builtin/box/__init__.py b/mathics/builtin/box/__init__.py index 29798d481..0a1faa519 100644 --- a/mathics/builtin/box/__init__.py +++ b/mathics/builtin/box/__init__.py @@ -5,7 +5,7 @@ Boxing information, like the class of expression to be worked on and \ its size, allow formatters, like :StandardForm: -/doc/reference-of-built-in-symbols/forms-of-input-and-output/printforms/standardform/ \ +/doc/reference-of-built-in-symbols/forms-of-input-and-output/general-purpose-forms/standardform/ \ to do their work without \ having to know more specific details and intricacies of expression to be formatted. diff --git a/mathics/builtin/box/expression.py b/mathics/builtin/box/expression.py index 476e5156d..6180b630f 100644 --- a/mathics/builtin/box/expression.py +++ b/mathics/builtin/box/expression.py @@ -81,6 +81,10 @@ def __new__(cls, *elements, **kwargs): instance._elements = None return instance + def __init(self, *args, **kwargs): + super().__init(args, kwargs) + self.boxes = [] + def do_format(self, evaluation, format): return self diff --git a/mathics/builtin/box/graphics.py b/mathics/builtin/box/graphics.py index 19a45dfeb..497df615c 100644 --- a/mathics/builtin/box/graphics.py +++ b/mathics/builtin/box/graphics.py @@ -2,8 +2,9 @@ """ Boxing Symbols for 2D Graphics """ -from math import atan2, ceil, cos, degrees, floor, log10, pi, sin -from typing import Optional +from abc import ABC +from math import atan2, cos, degrees, pi, sin +from typing import Any, Dict, Final, List, Optional, Tuple from mathics.builtin.box.expression import BoxExpression from mathics.builtin.colors.color_directives import ( @@ -12,46 +13,114 @@ RGBColor, _ColorObject, ) -from mathics.builtin.drawing.graphics_internals import GLOBALS, _GraphicsElementBox +from mathics.builtin.drawing.graphics_internals import GLOBALS from mathics.builtin.graphics import ( DEFAULT_POINT_FACTOR, Arrowheads, - Coords, Graphics, - GraphicsElements, PointSize, _BezierCurve, - _data_and_options, - _extract_graphics, _Line, _norm, - _Polyline, _to_float, - coords, ) -from mathics.core.atoms import Integer, Real, String +from mathics.core.atoms import Integer, Real from mathics.core.attributes import A_HOLD_ALL, A_PROTECTED, A_READ_PROTECTED +from mathics.core.element import BaseElement from mathics.core.exceptions import BoxExpressionError from mathics.core.expression import Expression from mathics.core.formatter import lookup_method from mathics.core.list import ListExpression -from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue -from mathics.core.systemsymbols import SymbolAutomatic, SymbolTraditionalForm +from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import SymbolInsetBox, SymbolTraditionalForm from mathics.format.box import format_element +from mathics.format.box.common import elements_to_expressions +from mathics.format.box.graphics import Coords, _data_and_options, coords # No user docs here: Box primitives aren't documented. no_doc = True SymbolRegularPolygonBox = Symbol("RegularPolygonBox") -SymbolStandardForm = Symbol("StandardForm") -# Note: has to come before _ArcBox -class _RoundBox(_GraphicsElementBox): +class GraphicsElementBox(BoxExpression, ABC): + def init(self, graphics, item=None, style={}, opacity=1.0): + if item is not None and not item.has_form(self.get_name(), None): + raise BoxExpressionError + self.graphics = graphics + self.style = style + self.opacity = opacity + self.is_completely_visible = False # True for axis elements + + +# GraphicsElementBox Builtin class that should not get added as a definition, +# and therefore not added to to external documentation. + +DOES_NOT_ADD_BUILTIN_DEFINITION: Final[List[BoxExpression]] = [GraphicsElementBox] + + +class _Polyline(GraphicsElementBox): + """ + A structure containing a list of line segments + stored in ``self.lines`` created from + a list of points. + + Lines are formed by pairs of consecutive point. + """ + + def do_init(self, graphics, points): + if not points.has_form("List", None): + raise BoxExpressionError + if ( + points.elements + and points.elements[0].has_form("List", None) + and all( + element.has_form("List", None) + for element in points.elements[0].elements + ) + ): + elements = points.elements + self.multi_parts = True + elif len(points.elements) == 0: + # Ensure there are no line segments if there are no points. + self.lines = [] + return + else: + elements = [ListExpression(*points.elements)] + self.multi_parts = False + lines = [] + for element in elements: + if element.has_form("List", None): + lines.append(element.elements) + else: + raise BoxExpressionError + self.lines = [ + [graphics.coords(graphics, point) for point in line] for line in lines + ] + + def extent(self) -> list: + lw = self.style.get_line_width(face_element=False) + result = [] + for line in self.lines: + for c in line: + x, y = c.pos() + result.extend( + [ + (x - lw, y - lw), + (x - lw, y + lw), + (x + lw, y - lw), + (x + lw, y + lw), + ] + ) + return result + + +# Note: has to come before ArcBox +class RoundBox(GraphicsElementBox): face_element: Optional[bool] = None def init(self, graphics, style, item): - super(_RoundBox, self).init(graphics, item, style) + super().init(graphics, item, style) if len(item.elements) not in (1, 2): raise BoxExpressionError self.edge_color, self.face_color = style.get_style( @@ -74,7 +143,7 @@ def init(self, graphics, style, item): def extent(self) -> list: """ - Compute the bounding box for _RoundBox. Note that + Compute the bounding box for RoundBox. Note that We handle ellipses here too. """ line_width = self.style.get_line_width(face_element=self.face_element) / 2 @@ -87,7 +156,7 @@ def extent(self) -> list: return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)] -class _ArcBox(_RoundBox): +class ArcBox(RoundBox): def init(self, graphics, style, item): if len(item.elements) == 3: arc_expr = item.elements[2] @@ -112,7 +181,7 @@ def init(self, graphics, style, item): item = Expression(Symbol(item.get_head_name()), *item.elements[:2]) else: self.arc = None - super(_ArcBox, self).init(graphics, style, item) + super().init(graphics, style, item) def _arc_params(self): x, y = self.c.pos() @@ -151,7 +220,7 @@ def init(self, graphics, style, item=None): if not item: raise BoxExpressionError - super(ArrowBox, self).init(graphics, item, style) + super().init(graphics, item, style) elements = item.elements if len(elements) == 2: @@ -295,6 +364,8 @@ def shrink(line, s1, s2): yield s def _custom_arrow(self, format, format_transform): + from mathics.format.box.graphics import _extract_graphics + def make(graphics): xmin, xmax, ymin, ymax, ox, oy, ex, ey, code = _extract_graphics( graphics, format, self.graphics.evaluation @@ -363,7 +434,7 @@ def init(self, graphics, style, item, options): self.spline_degree = spline_degree.get_int_value() -class CircleBox(_ArcBox): +class CircleBox(ArcBox): """
'CircleBox' @@ -375,7 +446,7 @@ class CircleBox(_ArcBox): summary_text = "is the symbol used in boxing 'Circle' expressions" -class DiskBox(_ArcBox): +class DiskBox(ArcBox): """
'DiskBox' @@ -399,14 +470,25 @@ class GraphicsBox(BoxExpression): options = Graphics.options summary_text = "symbol used in boxing 'Graphics'" - def __new__(cls, *elements, **kwargs): - instance = super().__new__(cls, *elements, **kwargs) - instance.evaluation = kwargs.get("evaluation", None) - instance.elements = elements - return instance + def init(self, *items, **kwargs): + self._elements: Optional[Tuple[BaseElement], ...] = None + self.content = items[0] + self.box_options: Dict[str, Any] = kwargs + self.background_color = None + self.tooltip_text: Optional[str] = None + self.evaluation = kwargs.pop("_evaluation", None) + self.boxwidth: int = -1 + self.boxheight: int = -1 + self.boxes: list = [] @property def elements(self): + if self._elements is None: + self._elements = elements_to_expressions( + self, + (self.content,), + self.box_options, + ) return self._elements @elements.setter @@ -414,504 +496,20 @@ def elements(self, value): self._elements = value return self._elements - def _get_image_size(self, options, graphics_options, max_width): - inside_row = options.pop("inside_row", False) - inside_list = options.pop("inside_list", False) - image_size_multipliers = options.pop("image_size_multipliers", None) - - aspect_ratio = graphics_options["System`AspectRatio"] - - if image_size_multipliers is None: - image_size_multipliers = (0.5, 0.25) - - if aspect_ratio is SymbolAutomatic: - aspect = None - else: - aspect = aspect_ratio.round_to_float() - - image_size = graphics_options["System`ImageSize"] - if isinstance(image_size, Integer): - base_width = image_size.get_int_value() - base_height = None # will be computed later in calc_dimensions - elif image_size.has_form("System`List", 2): - base_width, base_height = ( - [x.round_to_float() for x in image_size.elements] + [0, 0] - )[:2] - if base_width is None or base_height is None: - raise BoxExpressionError - aspect = base_height / base_width - else: - image_size = image_size.get_name() - base_width, base_height = { - "System`Automatic": (400, 350), - "System`Tiny": (100, 100), - "System`Small": (200, 200), - "System`Medium": (400, 350), - "System`Large": (600, 500), - }.get(image_size, (None, None)) - if base_width is None: - raise BoxExpressionError - if max_width is not None and base_width > max_width: - base_width = max_width - - if inside_row: - multi = image_size_multipliers[1] - elif inside_list: - multi = image_size_multipliers[0] - else: - multi = 1 - - return base_width, base_height, multi, aspect - - def _prepare_elements(self, elements, options, neg_y=False, max_width=None): - if not elements: - raise BoxExpressionError - self.graphics_options = self.get_option_values(elements[1:], **options) - background = self.graphics_options["System`Background"] - if ( - isinstance(background, Symbol) - and background.get_name() == "System`Automatic" - ): - self.background_color = None - else: - try: - self.background_color = _ColorObject.create(background) - except ColorError: - self.background_color = None - - base_width, base_height, size_multiplier, size_aspect = self._get_image_size( - options, self.graphics_options, max_width - ) - - plot_range = self.graphics_options["System`PlotRange"].to_python() - if plot_range == "System`Automatic": - plot_range = ["System`Automatic", "System`Automatic"] - - if not isinstance(plot_range, list) or len(plot_range) != 2: - raise BoxExpressionError - - evaluation = options.get("evaluation", None) - if evaluation is None: - evaluation = self.evaluation - elements = GraphicsElements(elements[0], evaluation, neg_y) - if hasattr(elements, "background_color"): - self.background_color = elements.background_color - if hasattr(elements, "tooltip_text"): - self.tooltip_text = elements.tooltip_text - - axes = [] # to be filled further down - - def calc_dimensions(final_pass=True): - """ - calc_dimensions gets called twice: In the first run - (final_pass = False, called inside _prepare_elements), the extent - of all user-defined graphics is determined. - Axes are created accordingly. - In the second run (final_pass = True, called from outside), - the dimensions of these axes are taken into account as well. - This is also important to size absolutely sized objects correctly - (e.g. values using AbsoluteThickness). - """ - - # always need to compute extent if size aspect is automatic - if "System`Automatic" in plot_range or size_aspect is None: - xmin, xmax, ymin, ymax = elements.extent() - else: - xmin = xmax = ymin = ymax = None - - if ( - final_pass - and any(x for x in axes) - and plot_range != ["System`Automatic", "System`Automatic"] - ): - # Take into account the dimensions of axes and axes labels - # (they should be displayed completely even when a specific - # PlotRange is given). - exmin, exmax, eymin, eymax = elements.extent( - completely_visible_only=True - ) - else: - exmin = exmax = eymin = eymax = None - - def get_range(min, max): - if max < min: - min, max = max, min - elif min == max: - if min < 0: - min, max = 2 * min, 0 - elif min > 0: - min, max = 0, 2 * min - else: - min, max = -1, 1 - return min, max - - try: - if plot_range[0] == "System`Automatic": - if xmin is None and xmax is None: - xmin = 0 - xmax = 1 - elif xmin == xmax: - xmin -= 1 - xmax += 1 - elif isinstance(plot_range[0], list) and len(plot_range[0]) == 2: - xmin, xmax = list(map(float, plot_range[0])) - xmin, xmax = get_range(xmin, xmax) - xmin = elements.translate((xmin, 0))[0] - xmax = elements.translate((xmax, 0))[0] - if exmin is not None and exmin < xmin: - xmin = exmin - if exmax is not None and exmax > xmax: - xmax = exmax - else: - raise BoxExpressionError - - if plot_range[1] == "System`Automatic": - if ymin is None and ymax is None: - ymin = 0 - ymax = 1 - elif ymin == ymax: - ymin -= 1 - ymax += 1 - elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2: - ymin, ymax = list(map(float, plot_range[1])) - ymin, ymax = get_range(ymin, ymax) - ymin = elements.translate((0, ymin))[1] - ymax = elements.translate((0, ymax))[1] - if ymin > ymax: - ymin, ymax = ymax, ymin - if eymin is not None and eymin < ymin: - ymin = eymin - if eymax is not None and eymax > ymax: - ymax = eymax - else: - raise BoxExpressionError - except (ValueError, TypeError): - raise BoxExpressionError - - w = 0 if (xmin is None or xmax is None) else xmax - xmin - h = 0 if (ymin is None or ymax is None) else ymax - ymin - - if size_aspect is None: - aspect = h / w - else: - aspect = size_aspect - - height = base_height - if height is None: - height = base_width * aspect - width = height / aspect - if width > base_width: - width = base_width - height = width * aspect - - width *= size_multiplier - height *= size_multiplier - - return xmin, xmax, ymin, ymax, w, h, width, height - - xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions(final_pass=False) - - elements.set_size(xmin, ymin, w, h, width, height) - - xmin -= w * 0.02 - xmax += w * 0.02 - ymin -= h * 0.02 - ymax += h * 0.02 - - axes.extend( - self.create_axes(elements, self.graphics_options, xmin, xmax, ymin, ymax) - ) - - return elements, calc_dimensions - - # FIXME: this doesn't always properly align with overlaid SVG plots - def axis_ticks(self, xmin, xmax): - def round_to_zero(value): - if value == 0: - return 0 - elif value < 0: - return ceil(value) - else: - return floor(value) - - def round_step(value): - if not value: - return 1, 1 - sub_steps = 5 - try: - shift = 10.0 ** floor(log10(value)) - except ValueError: - return 1, 1 - value = value / shift - if value < 1.5: - value = 1 - elif value < 3: - value = 2 - sub_steps = 4 - elif value < 8: - value = 5 - else: - value = 10 - return value * shift, sub_steps - - step_x, sub_x = round_step((xmax - xmin) / 5.0) - step_x_small = step_x / sub_x - steps_x = int(floor((xmax - xmin) / step_x)) - steps_x_small = int(floor((xmax - xmin) / step_x_small)) - - start_k_x = int(ceil(xmin / step_x)) - start_k_x_small = int(ceil(xmin / step_x_small)) - - if xmin <= 0 <= xmax: - origin_k_x = 0 - else: - origin_k_x = start_k_x - origin_x = origin_k_x * step_x - - ticks = [] - ticks_small = [] - for k in range(start_k_x, start_k_x + steps_x + 1): - if k != origin_k_x: - x = k * step_x - if x > xmax: - break - ticks.append(x) - for k in range(start_k_x_small, start_k_x_small + steps_x_small + 1): - if k % sub_x != 0: - x = k * step_x_small - if x > xmax: - break - ticks_small.append(x) - - return ticks, ticks_small, origin_x - def boxes_to_svg(self, elements=None, **options) -> str: """This is the top-level function that converts a Mathics Expression in to something suitable for SVG rendering. """ - if not elements: - elements = self._elements + assert elements is None - elements, calc_dimensions = self._prepare_elements( - elements, options, neg_y=True - ) - xmin, xmax, ymin, ymax, w, h, self.width, self.height = calc_dimensions() - data = (elements, xmin, xmax, ymin, ymax, w, h, self.width, self.height) - elements.view_width = w + elements = self.content format_fn = lookup_method(self, "svg") - svg_body = format_fn(self, elements, data=data, **options) + svg_body = format_fn(self, **options) return svg_body - def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax) -> tuple: - # Note that Asymptote has special commands for drawing axes, like "xaxis" - # "yaxis", "xtick" "labelx", "labely". Extend our language - # here and use those in render-like routines. - - use_log_for_y_axis = graphics_options.get( - "System`LogPlot", SymbolFalse - ).to_python() - axes_option = graphics_options.get("System`Axes") - - if axes_option is SymbolTrue: - axes = (True, True) - elif axes_option.has_form("List", 2): - axes = ( - axes_option.elements[0] is SymbolTrue, - axes_option.elements[1] is SymbolTrue, - ) - else: - axes = (False, False) - - # The Style option pushes its setting down into graphics components - # like ticks, axes, and labels. - ticks_style_option = graphics_options.get("System`TicksStyle") - axes_style_option = graphics_options.get("System`AxesStyle") - label_style = graphics_options.get("System`LabelStyle") - - if ticks_style_option.has_form("List", 2): - ticks_style = ticks_style_option.elements - else: - ticks_style = [ticks_style_option] * 2 - - if axes_style_option.has_form("List", 2): - axes_style = axes_style_option.elements - else: - axes_style = [axes_style_option] * 2 - - ticks_style = [elements.create_style(s) for s in ticks_style] - axes_style = [elements.create_style(s) for s in axes_style] - label_style = elements.create_style(label_style) - ticks_style[0].extend(axes_style[0]) - ticks_style[1].extend(axes_style[1]) - - def add_element(element): - element.is_completely_visible = True - elements.elements.append(element) - - # Units seem to be in point size units - - ticks_x, ticks_x_small, origin_x = self.axis_ticks(xmin, xmax) - ticks_y, ticks_y_small, origin_y = self.axis_ticks(ymin, ymax) - - axes_extra = 6 - - tick_small_size = 3 - tick_large_size = 5 - - tick_label_d = 2 - - ticks_x_int = all(floor(x) == x for x in ticks_x) - ticks_y_int = all(floor(x) == x for x in ticks_y) - - for ( - index, - ( - min, - max, - p_self0, - p_other0, - p_origin, - ticks, - ticks_small, - ticks_int, - is_logscale, - ), - ) in enumerate( - [ - ( - xmin, - xmax, - lambda y: (0, y), - lambda x: (x, 0), - lambda x: (x, origin_y), - ticks_x, - ticks_x_small, - ticks_x_int, - False, - ), - ( - ymin, - ymax, - lambda x: (x, 0), - lambda y: (0, y), - lambda y: (origin_x, y), - ticks_y, - ticks_y_small, - ticks_y_int, - use_log_for_y_axis, - ), - ] - ): - # Where should the placement of tick mark labels go? - if index == 0: - # x labels go under tick marks - alignment = "bottom" - elif index == 1: - # y labels go to the left of tick marks - alignment = "left" - else: - alignment = None - - if axes[index]: - add_element( - LineBox( - elements, - axes_style[index], - lines=[ - [ - Coords( - elements, pos=p_origin(min), d=p_other0(-axes_extra) - ), - Coords( - elements, pos=p_origin(max), d=p_other0(axes_extra) - ), - ] - ], - ) - ) - ticks_lines = [] - - tick_label_style = ticks_style[index].clone() - tick_label_style.extend(label_style) - - for x in ticks: - ticks_lines.append( - [ - Coords(elements, pos=p_origin(x)), - Coords( - elements, pos=p_origin(x), d=p_self0(tick_large_size) - ), - ] - ) - # FIXME: for log plots we labels should appear - # as 10^x rather than say 1000000. - tick_value = 10**x if is_logscale else x - if ticks_int: - content = String(str(int(tick_value))) - elif tick_value == floor(x): - content = String( - "%.1f" % tick_value - ) # e.g. 1.0 (instead of 1.) - else: - content = String( - "%g" % tick_value - ) # fix e.g. 0.6000000000000001 - - add_element( - InsetBox( - elements, - tick_label_style, - content=content, - pos=Coords( - elements, pos=p_origin(x), d=p_self0(-tick_label_d) - ), - opos=p_self0(1), - opacity=1.0, - alignment=alignment, - ) - ) - for x in ticks_small: - pos = p_origin(x) - ticks_lines.append( - [ - Coords(elements, pos=pos), - Coords(elements, pos=pos, d=p_self0(tick_small_size)), - ] - ) - add_element(LineBox(elements, axes_style[0], lines=ticks_lines)) - return axes - - # Old code? - # if axes[1]: - # add_element(LineBox(elements, axes_style[1], lines=[[Coords(elements, pos=(origin_x,ymin), d=(0,-axes_extra)), - # Coords(elements, pos=(origin_x,ymax), d=(0,axes_extra))]])) - # ticks = [] - # tick_label_style = ticks_style[1].clone() - # tick_label_style.extend(label_style) - # for k in range(start_k_y, start_k_y+steps_y+1): - # if k != origin_k_y: - # y = k * step_y - # if y > ymax: - # break - # pos = (origin_x,y) - # ticks.append([Coords(elements, pos=pos), - # Coords(elements, pos=pos, d=(tick_large_size,0))]) - # add_element(InsetBox(elements, tick_label_style, content=Real(y), pos=Coords(elements, pos=pos, - # d=(-tick_label_d,0)), opos=(1,0))) - # for k in range(start_k_y_small, start_k_y_small+steps_y_small+1): - # if k % sub_y != 0: - # y = k * step_y_small - # if y > ymax: - # break - # pos = (origin_x,y) - # ticks.append([Coords(elements, pos=pos), - # Coords(elements, pos=pos, d=(tick_small_size,0))]) - # add_element(LineBox(elements, axes_style[1], lines=ticks)) - - -class FilledCurveBox(_GraphicsElementBox): +class FilledCurveBox(GraphicsElementBox): """
'FilledCurveBox' @@ -991,7 +589,7 @@ def extent(self): return result -class InsetBox(_GraphicsElementBox): +class InsetBox(GraphicsElementBox): # We have no documentation for this (yet). no_doc = True @@ -1202,7 +800,7 @@ def process_option(self, name, value): raise BoxExpressionError -class RectangleBox(_GraphicsElementBox): +class RectangleBox(GraphicsElementBox): # We have no documentation for this (yet). no_doc = True @@ -1303,8 +901,8 @@ def vertices(): Symbol("ArrowBox"): ArrowBox, Symbol("CircleBox"): CircleBox, Symbol("PolygonBox"): PolygonBox, - Symbol("RegularPolygonBox"): RegularPolygonBox, + SymbolRegularPolygonBox: RegularPolygonBox, Symbol("PointBox"): PointBox, - Symbol("InsetBox"): InsetBox, + SymbolInsetBox: InsetBox, } ) diff --git a/mathics/builtin/box/graphics3d.py b/mathics/builtin/box/graphics3d.py index 84226eeee..6f8b69e2a 100644 --- a/mathics/builtin/box/graphics3d.py +++ b/mathics/builtin/box/graphics3d.py @@ -3,38 +3,21 @@ Boxing Symbols for 3D Graphics """ -import json -import logging -import numbers from mathics.builtin.box.graphics import ( ArrowBox, GraphicsBox, + GraphicsElementBox, LineBox, PointBox, PolygonBox, ) -from mathics.builtin.colors.color_directives import ( - ColorError, - Opacity, - RGBColor, - _ColorObject, -) -from mathics.builtin.drawing.graphics3d import ( - Coords3D, - Graphics3D, - Graphics3DElements, - Style3D, -) -from mathics.builtin.drawing.graphics_internals import ( - GLOBALS3D, - _GraphicsElementBox, - get_class, -) +from mathics.builtin.colors.color_directives import Opacity, RGBColor, _ColorObject +from mathics.builtin.drawing.graphics3d import Graphics3D, Style3D +from mathics.builtin.drawing.graphics_internals import GLOBALS3D from mathics.core.exceptions import BoxExpressionError -from mathics.core.formatter import lookup_method -from mathics.core.symbols import Symbol, SymbolTrue -from mathics.eval.nevaluator import eval_N +from mathics.core.symbols import Symbol +from mathics.format.box.graphics3d import expr_to_list_of_3d_points # No user docs here - Box primitives aren't documented. no_doc = True @@ -51,476 +34,26 @@ class Graphics3DBox(GraphicsBox): options = Graphics3D.options summary_text = "symbol used boxing Graphics3D expressions" - def _prepare_elements(self, elements, options, max_width=None): - if not elements: - raise BoxExpressionError - - self.graphics_options = self.get_option_values(elements[1:], **options) - - background = self.graphics_options["System`Background"] - if ( - isinstance(background, Symbol) - and background.get_name() == "System`Automatic" - ): - self.background_color = None - else: - try: - self.background_color = _ColorObject.create(background) - except ColorError: - logging.warning(f"{str(background)} is not a valid color spec.") - self.background_color = None - - evaluation = options["evaluation"] - - base_width, base_height, size_multiplier, size_aspect = self._get_image_size( - options, self.graphics_options, max_width - ) - - # TODO: Handle ImageScaled[], and Scaled[] - lighting_option = self.graphics_options["System`Lighting"] - lighting = lighting_option.to_python() # can take symbols or strings + def init(self, *items, **kwargs): + super().init(*items, **kwargs) self.lighting = [] + self.viewpoint = [1.3, -2.4, 2] - if lighting == "System`Automatic": - self.lighting = [ - {"type": "Ambient", "color": [0.3, 0.2, 0.4]}, - { - "type": "Directional", - "color": [0.8, 0.0, 0.0], - "position": [2, 0, 2], - }, - { - "type": "Directional", - "color": [0.0, 0.8, 0.0], - "position": [2, 2, 2], - }, - { - "type": "Directional", - "color": [0.0, 0.0, 0.8], - "position": [0, 2, 2], - }, - ] - elif lighting == "Neutral": # Lighting->"Neutral" - self.lighting = [ - {"type": "Ambient", "color": [0.3, 0.3, 0.3]}, - { - "type": "Directional", - "color": [0.3, 0.3, 0.3], - "position": [2, 0, 2], - }, - { - "type": "Directional", - "color": [0.3, 0.3, 0.3], - "position": [2, 2, 2], - }, - { - "type": "Directional", - "color": [0.3, 0.3, 0.3], - "position": [0, 2, 2], - }, - ] - elif lighting == "System`None": - pass - - elif isinstance(lighting, list) and all( - isinstance(light, list) for light in lighting - ): - for light in lighting: - if light[0] in ['"Ambient"', '"Directional"', '"Point"', '"Spot"']: - try: - head = light[1].get_head_name() - except AttributeError: - break - color = get_class(head)(light[1]) - if light[0] == '"Ambient"': - self.lighting.append( - {"type": "Ambient", "color": color.to_rgba()} - ) - elif light[0] == '"Directional"': - position = [0, 0, 0] - if isinstance(light[2], list): - if len(light[2]) == 3: - position = light[2] - if len(light[2]) == 2 and all( # noqa - isinstance(p, list) and len(p) == 3 for p in light[2] - ): - position = [ - light[2][0][i] - light[2][1][i] for i in range(3) - ] - self.lighting.append( - { - "type": "Directional", - "color": color.to_rgba(), - "position": position, - } - ) - elif light[0] == '"Point"': - position = [0, 0, 0] - if isinstance(light[2], list) and len(light[2]) == 3: - position = light[2] - self.lighting.append( - { - "type": "Point", - "color": color.to_rgba(), - "position": position, - } - ) - elif light[0] == '"Spot"': - position = [0, 0, 1] - target = [0, 0, 0] - if isinstance(light[2], list): - if len(light[2]) == 2: - if ( - isinstance(light[2][0], list) - and len(light[2][0]) == 3 # noqa - ): - position = light[2][0] - if ( - isinstance(light[2][1], list) - and len(light[2][1]) == 3 # noqa - ): - target = light[2][1] - if len(light[2]) == 3: - position = light[2] - angle = light[3] - self.lighting.append( - { - "type": "Spot", - "color": color.to_rgba(), - "position": position, - "target": target, - "angle": angle, - } - ) - - else: - evaluation.message("Graphics3D", "invlight", lighting_option) - - # ViewPoint Option - viewpoint_option = self.graphics_options["System`ViewPoint"] - viewpoint = eval_N(viewpoint_option, evaluation).to_python() - - if isinstance(viewpoint, list) and len(viewpoint) == 3: - if all(isinstance(x, numbers.Real) for x in viewpoint): - # TODO Infinite coordinates e.g. {0, 0, Infinity} - pass - else: - try: - viewpoint = { - "Above": [0, 0, 2], - "Below": [0, 0, -2], - "Front": [0, -2, 0], - "Back": [0, 2, 0], - "Left": [-2, 0, 0], - "Right": [2, 0, 0], - }[viewpoint] - except KeyError: - # evaluation.message() - # TODO - viewpoint = [1.3, -2.4, 2] - - assert ( - isinstance(viewpoint, list) - and len(viewpoint) == 3 - and all(isinstance(x, numbers.Real) for x in viewpoint) - ) - self.viewpoint = viewpoint - - # TODO Aspect Ratio - # aspect_ratio = self.graphics_options['AspectRatio'].to_python() - - boxratios = self.graphics_options["System`BoxRatios"].to_python() - if boxratios == "System`Automatic": - boxratios = ["System`Automatic"] * 3 - - if not isinstance(boxratios, list) or len(boxratios) != 3: - raise BoxExpressionError - - plot_range = self.graphics_options["System`PlotRange"].to_python() - if plot_range == "System`Automatic": - plot_range = ["System`Automatic"] * 3 - if not isinstance(plot_range, list) or len(plot_range) != 3: - raise BoxExpressionError - - elements = Graphics3DElements(elements[0], evaluation) - # If one of the primitives or directives fails to be - # converted into a box expression, then the background color - # is set to pink, overwriting the options. - if hasattr(elements, "background_color"): - self.background_color = elements.background_color - - def calc_dimensions(final_pass=True): - # TODO: the code below is broken in any other case but Automatic - # because it calls elements.translate which is not implemented. - # Plots may pass specific plot ranges, triggering this deficiency - # and causing tests to fail The following line avoids this, - # and it should not change the behavior of any case which did - # previously fail with an exception. - # - # This code should be DRYed (together with the very similar code - # for the 2d case), and the missing .translate method added. - plot_range = ["System`Automatic"] * 3 - - if "System`Automatic" in plot_range: - xmin, xmax, ymin, ymax, zmin, zmax = elements.extent() - else: - xmin = xmax = ymin = ymax = zmin = zmax = None - - try: - if plot_range[0] == "System`Automatic": - if xmin is None and xmax is None: - xmin = 0 - xmax = 1 - elif xmin == xmax: - xmin -= 1 - xmax += 1 - elif isinstance(plot_range[0], list) and len(plot_range[0]) == 2: - xmin, xmax = list(map(float, plot_range[0])) - xmin = elements.translate((xmin, 0, 0))[0] - xmax = elements.translate((xmax, 0, 0))[0] - else: - raise BoxExpressionError - - if plot_range[1] == "System`Automatic": - if ymin is None and ymax is None: - ymin = 0 - ymax = 1 - elif ymin == ymax: - ymin -= 1 - ymax += 1 - elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2: - ymin, ymax = list(map(float, plot_range[1])) - ymin = elements.translate((0, ymin, 0))[1] - ymax = elements.translate((0, ymax, 0))[1] - else: - raise BoxExpressionError - - if plot_range[2] == "System`Automatic": - if zmin is None and zmax is None: - zmin = 0 - zmax = 1 - elif zmin == zmax: - zmin -= 1 - zmax += 1 - elif isinstance(plot_range[1], list) and len(plot_range[2]) == 2: - zmin, zmax = list(map(float, plot_range[2])) - zmin = elements.translate((0, 0, zmin))[2] - zmax = elements.translate((0, 0, zmax))[2] - else: - raise BoxExpressionError - except (ValueError, TypeError): - raise BoxExpressionError - - boxscale = [1.0, 1.0, 1.0] - if boxratios[0] != "System`Automatic": - boxscale[0] = boxratios[0] / (xmax - xmin) - if boxratios[1] != "System`Automatic": - boxscale[1] = boxratios[1] / (ymax - ymin) - if boxratios[2] != "System`Automatic": - boxscale[2] = boxratios[2] / (zmax - zmin) - - if final_pass: - xmin *= boxscale[0] - xmax *= boxscale[0] - ymin *= boxscale[1] - ymax *= boxscale[1] - zmin *= boxscale[2] - zmax *= boxscale[2] - - # Rescale lighting - for i, light in enumerate(self.lighting): - if self.lighting[i]["type"] != "Ambient": - self.lighting[i]["position"] = [ - light["position"][j] * boxscale[j] for j in range(3) - ] - if self.lighting[i]["type"] == "Spot": - self.lighting[i]["target"] = [ - light["target"][j] * boxscale[j] for j in range(3) - ] - - w = 0 if (xmin is None or xmax is None) else xmax - xmin - h = 0 if (ymin is None or ymax is None) else ymax - ymin - - return xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h - - xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h = calc_dimensions( - final_pass=False - ) - - axes, ticks, ticks_style = self.create_axes( - elements, - self.graphics_options, - xmin, - xmax, - ymin, - ymax, - zmin, - zmax, - boxscale, - ) + def _prepare_elements(self, elements, options, max_width=None): + from mathics.format.box.graphics3d import prepare_elements - return elements, axes, ticks, ticks_style, calc_dimensions, boxscale + return prepare_elements(self, elements, options, max_width) def boxes_to_js(self, elements=None, **options): """Turn the Graphics3DBox to into a something javascript-ish We include enclosing script tagging. """ - json_repr = self.boxes_to_json(elements, **options) + from mathics.format.render.json import graphics3d_boxes_to_json + + json_repr = graphics3d_boxes_to_json(self, elements, **options) js = f"" return js - def boxes_to_json(self, elements=None, **options): - """Turn the Graphics3DBox to into a something JSON like. - This can be used to embed in something else like MathML or Javascript. - - In contrast to to javascript or MathML, no enclosing tags are included. - the caller will do that if it is needed. - """ - if not elements: - elements = self._elements - - ( - elements, - axes, - ticks, - ticks_style, - calc_dimensions, - boxscale, - ) = self._prepare_elements(elements, options) - - background = "rgba(100.0%, 100.0%, 100.0%, 100.0%)" - if self.background_color: - components = self.background_color.to_rgba() - if len(components) == 3: - background = "rgb(" + ", ".join(f"{100*c}%" for c in components) + ")" - else: - background = "rgba(" + ", ".join(f"{100*c}%" for c in components) + ")" - - tooltip_text = ( - elements.tooltip_text if hasattr(elements, "tooltip_text") else "" - ) - - js_ticks_style = [s.to_js() for s in ticks_style] - - elements._apply_boxscaling(boxscale) - - xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h = calc_dimensions() - elements.view_width = w - - # FIXME: json is the only thing we can convert MathML into. - # Handle other graphics formats. - format_fn = lookup_method(elements, "json") - - json_repr = json.dumps( - { - "elements": format_fn(elements, **options), - "background_color": background, - "tooltip_text": tooltip_text, - "axes": { - "hasaxes": axes, - "ticks": ticks, - "ticks_style": js_ticks_style, - }, - "extent": { - "xmin": xmin, - "xmax": xmax, - "ymin": ymin, - "ymax": ymax, - "zmin": zmin, - "zmax": zmax, - }, - "lighting": self.lighting, - "viewpoint": self.viewpoint, - "protocol": "1.1", - } - ) - - return json_repr - - def create_axes( - self, elements, graphics_options, xmin, xmax, ymin, ymax, zmin, zmax, boxscale - ): - axes = graphics_options.get("System`Axes") - if axes is SymbolTrue: - axes = (True, True, True) - elif axes.has_form("List", 3): - axes = (element is SymbolTrue for element in axes.elements) - else: - axes = (False, False, False) - ticks_style = graphics_options.get("System`TicksStyle") - axes_style = graphics_options.get("System`AxesStyle") - label_style = graphics_options.get("System`LabelStyle") - - # FIXME: Doesn't handle GrayScale - if ticks_style.has_form("List", 1, 2, 3): - ticks_style = ticks_style.elements - elif ticks_style.has_form("RGBColor", None): - ticks_style = [ticks_style] * 3 - else: - ticks_style = [] - - if axes_style.has_form("List", 1, 2, 3): - axes_style = axes_style.elements - else: - axes_style = [axes_style] * 3 - - # FIXME: Not quite right. We only handle color - ticks_style = [ - elements.create_style(s).get_style(_ColorObject, face_element=False)[0] - for s in ticks_style - ] - - axes_style = [elements.create_style(s) for s in axes_style] - label_style = elements.create_style(label_style) - - # For later - # ticks_style[0].extend(axes_style[0]) - # ticks_style[1].extend(axes_style[1]) - # ticks_style[2].extend(axes_style[2]) - - ticks = [ - self.axis_ticks(xmin, xmax), - self.axis_ticks(ymin, ymax), - self.axis_ticks(zmin, zmax), - ] - - # Add zero if required, since axis_ticks does not - if xmin <= 0 <= xmax: - ticks[0][0].append(0.0) - if ymin <= 0 <= ymax: - ticks[1][0].append(0.0) - if zmin <= 0 <= zmax: - ticks[2][0].append(0.0) - - # Convert ticks to nice strings e.g 0.100000000000002 -> '0.1' and - # scale ticks appropriately - ticks = [ - [ - [boxscale[i] * x for x in t[0]], - [boxscale[i] * x for x in t[1]], - ["%g" % x for x in t[0]], - ] - for i, t in enumerate(ticks) - ] - - return axes, ticks, ticks_style - - def get_boundbox_lines(self, xmin, xmax, ymin, ymax, zmin, zmax): - return [ - [(xmin, ymin, zmin), (xmax, ymin, zmin)], - [(xmin, ymax, zmin), (xmax, ymax, zmin)], - [(xmin, ymin, zmax), (xmax, ymin, zmax)], - [(xmin, ymax, zmax), (xmax, ymax, zmax)], - [(xmin, ymin, zmin), (xmin, ymax, zmin)], - [(xmax, ymin, zmin), (xmax, ymax, zmin)], - [(xmin, ymin, zmax), (xmin, ymax, zmax)], - [(xmax, ymin, zmax), (xmax, ymax, zmax)], - [(xmin, ymin, zmin), (xmin, ymin, zmax)], - [(xmax, ymin, zmin), (xmax, ymin, zmax)], - [(xmin, ymax, zmin), (xmin, ymax, zmax)], - [(xmax, ymax, zmin), (xmax, ymax, zmax)], - ] - class Arrow3DBox(ArrowBox): # We have no documentation for this (yet). @@ -542,7 +75,7 @@ def _apply_boxscaling(self, boxscale): coords.scale(boxscale) -class Cone3DBox(_GraphicsElementBox): +class Cone3DBox(GraphicsElementBox): # """ # Internal Python class used when Boxing a 'Cone' object. # """ @@ -560,14 +93,7 @@ def init(self, graphics, style, item): if len(item.elements) != 2: raise BoxExpressionError - points = item.elements[0].to_python() - if not all( - len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) - for point in points - ): - raise BoxExpressionError - - self.points = tuple(Coords3D(graphics, pos=point) for point in points) + self.points = expr_to_list_of_3d_points(item.elements[0]) self.radius = item.elements[1].to_python() def extent(self): @@ -593,7 +119,7 @@ def _apply_boxscaling(self, boxscale): pass -class Cuboid3DBox(_GraphicsElementBox): +class Cuboid3DBox(GraphicsElementBox): # """ # Internal Python class used when Boxing a 'Cuboid' object. # """ @@ -611,14 +137,7 @@ def init(self, graphics, style, item): if len(item.elements) != 1: raise BoxExpressionError - points = item.elements[0].to_python() - if not all( - len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) - for point in points - ): - raise BoxExpressionError - - self.points = tuple(Coords3D(pos=point) for point in points) + self.points = expr_to_list_of_3d_points(item.elements[0]) def extent(self): return [coords.pos()[0] for coords in self.points] @@ -628,7 +147,7 @@ def _apply_boxscaling(self, boxscale): pass -class Cylinder3DBox(_GraphicsElementBox): +class Cylinder3DBox(GraphicsElementBox): # """ # Internal Python class used when Boxing a 'Cylinder' object. # """ @@ -646,14 +165,7 @@ def init(self, graphics, style, item): if len(item.elements) != 2: raise BoxExpressionError - points = item.elements[0].to_python() - if not all( - len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) - for point in points - ): - raise BoxExpressionError - - self.points = tuple(Coords3D(pos=point) for point in points) + self.points = expr_to_list_of_3d_points(item.elements[0]) self.radius = item.elements[1].to_python() def extent(self): @@ -759,7 +271,7 @@ def _apply_boxscaling(self, boxscale): coords.scale(boxscale) -class Sphere3DBox(_GraphicsElementBox): +class Sphere3DBox(GraphicsElementBox): # summary_text = "box representation for a sphere" # We have no documentation for this (yet). @@ -775,16 +287,7 @@ def init(self, graphics, style, item): if len(item.elements) != 2: raise BoxExpressionError - points = item.elements[0].to_python() - if not all(isinstance(point, list) for point in points): - points = [points] - if not all( - len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) - for point in points - ): - raise BoxExpressionError - - self.points = tuple(Coords3D(pos=point) for point in points) + self.points = expr_to_list_of_3d_points(item.elements[0]) self.radius = item.elements[1].to_python() def extent(self): @@ -808,7 +311,7 @@ def _apply_boxscaling(self, boxscale): pass -class Tube3DBox(_GraphicsElementBox): +class Tube3DBox(GraphicsElementBox): # summary_text = "box representation for a tube" # We have no documentation for this (yet). @@ -822,14 +325,7 @@ def init(self, graphics, style, item): self.edge_opacity, self.face_opacity = style.get_style( Opacity, face_element=True ) - points = item.elements[0].to_python() - if not all( - len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) - for point in points - ): - raise BoxExpressionError - - self.points = [Coords3D(graphics, pos=point) for point in points] + self.points = expr_to_list_of_3d_points(item.elements[0]) self.radius = item.elements[1].to_python() def extent(self): diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index 4c1c33c55..d198db8b2 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -7,7 +7,7 @@ The routines here assist in boxing at the bottom of the hierarchy, typically found when used via a notebook. -Boxing is recursively performed using on the :Head:/doc/reference-of-built-in-symbols/atomic-elements-of-expressions/atomic-primitives/head/ of a \Mathics expression +Boxing is recursively performed using on the :Head:/doc/reference-of-built-in-symbols/atomic-elements-of-expressions/atomic-primitives/head/ of a \Mathics expression. """ # The Box objects are `BoxElementMixin` objects. These objects are literal @@ -16,39 +16,23 @@ # output. -from typing import Tuple - from mathics.builtin.box.expression import BoxExpression -from mathics.builtin.options import filter_non_default_values, options_to_rules from mathics.core.atoms import String from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin -from mathics.core.element import BaseElement, BoxElementMixin, EvalMixin +from mathics.core.element import BoxElementMixin, EvalMixin from mathics.core.evaluation import Evaluation from mathics.core.exceptions import BoxConstructError from mathics.core.expression import Expression from mathics.core.list import ListExpression from mathics.core.symbols import Symbol from mathics.format.box import to_boxes +from mathics.format.box.common import elements_to_expressions # This tells documentation how to sort this module sort_order = "mathics.builtin.low-level-notebook-structure" -def elements_to_expressions( - self: BoxExpression, elements: Tuple[BaseElement], options: dict -) -> Tuple[BaseElement]: - """ - Return a tuple of Mathics3 normal atoms or expressions. - """ - opts = sorted(options_to_rules(options, filter_non_default_values(self))) - expr_elements = [ - elem.to_expression() if isinstance(elem, BoxExpression) else elem - for elem in elements - ] - return tuple(expr_elements + opts) - - class BoxData(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/BoxData.html @@ -85,6 +69,73 @@ def is_constant_list(list): return True +class FormBox(BoxExpression): + r""" + :WMA link: + https://reference.wolfram.com/language/ref/FormBox.html + +
+
'FormBox'[$boxes$, $form$] +
is a low-level boxing construct that wraps $boxes$ and $form$ into a box. \ + 'form' must be one of the forms in '$BoxForms' list. +
+ + ## No examples because our implementation and understanding of the concept + ## may be lacking. See https://github.com/Mathics3/mathics-core/pull/1653 + ## for the sordid discussion. + """ + + # FormBox provides a way to tell the interpreter in `ToExpression` + # how to interpret the 'boxes' expression to reconstruct + # an expression. For example, the box expression + # `RowBox[{"Sin", "(","Pi", ")"}]` + # is interpreted in `StandardForm` as `Times[Sin, Pi]`. + # However, if it is enclosed in `FormBox[..., TraditionalForm]` + # it is interpreted as `Sin[Pi]`. + # + # It also has effect in how the WMA notebook interface renders + # the box expression: variables in `TraditionalForm` are shown + # in italics, while in other forms are shown in regular a regular + # font. + # On the other hand, the form does not have any effect on + # `ToString`, `MathMLForm` and `TeXForm`, so at the render level, + # we can not notice any difference in the currently available + # Mathics3 frontends. + + attributes = A_PROTECTED | A_READ_PROTECTED + summary_text = "wrap boxes with an association to a particular form" + + def init(self, *elems, **kwargs): + self.box_options = kwargs + self.form = elems[1] + self.boxes = elems[0] + assert isinstance(self.boxes, BoxElementMixin), f"{type(self.boxes)}" + + @property + def elements(self): + if self._elements is None: + self._elements = elements_to_expressions( + self, + ( + self.boxes, + self.form, + ), + self.box_options, + ) + return self._elements + + def eval_tagbox(self, expr, form: Symbol, evaluation: Evaluation): + """FormBox[expr_, form_Symbol]""" + options = {} + expr = to_boxes(expr, evaluation, options) + assert isinstance(expr, BoxElementMixin), f"{expr}" + return FormBox(expr, form, **options) + + @property + def is_multiline(self) -> bool: + return self.boxes.is_multiline + + class FractionBox(BoxExpression): """ @@ -165,7 +216,7 @@ def elements(self): return self._elements def init(self, *elems, **kwargs): - self.options = kwargs + self.box_options = kwargs self.items = elems self._elements = elems @@ -173,7 +224,7 @@ def get_array(self, elements, evaluation): if not elements: raise BoxConstructError - options = self.options + options = self.box_options expr = elements[0] if not expr.has_form("List", None): @@ -225,12 +276,12 @@ class InterpretationBox(BoxExpression): summary_text = "box associated to an input expression" def __repr__(self): - result = "InterpretationBox\n " + repr(self.boxed) + result = "InterpretationBox\n " + repr(self.boxes) result += f"\n {self.box_options}" return result def init(self, *expr, **options): - self.boxed = expr[0] + self.boxes = expr[0] self.expr = expr[1] self.box_options = options @@ -240,7 +291,7 @@ def elements(self): self._elements = elements_to_expressions( self, ( - self.boxed, + self.boxes, self.expr, ), self.box_options, @@ -270,7 +321,11 @@ def eval_to_expression2(self, boxexpr, form, evaluation): def eval_display(self, boxexpr, evaluation): """DisplayForm[boxexpr_InterpretationBox]""" - return boxexpr.boxed + return boxexpr.boxes + + @property + def is_multiline(self) -> bool: + return self.boxes.is_multiline class PaneBox(BoxExpression): @@ -294,12 +349,12 @@ class PaneBox(BoxExpression): def elements(self): if self._elements is None: self._elements = elements_to_expressions( - self, (self.boxed,), self.box_options + self, (self.boxes,), self.box_options ) return self._elements def init(self, expr, **options): - self.boxed = expr + self.boxes = expr self.box_options = options def eval_panebox1(self, expr, evaluation, options): @@ -316,6 +371,10 @@ def eval_display(boxexpr, evaluation): """DisplayForm[boxexpr_PaneBox]""" return boxexpr.elements[0] + @property + def is_multiline(self) -> bool: + return self.boxes.is_multiline + class RowBox(BoxExpression): """ @@ -359,6 +418,9 @@ def eval_list(self, boxes, evaluation): def init(self, *items, **kwargs): # TODO: check that each element is an string or a BoxElementMixin self.box_options = {} + if len(items) == 0: + self.items = tuple() + return if isinstance(items[0], Expression): if len(items) != 1: raise Exception( @@ -382,6 +444,10 @@ def check_item(item): self.items = tuple((check_item(item) for item in items)) + @property + def is_multiline(self) -> bool: + return any(item.is_multiline for item in self.items) + class ShowStringCharacters(Builtin): """ @@ -470,8 +536,6 @@ class StyleBox(BoxExpression): """ options = { - "ShowStringCharacters": "False", - "ShowSpecialCharacters": "False", "$OptionSyntax": "Ignore", } attributes = A_PROTECTED | A_READ_PROTECTED @@ -526,6 +590,10 @@ def init(self, boxes, style=None, **options): self.boxes, BoxElementMixin ), f"{type(self.boxes)},{self.boxes}" + @property + def is_multiline(self) -> bool: + return self.boxes.is_multiline + class SubscriptBox(BoxExpression): """ @@ -684,8 +752,8 @@ class TagBox(BoxExpression): def init(self, *elems, **kwargs): self.box_options = kwargs self.form = elems[1] - self.boxed = elems[0] - assert isinstance(self.boxed, BoxElementMixin), f"{type(self.boxes)}" + self.boxes = elems[0] + assert isinstance(self.boxes, BoxElementMixin), f"{type(self.boxes)}" @property def elements(self): @@ -693,7 +761,7 @@ def elements(self): self._elements = elements_to_expressions( self, ( - self.boxed, + self.boxes, self.form, ), self.box_options, @@ -707,6 +775,10 @@ def eval_tagbox(self, expr, form: Symbol, evaluation: Evaluation): assert isinstance(expr, BoxElementMixin), f"{expr}" return TagBox(expr, form, **options) + @property + def is_multiline(self) -> bool: + return self.boxes.is_multiline + class TemplateBox(BoxExpression): """ diff --git a/mathics/builtin/box/uniform_polyhedra.py b/mathics/builtin/box/uniform_polyhedra.py index 19db41550..2644ea6b8 100644 --- a/mathics/builtin/box/uniform_polyhedra.py +++ b/mathics/builtin/box/uniform_polyhedra.py @@ -1,8 +1,9 @@ import numbers -from mathics.builtin.box.graphics3d import Coords3D +from mathics.builtin.box.graphics import GraphicsElementBox from mathics.builtin.colors.color_directives import Opacity, _ColorObject -from mathics.builtin.drawing.graphics_internals import GLOBALS3D, _GraphicsElementBox +from mathics.builtin.drawing.graphics3d import Coords3D +from mathics.builtin.drawing.graphics_internals import GLOBALS3D from mathics.core.exceptions import BoxExpressionError from mathics.core.symbols import Symbol @@ -10,7 +11,7 @@ no_doc = True -class UniformPolyhedron3DBox(_GraphicsElementBox): +class UniformPolyhedron3DBox(GraphicsElementBox): # Let's overwrite the default summary_text here, # to recover the spaces. summary_text = "box representation of a 3d uniform polyhedron" diff --git a/mathics/builtin/datentime.py b/mathics/builtin/datentime.py index c89be0132..659cc8ad0 100644 --- a/mathics/builtin/datentime.py +++ b/mathics/builtin/datentime.py @@ -593,7 +593,7 @@ class DateObject(_DateFormat, ImmutableValueMixin):
>> DateObject[{2020, 4, 15}] - = [...] + = ... """ fmt_keywords = { @@ -697,7 +697,7 @@ def eval_makeboxes( fmt: BaseElement, evaluation: Evaluation, ) -> Optional[Expression]: - "MakeBoxes[DateObject[datetime_List, gran_, cal_, tz_, fmt_], StandardForm|TraditionalForm|OutputForm]" + "MakeBoxes[DateObject[datetime_List, gran_, cal_, tz_, fmt_], StandardForm|TraditionalForm]" # TODO: if fmt.sameQ(SymbolAutomatic): fmt = ListExpression(String("DateTimeShort")) diff --git a/mathics/builtin/drawing/__init__.py b/mathics/builtin/drawing/__init__.py index 12c56c163..292e61179 100644 --- a/mathics/builtin/drawing/__init__.py +++ b/mathics/builtin/drawing/__init__.py @@ -10,15 +10,15 @@ :'Circle': /doc/reference-of-built-in-symbols/drawing-graphics/circle and :'Cuboid': -/doc/reference-of-built-in-symbols/graphics-and-drawing/three-dimensional-graphics/cuboid/ \ +/doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/three-dimensional-graphics/cuboid/ \ and place them in a coordinate space.
  • Compute the points of the space using a function. This is done using functions \ like :'Plot': - /doc/reference-of-built-in-symbols/graphics-and-drawing/plotting-data/plot \ + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/general-graphical-plots/plot \ and :'ListPlot': - /doc/reference-of-built-in-symbols/graphics-and-drawing/plotting-data/listplot. + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/list-plots/listplot. """ diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index 5caa0b1c3..0502ae3d5 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -6,17 +6,13 @@ """ from mathics.builtin.colors.color_directives import RGBColor -from mathics.builtin.graphics import ( - CoordinatesError, - Graphics, - Style, - _GraphicsElements, -) +from mathics.builtin.graphics import Graphics from mathics.core.atoms import Integer, Rational, Real from mathics.core.builtin import Builtin from mathics.core.expression import Evaluation, Expression from mathics.core.symbols import SymbolN from mathics.eval.nevaluator import eval_N +from mathics.format.box.graphics import CoordinatesError, Style, _GraphicsElements # This tells documentation how to sort this module # Here we are also hiding "drawing" since this erroneously appears at the top level. @@ -70,7 +66,7 @@ class Graphics3D(Graphics):
    represents a three-dimensional graphic. See :Drawing Option and Option Values: - /doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/drawing-options-and-option-values for a list of Plot options.
  • @@ -107,6 +103,7 @@ class Graphics3D(Graphics): . draw(((1,1,-1)--(1,1,1)), rgb(0.4, 0.4, 0.4)+linewidth(1)); . \end{asy} """ + summary_text = "a three-dimensional graphics image wrapper" options = Graphics.options.copy() options.update( @@ -140,11 +137,6 @@ class Graphics3D(Graphics): messages = {"invlight": "`1` is not a valid list of light sources."} - rules = { - "MakeBoxes[Graphics3D[content_, OptionsPattern[Graphics3D]], " - " OutputForm]": '"-Graphics3D-"' - } - def total_extent_3d(extents): xmin = xmax = ymin = ymax = zmin = zmax = None diff --git a/mathics/builtin/drawing/graphics_internals.py b/mathics/builtin/drawing/graphics_internals.py index adb5c27aa..1187cbe06 100644 --- a/mathics/builtin/drawing/graphics_internals.py +++ b/mathics/builtin/drawing/graphics_internals.py @@ -5,7 +5,6 @@ from abc import ABC -from mathics.builtin.box.expression import BoxExpression from mathics.core.builtin import BuiltinElement from mathics.core.exceptions import BoxExpressionError from mathics.core.symbols import Symbol, system_symbols_dict @@ -25,16 +24,6 @@ def create_as_style(klass, graphics, item): return klass(graphics, item) -class _GraphicsElementBox(BoxExpression, ABC): - def init(self, graphics, item=None, style={}, opacity=1.0): - if item is not None and not item.has_form(self.get_name(), None): - raise BoxExpressionError - self.graphics = graphics - self.style = style - self.opacity = opacity - self.is_completely_visible = False # True for axis elements - - def get_class(symbol: Symbol): """ Returns the Builtin graphic primitive associated to the diff --git a/mathics/builtin/drawing/plot_chart.py b/mathics/builtin/drawing/plot_chart.py index a21365dcc..45cbdad4e 100644 --- a/mathics/builtin/drawing/plot_chart.py +++ b/mathics/builtin/drawing/plot_chart.py @@ -20,8 +20,10 @@ SymbolEdgeForm, SymbolGraphics, SymbolStyle, + SymbolText, ) from mathics.eval.drawing.charts import draw_bar_chart, eval_chart +from mathics.eval.drawing.plot import TwoTenths from mathics.eval.nevaluator import eval_N # This tells documentation how to sort this module @@ -29,9 +31,7 @@ SymbolDisk = Symbol("Disk") SymbolFaceForm = Symbol("FaceForm") -SymbolText = Symbol("Text") -TwoTenths = Real(0.2) MTwoTenths = -TwoTenths diff --git a/mathics/builtin/drawing/plot_listplot.py b/mathics/builtin/drawing/plot_listplot.py index 22fafa5f9..7ff1468eb 100644 --- a/mathics/builtin/drawing/plot_listplot.py +++ b/mathics/builtin/drawing/plot_listplot.py @@ -162,13 +162,13 @@ class ListPlot(_ListPlot): = -Graphics- Compare with :'Plot': - /doc/reference-of-built-in-symbols/graphics-and-drawing/plotting-data/plot/. + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/general-graphical-plots/plot/. >> ListPlot[Table[n ^ 2, {n, 30}], Filling->Axis] = -Graphics- Compare with :'Plot': - /doc/reference-of-built-in-symbols/graphics-and-drawing/plotting-data/plot. + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/general-graphical-plots/plot. """ options = Graphics.options.copy() @@ -405,7 +405,7 @@ class DiscretePlot(_ListPlot): = -Graphics- Compare with :'Plot': - /doc/reference-of-built-in-symbols/graphics-and-drawing/plotting-data/plot/. + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/general-graphical-plots/plot/. """ attributes = A_HOLD_ALL | A_PROTECTED diff --git a/mathics/builtin/drawing/plot_plot3d.py b/mathics/builtin/drawing/plot_plot3d.py index 4dced462a..e921ee7c6 100644 --- a/mathics/builtin/drawing/plot_plot3d.py +++ b/mathics/builtin/drawing/plot_plot3d.py @@ -111,6 +111,8 @@ def eval( if isinstance(self, ParametricPlot3D) and len(plot_options.ranges) == 1: # ParametricPlot3D with one independent variable generating a curve default_plot_points = (1000,) + elif isinstance(self, ContourPlot3D): + default_plot_points = (50, 50, 50) elif plot.use_vectorized_plot: default_plot_points = (200, 200) else: @@ -163,7 +165,7 @@ class ComplexPlot3D(_Plot3D): $z_{max}$ with surface colored according to phase See :Drawing Option and Option Values: - /doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/drawing-options-and-option-values for a list of Plot options.
    @@ -191,7 +193,7 @@ class ComplexPlot(_Plot3D): $z_{max}$ colored according to phase See :Drawing Option and Option Values: - /doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/drawing-options-and-option-values for a list of Plot options. @@ -219,7 +221,7 @@ class ContourPlot(_Plot3D): $x$ ranging from $x_{min}$ to $x_{max}$ and $y$ ranging from $y_{min}$ to $y_{max}$. See :Drawing Option and Option Values: - /doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/drawing-options-and-option-values for a list of Plot options. @@ -235,6 +237,39 @@ class ContourPlot(_Plot3D): graphics_class = Graphics +class ContourPlot3D(_Plot3D): + """ + :Isosurface: https://en.wikipedia.org/wiki/Isosurface ( + :WMA link: https://reference.wolfram.com/language/ref/ContourPlot3D.html) +
    +
    'ContourPlot3D'[$f(x,y,z)$, {$x$, $x_{min}$, $x_{max}$}, {$y$, $y_{min}$, $y_{max}$, {$y$, $y_{min}$, $y_{max}$}] +
    creates a three-dimensional contour plot of $f(x,y,z)$ over the specified region on $x$, $y$, and $z$. + + See :Drawing Option and Option Values: + /doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values + for a list of Plot options. +
    + + >> ContourPlot3D[x ^ 2 + y ^ 2 - z ^ 2, {x, -1, 1}, {y, -1, 1}, {z, -1, 1}] + = ContourPlot3D[x ^ 2 + y ^ 2 - z ^ 2, {x, -1, 1}, {y, -1, 1}, {z, -1, 1}] + + Multiple isosurfaces (3d contours) of a second degree equation form conical suraces, hyperboloids in this case. + """ + + requires = ["skimage"] + summary_text = "creates a 3d contour plot" + expected_args = 4 + options = _Plot3D.options3d | { + "Contours": "Automatic", + "BoxRatios": "{1,1,1}", + "Mesh": "None", + } + # TODO: other options? + + many_functions = False + graphics_class = Graphics3D + + class DensityPlot(_Plot3D): """ :heat map:https://en.wikipedia.org/wiki/Heat_map (:WMA link: https://reference.wolfram.com/language/ref/DensityPlot.html) @@ -317,7 +352,7 @@ class Plot3D(_Plot3D): $x_{max}$ and $y$ ranging from $y_{min}$ to $y_{max}$. See :Drawing Option and Option Values: - /doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/drawing-options-and-option-values for a list of Plot options. diff --git a/mathics/builtin/forms/__init__.py b/mathics/builtin/forms/__init__.py index 8900c1818..a4fdcc467 100644 --- a/mathics/builtin/forms/__init__.py +++ b/mathics/builtin/forms/__init__.py @@ -3,7 +3,7 @@ A Form specifies the way Mathics3 Expressions are formatted or displayed. -The variable :$OutputForms: +The variable :\$OutputForms: /doc/reference-of-built-in-symbols/forms-of-input-and-output/form-variables/\$outputforms/ \ has a list of Forms defined. diff --git a/mathics/builtin/forms/base.py b/mathics/builtin/forms/base.py index 548a54ac2..791f6c443 100644 --- a/mathics/builtin/forms/base.py +++ b/mathics/builtin/forms/base.py @@ -1,3 +1,5 @@ +from typing import Final, List + import mathics.core.definitions as definitions from mathics.core.builtin import Builtin from mathics.core.symbols import Symbol @@ -30,17 +32,21 @@ def __new__(cls, *args, **kwargs): name = cls.__name__ if hasattr(cls, "in_printforms") and cls.in_printforms: - definitions.PrintForms.add(Symbol(name)) + definitions.PRINT_FORMS.add(Symbol(name)) if hasattr(cls, "in_outputforms") and cls.in_outputforms: - if name in definitions.OutputForms: + if name in definitions.OUTPUT_FORMS: raise RuntimeError(f"{name} already added to $OutputsForms") - definitions.OutputForms.add(Symbol(name)) + definitions.OUTPUT_FORMS.add(Symbol(name)) + if hasattr(cls, "in_boxforms") and cls.in_boxforms: + if name in definitions.BOX_FORMS: + raise RuntimeError(f"{name} already added to $BoxForms") + definitions.BOX_FORMS.add(Symbol(name)) + form_symbol_to_class[Symbol(name)] = cls return instance -# FormBaseClass is a public Builtin class that -# should not get added as a definition (and therefore not added to -# to external documentation. +# FormBaseClass is a Builtin class that should not get added as a +# definition, and therefore not added to to external documentation. -DOES_NOT_ADD_BUILTIN_DEFINITION = [FormBaseClass] +DOES_NOT_ADD_BUILTIN_DEFINITION: Final[List[Builtin]] = [FormBaseClass] diff --git a/mathics/builtin/forms/data.py b/mathics/builtin/forms/data.py index 049116338..f9716af83 100644 --- a/mathics/builtin/forms/data.py +++ b/mathics/builtin/forms/data.py @@ -4,30 +4,23 @@ Some forms are specific to formatting certain kinds of data, like numbers, strings, or matrices. These are in contrast to the Forms like :OutputForm: -/doc/reference-of-built-in-symbols/forms-of-input-and-output/printforms/outputform/ \ +/doc/reference-of-built-in-symbols/forms-of-input-and-output/general-purpose-forms/outputform/ \ or :StandardForm: -/doc/reference-of-built-in-symbols/forms-of-input-and-output/printforms/standardform/, \ +/doc/reference-of-built-in-symbols/forms-of-input-and-output/general-purpose-forms/standardform/, \ which are intended to work over all kinds of data. """ from typing import Any, Callable, Dict, List, Optional -from mathics.builtin.box.layout import RowBox, StyleBox +from mathics.builtin.box.layout import RowBox, StyleBox, SuperscriptBox from mathics.builtin.forms.base import FormBaseClass from mathics.core.atoms import Integer, Real, String from mathics.core.builtin import Builtin 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, SymbolFalse, SymbolNull, SymbolTrue -from mathics.core.systemsymbols import ( - SymbolAutomatic, - SymbolInfinity, - SymbolMakeBoxes, - SymbolRowBox, - SymbolSuperscriptBox, -) +from mathics.core.systemsymbols import SymbolAutomatic, SymbolInfinity, SymbolMakeBoxes from mathics.eval.strings import eval_StringForm_MakeBoxes, eval_ToString from mathics.format.box import ( StringLParen, @@ -95,7 +88,7 @@ class BaseForm(FormBaseClass): def eval_makeboxes(self, expr, n, f, evaluation: Evaluation): """MakeBoxes[BaseForm[expr_, n_], - f:StandardForm|TraditionalForm|OutputForm]""" + (f:StandardForm|TraditionalForm)]""" try: return eval_baseform(expr, n, f, evaluation) except ValueError: @@ -564,16 +557,13 @@ def default_NumberFormat( py_exp = exp.get_string_value() if py_exp: mul = String(options["NumberMultiplier"]) - return Expression( - SymbolRowBox, - ListExpression(man, mul, Expression(SymbolSuperscriptBox, base, exp)), - ) + return RowBox(man, mul, SuperscriptBox(base, exp)) return man def eval_makeboxes(self, fexpr, form, evaluation): """MakeBoxes[fexpr:NumberForm[_?AtomQ, ___], - form:StandardForm|TraditionalForm|OutputForm]""" + form:StandardForm|TraditionalForm]""" try: target, prec_parms, py_options = get_numberform_parameters( fexpr, evaluation @@ -603,7 +593,6 @@ def eval_makeboxes(self, fexpr, form, evaluation): if py_n is not None: py_options["_Form"] = form.get_name() - return numberform_to_boxes(target, py_n, py_f, evaluation, py_options) return Expression(SymbolMakeBoxes, target, form) @@ -637,7 +626,7 @@ class SequenceForm(FormBaseClass): def eval_makeboxes(self, args, form, evaluation, options: dict): """MakeBoxes[SequenceForm[args___, OptionsPattern[SequenceForm]], - form:StandardForm|TraditionalForm|OutputForm]""" + form:StandardForm|TraditionalForm]""" encoding = options["System`CharacterEncoding"] return RowBox( *[ @@ -712,7 +701,7 @@ class StringForm(FormBaseClass): def eval_makeboxes(self, s, args, form, evaluation): """MakeBoxes[StringForm[s_String, args___], - form:StandardForm|TraditionalForm|OutputForm]""" + form:StandardForm|TraditionalForm]""" try: result = eval_StringForm_MakeBoxes(s, args.get_sequence(), form, evaluation) except ValueError: @@ -771,8 +760,8 @@ class TableForm(FormBaseClass): summary_text = "format as a table" def eval_makeboxes(self, table, f, evaluation, options): - """MakeBoxes[%(name)s[table_, OptionsPattern[%(name)s]], - f:StandardForm|TraditionalForm|OutputForm]""" + """MakeBoxes[%(name)s[table_, OptionsPattern[]], + f:StandardForm|TraditionalForm]""" return eval_tableform(self, table, f, evaluation, options) @@ -802,10 +791,9 @@ class MatrixForm(TableForm): in_printforms = False summary_text = "format as a matrix" - def eval_makeboxes_matrix(self, table, form, evaluation, options): - """MakeBoxes[%(name)s[table_, OptionsPattern[%(name)s]], - form:StandardForm|TraditionalForm]""" - + def eval_makeboxes(self, table, form, evaluation, options): + """MakeBoxes[MatrixForm[table_, OptionsPattern[]], + (form:StandardForm|TraditionalForm)]""" result = super().eval_makeboxes(table, form, evaluation, options) if result.get_head_name() == "System`GridBox": return RowBox(StringLParen, result, StringRParen) diff --git a/mathics/builtin/forms/print.py b/mathics/builtin/forms/print.py index 7e5e34503..2e47e2fe9 100644 --- a/mathics/builtin/forms/print.py +++ b/mathics/builtin/forms/print.py @@ -3,23 +3,24 @@ A number of forms are suitable for formatting any kind of \Mathics expression. -The variable :$PrintForms: -/doc/reference-of-built-in-symbols/forms-of-input-and-output/form-variables/$printforms/ \ +The variable :\$PrintForms:/doc/reference-of-built-in-symbols/forms-of-input-and-output/form-variables/\$printforms \ contains a list of Forms \ that are in this category. After formatting the \Mathics expression, these removing mention of the form. -While Forms that appear in '$PrintForms' can be altered at run time, \ -below are the functions that appear in '$PrintForms' at startup. +While Forms that appear in '\$PrintForms' can be altered at run time, \ +below are the functions that appear in '\$PrintForms' at startup. """ -from mathics.builtin.box.layout import InterpretationBox, StyleBox, TagBox +from mathics.builtin.box.layout import InterpretationBox, PaneBox, StyleBox from mathics.builtin.forms.base import FormBaseClass from mathics.core.atoms import String +from mathics.core.element import BaseElement +from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression -from mathics.core.symbols import SymbolFalse, SymbolFullForm, SymbolTrue -from mathics.core.systemsymbols import SymbolInputForm -from mathics.format.box import eval_makeboxes_fullform, eval_mathmlform, eval_texform -from mathics.format.form import render_input_form +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 sort_order = "mathics.builtin.forms.general-purpose-forms" @@ -47,19 +48,6 @@ class FullForm(FormBaseClass): in_printforms = False summary_text = "format expression in underlying M-Expression representation" - def eval_makeboxes(self, expr, fmt, evaluation): - """MakeBoxes[FullForm[expr_], fmt_]""" - fullform_box = eval_makeboxes_fullform(expr, evaluation) - style_box = StyleBox( - fullform_box, - **{ - "System`ShowSpecialCharacters": SymbolFalse, - "System`ShowStringCharacters": SymbolTrue, - "System`NumberMarks": SymbolTrue, - }, - ) - return TagBox(style_box, SymbolFullForm) - class InputForm(FormBaseClass): r""" @@ -111,26 +99,6 @@ class InputForm(FormBaseClass): in_printforms = True summary_text = "format expression suitable for Mathics3 input" - # TODO: eventually, remove OutputForm in the second argument. - def eval_makeboxes(self, expr, evaluation): - """MakeBoxes[InputForm[expr_], Alternatives[StandardForm,TraditionalForm,OutputForm]]""" - - inputform = String(render_input_form(expr, evaluation)) - inputform = StyleBox( - inputform, - **{ - "System`ShowSpecialCharacters": SymbolFalse, - "System`ShowStringCharacters": SymbolTrue, - "System`NumberMarks": SymbolTrue, - }, - ) - expr = Expression(SymbolInputForm, expr) - return InterpretationBox( - inputform, - expr, - **{"System`Editable": SymbolTrue, "System`AutoDelete": SymbolTrue}, - ) - class MathMLForm(FormBaseClass): """ @@ -164,10 +132,6 @@ class MathMLForm(FormBaseClass): summary_text = "format expression as MathML commands" - def eval_mathml(self, expr, evaluation) -> Expression: - "MakeBoxes[MathMLForm[expr_], (OutputForm|StandardForm|TraditionalForm)]" - return eval_mathmlform(expr, evaluation) - class OutputForm(FormBaseClass): """ @@ -194,9 +158,11 @@ class OutputForm(FormBaseClass): = -Graphics- """ + in_outputforms = True + in_printforms = True + + formats = {"OutputForm[s_String]": "s"} summary_text = "format expression in plain text" - # Remove me at the end of the refactor - rules = {"MakeBoxes[OutputForm[expr_], form_]": "MakeBoxes[expr, OutputForm]"} class StandardForm(FormBaseClass): @@ -220,6 +186,7 @@ class StandardForm(FormBaseClass): in_outputforms = True in_printforms = True + in_boxforms = True summary_text = "format expression the default way" @@ -242,7 +209,7 @@ class TraditionalForm(FormBaseClass): in_outputforms = True in_printforms = True - + in_boxforms = True summary_text = "format expression using traditional mathematical notation" @@ -265,6 +232,34 @@ class TeXForm(FormBaseClass): in_printforms = True summary_text = "format expression as LaTeX commands" - def eval_tex(self, expr, evaluation) -> Expression: - "MakeBoxes[TeXForm[expr_], (OutputForm|StandardForm|TraditionalForm)]" - return eval_texform(expr, evaluation) + +@is_print_form_callback("System`InputForm") +def eval_makeboxes_inputform(expr: BaseElement, evaluation: Evaluation): + """MakeBoxes[InputForm[expr_], StandardForm|TraditionalForm]""" + inputform = String(render_input_form(expr, evaluation)) + inputform = StyleBox( + inputform, + **{ + "System`ShowStringCharacters": SymbolTrue, + "System`NumberMarks": SymbolTrue, + }, + ) + expr = Expression(SymbolInputForm, expr) + return InterpretationBox( + inputform, + expr, + **{"System`Editable": SymbolTrue, "System`AutoDelete": SymbolTrue}, + ) + + +@is_print_form_callback("System`OutputForm") +def eval_makeboxes_outputform(expr: BaseElement, evaluation: Evaluation, **kwargs): + """ + Build a 2D representation of the expression using only keyboard characters. + """ + + 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 3f0193f3b..720b9a24d 100644 --- a/mathics/builtin/forms/variables.py +++ b/mathics/builtin/forms/variables.py @@ -5,13 +5,147 @@ """ -from mathics.core.attributes import A_LOCKED, A_PROTECTED -from mathics.core.builtin import Predefined +from mathics.core.attributes import A_LOCKED, A_PROTECTED, A_READ_PROTECTED +from mathics.core.builtin import Builtin, Predefined from mathics.core.list import ListExpression sort_order = "mathics.builtin.forms.form-variables" +class BoxForms_(Predefined): + r""" +
    +
    '\$BoxForms' +
    contains the current list of general-purpose "BoxForms" formatters. +
    + + Elements of '\$BoxForms' are valid forms to be used as a second parameter \ + in 'MakeBoxes' expressions. + + Adding a new 'MakeBoxes' rule does not \ + automatically extend the $BoxForms list as happens, for example, in :\$PrintForm: + /doc/reference-of-built-in-symbols/forms-of-input-and-output/form-variables/\$printforms + when new 'FormatValues' are defined. + + To see how to add a new form, let us first list the values defined in '\$Boxforms'. \ + This will be useful to compare against later: + >> $BoxForms + = ... + + Now we add a rule with our new form 'MyBoxForm': + + >> MakeBoxes[x_Integer, MyBoxForm] := StringJoin[Table["o",{x}]] + + Although a rule involving 'MyBoxForm' has been defined in 'MakeBoxes', the \ + rule is not triggered in boxing. + + >> MyBoxForm[3] + = MyBoxForm[3] + + And it is not defined in '\$BoxForms' either: + + >> $BoxForms + = ... + + To have 'MyBoxForm' formatting take effect in boxing via 'MakeBoxes', \ + we first need to add the new form to '\$BoxForms': + >> AppendTo[$BoxForms, MyBoxForm] + = ... + + This automatically stores the new form in '\$PrintForms' and \ + '\$OutputForms': + >> MemberQ[$PrintForms, MyBoxForm] + = True + >> MemberQ[$OutputForms, MyBoxForm] + = True + + We also need to define in 'MyBoxForm' its 'ParentForm': + + >> Unprotect[ParentForm];ParentForm[MyBoxForm]=TraditionalForm + = TraditionalForm + + Now, + >> MyBoxForm[3] + = ooo + + The 'ParentForm' is used when a 'MakeBoxes' rule for a given expression \ + is not available: + >> MyBoxForm[F[3]] + = F(3) + + Above, the 'MyBoxForm' rule is not used to format the argument, because \ + the rule used to format the expression propagates 'TradionalForm' (the \ + 'ParentForm' of our custom 'BoxForm') to the arguments. + + To fix this, define a rule that propagates the box form to its elements: + + >> MakeBoxes[head_[elements___],MyBoxForm] := RowBox[{MakeBoxes[head,MyBoxForm], "<", RowBox[MakeBoxes[#1, MyBoxForm]&/@{elements}] ,">"}] + Now, + >> MyBoxForm[F[3]] + = F + + Suppose now we want to remove the new BoxForm. We can reset '$BoxForms' \ + to its default values by unset it: + >> $BoxForms=.; $BoxForms + = ... + + This does not remove the value from '\$PrintForm' or '\$OutputForm': + >> {MemberQ[$PrintForms, MyBoxForm], MemberQ[$OutputForms, MyBoxForm]} + = {True, True} + + To remove 'MyBoxForm', unset in each variable: + >> $PrintForms=.; $OutputForms=.; + >> {MemberQ[$PrintForms, MyBoxForm], MemberQ[$OutputForms, MyBoxForm]} + = {False, False} + """ + attributes = A_READ_PROTECTED + messages = { + "formset": "Cannot set $BoxForms to ``; value must be a list that includes TraditionalForm and StandardForm." + } + name = "$BoxForms" + summary_text = "the list of box forms" + + def evaluate(self, evaluation): + return ListExpression(*evaluation.definitions.boxforms) + + +class OutputForms_(Predefined): + r""" +
    +
    '\$OutputForms' +
    contains the list of all output forms. It is updated automatically when new 'OutputForms' are defined by setting format values. +
    + + >> $OutputForms + = ... + """ + + attributes = A_LOCKED | A_PROTECTED + name = "$OutputForms" + summary_text = "the list of output forms" + + def evaluate(self, evaluation): + return ListExpression(*evaluation.definitions.outputforms) + + +class ParentForm(Builtin): + r""" +
    +
    'ParentForm'[$Form$] +
    Return the parent form of the Box Form $Form$. +
    + + 'ParentForm' is used to set and retrieve the parent form of a user-defined \ + box form. See :\$BoxForms': + /doc/reference-of-built-in-symbols/forms-of-input-and-output/form-variables/\$boxforms + for a usage example. + """ + + attributes = A_PROTECTED + messages = {"deflt": "The ParentForm of `` is not defined on $BoxForms."} + summary_text = "sets the parent form of a custom box form" + + class PrintForms_(Predefined): r"""
    @@ -41,26 +175,7 @@ class PrintForms_(Predefined): attributes = A_LOCKED | A_PROTECTED name = "$PrintForms" - summary_text = "contains a list of print forms" + summary_text = "the list of print forms" def evaluate(self, evaluation): return ListExpression(*evaluation.definitions.printforms) - - -class OutputForms_(Predefined): - r""" -
    -
    '\$OutputForms' -
    contains the list of all output forms. It is updated automatically when new 'OutputForms' are defined by setting format values. -
    - - >> $OutputForms - = ... - """ - - attributes = A_LOCKED | A_PROTECTED - name = "$OutputForms" - summary_text = "contains a list all output forms" - - def evaluate(self, evaluation): - return ListExpression(*evaluation.definitions.outputforms) diff --git a/mathics/builtin/functional/application.py b/mathics/builtin/functional/application.py index 3c5bebbcd..414ec25b1 100644 --- a/mathics/builtin/functional/application.py +++ b/mathics/builtin/functional/application.py @@ -198,9 +198,7 @@ class Slot(SympyFunction, PrefixOperator): rules = { "Slot[]": "Slot[1]", "MakeBoxes[Slot[n_Integer?NonNegative]," - " f:StandardForm|TraditionalForm|InputForm|OutputForm]": ( - '"#" <> ToString[n]' - ), + " (f:StandardForm|TraditionalForm)]": ('"#" <> ToString[n]'), } summary_text = "one argument of a pure function" @@ -237,6 +235,6 @@ class SlotSequence(PrefixOperator, Builtin): rules = { "SlotSequence[]": "SlotSequence[1]", "MakeBoxes[SlotSequence[n_Integer?Positive]," - "f:StandardForm|TraditionalForm|InputForm|OutputForm]": ('"##" <> ToString[n]'), + "(f:StandardForm|TraditionalForm)]": ('"##" <> ToString[n]'), } summary_text = "the full sequence of arguments of a pure function" diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index b4f53603a..42b0d52ae 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -4,13 +4,10 @@ """ Drawing Graphics """ - -import logging from math import sqrt from mathics.builtin.colors.color_directives import ( CMYKColor, - ColorError, GrayLevel, Hue, LABColor, @@ -19,41 +16,31 @@ Opacity, RGBColor, XYZColor, - _ColorObject, -) -from mathics.builtin.drawing.graphics_internals import ( - GLOBALS, - _GraphicsDirective, - _GraphicsElementBox, - get_class, ) +from mathics.builtin.drawing.graphics_internals import GLOBALS, _GraphicsDirective from mathics.builtin.options import options_to_rules from mathics.core.atoms import Integer, Rational, Real from mathics.core.attributes import A_PROTECTED, A_READ_PROTECTED from mathics.core.builtin import Builtin -from mathics.core.convert.expression import to_expression, to_mathics_list from mathics.core.exceptions import BoxExpressionError -from mathics.core.expression import Expression -from mathics.core.formatter import lookup_method -from mathics.core.list import ListExpression -from mathics.core.symbols import ( - Symbol, - SymbolList, - SymbolNull, - symbol_set, - system_symbols_dict, -) +from mathics.core.symbols import Symbol, SymbolList, symbol_set, system_symbols_dict from mathics.core.systemsymbols import ( SymbolEdgeForm, SymbolFaceForm, - SymbolMakeBoxes, + SymbolInset, + SymbolLine, + SymbolPoint, + SymbolPolygon, SymbolRule, + SymbolStyle, + SymbolText, ) from mathics.eval.nevaluator import eval_N # This following line tells documentation how to sort this module sort_order = "mathics.builtin.drawing-graphics" +GRAPHICS_SYMBOLS = {} GRAPHICS_OPTIONS = { "AlignmentPoint": "Center", "AspectRatio": "Automatic", @@ -98,60 +85,6 @@ DEFAULT_POINT_FACTOR = 0.007 -ERROR_BACKGROUND_COLOR = RGBColor(components=[1, 0.3, 0.3, 0.25]) - - -class CoordinatesError(BoxExpressionError): - pass - - -def coords(value): - if value.has_form("List", 2): - x, y = value.elements[0].round_to_float(), value.elements[1].round_to_float() - if x is None or y is None: - raise CoordinatesError - return (x, y) - raise CoordinatesError - - -class Coords: - def __init__(self, graphics, expr=None, pos=None, d=None): - self.graphics = graphics - self.p = pos - self.d = d - if expr is not None: - if expr.has_form("Offset", 1, 2): - self.d = coords(expr.elements[0]) - if len(expr.elements) > 1: - self.p = coords(expr.elements[1]) - else: - self.p = None - else: - self.p = coords(expr) - - def pos(self): - p = self.graphics.translate(self.p) - p = (cut(p[0]), cut(p[1])) - if self.d is not None: - d = self.graphics.translate_absolute(self.d) - return (p[0] + d[0], p[1] + d[1]) - return p - - def add(self, x, y): - p = (self.p[0] + x, self.p[1] + y) - return Coords(self.graphics, pos=p, d=self.d) - - -def cut(value): - "Cut values in graphics primitives (not displayed otherwise in SVG)" - border = 10**8 - if value < -border: - value = -border - elif value > border: - value = border - return value - - def _to_float(x): x = x.round_to_float() if x is None: @@ -159,62 +92,6 @@ def _to_float(x): return x -def _data_and_options(elements, defined_options): - data = [] - options = defined_options.copy() - for element in elements: - if element.get_head_name() == "System`Rule": - if len(element.elements) != 2: - raise BoxExpressionError - name, value = element.elements - name_head = name.get_head_name() - if name_head == "System`Symbol": - py_name = name.get_name() - elif name_head == "System`String": - py_name = "System`" + name.get_string_value() - else: # unsupported name type - raise BoxExpressionError - options[py_name] = value - else: - data.append(element) - return data, options - - -def _extract_graphics(graphics, format, evaluation): - graphics_box = Expression(SymbolMakeBoxes, graphics).evaluate(evaluation) - # builtin = GraphicsBox(expression=False) - elements, calc_dimensions = graphics_box._prepare_elements( - graphics_box.elements, {"evaluation": evaluation}, neg_y=True - ) - xmin, xmax, ymin, ymax, _, _, _, _ = calc_dimensions() - - # xmin, xmax have always been moved to 0 here. the untransformed - # and unscaled bounds are found in elements.xmin, elements.ymin, - # elements.extent_width, elements.extent_height. - - # now compute the position of origin (0, 0) in the transformed - # coordinate space. - - ex = elements.extent_width - ey = elements.extent_height - - sx = (xmax - xmin) / ex - sy = (ymax - ymin) / ey - - ox = -elements.xmin * sx + xmin - oy = -elements.ymin * sy + ymin - - # generate code for svg or asy. - - if format in ("asy", "svg"): - format_fn = lookup_method(elements, format) - code = format_fn(elements) - else: - raise NotImplementedError - - return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code - - class Show(Builtin): """ @@ -316,122 +193,29 @@ class Graphics(Builtin): def eval_makeboxes(self, content, evaluation, options): """MakeBoxes[%(name)s[content_, OptionsPattern[%(name)s]], - StandardForm|TraditionalForm|OutputForm]""" - - def convert(content): - head = content.get_head() - - if head is SymbolList: - return to_mathics_list( - *content.elements, elements_conversion_fn=convert - ) - elif head is Symbol("System`Style"): - return to_expression( - "StyleBox", *[convert(item) for item in content.elements] - ) - - if head in element_heads: - if head is Symbol("System`Text"): - head = Symbol("System`Inset") - atoms = content.get_atoms(include_heads=False) - if any( - not isinstance(atom, (Integer, Real)) - and atom not in GRAPHICS_SYMBOLS - for atom in atoms - ): - if head is Symbol("System`Inset"): - inset = content.elements[0] - if inset.get_head() is Symbol("System`Graphics"): - opts = {} - # opts = dict(opt._elements[0].name:opt_elements[1] for opt in inset._elements[1:]) - inset = self.eval_makeboxes( - inset._elements[0], evaluation, opts - ) - n_elements = [inset] + [ - eval_N(element, evaluation) - for element in content.elements[1:] - ] - else: - n_elements = ( - eval_N(element, evaluation) for element in content.elements - ) - else: - n_elements = content.elements - return Expression(Symbol(head.name + self.box_suffix), *n_elements) - return content + StandardForm|TraditionalForm]""" + from mathics.builtin.box.graphics import GraphicsBox + from mathics.builtin.box.graphics3d import Graphics3DBox + from mathics.builtin.drawing.graphics3d import Graphics3D + from mathics.format.box.graphics import primitives_to_boxes for option in options: if option not in ("System`ImageSize",): options[option] = eval_N(options[option], evaluation) - from mathics.builtin.box.graphics import GraphicsBox - from mathics.builtin.box.graphics3d import Graphics3DBox - from mathics.builtin.drawing.graphics3d import Graphics3D - if type(self) is Graphics: return GraphicsBox( - convert(content), evaluation=evaluation, *options_to_rules(options) + primitives_to_boxes(content, evaluation, self.box_suffix), + _evaluation=evaluation, + **options, ) elif type(self) is Graphics3D: return Graphics3DBox( - convert(content), evaluation=evaluation, *options_to_rules(options) + primitives_to_boxes(content, evaluation, self.box_suffix), + _evaluation=evaluation, + **options, ) - - -class _Polyline(_GraphicsElementBox): - """ - A structure containing a list of line segments - stored in ``self.lines`` created from - a list of points. - - Lines are formed by pairs of consecutive point. - """ - - def do_init(self, graphics, points): - if not points.has_form("List", None): - raise BoxExpressionError - if ( - points.elements - and points.elements[0].has_form("List", None) - and all( - element.has_form("List", None) - for element in points.elements[0].elements - ) - ): - elements = points.elements - self.multi_parts = True - elif len(points.elements) == 0: - # Ensure there are no line segments if there are no points. - self.lines = [] - return - else: - elements = [ListExpression(*points.elements)] - self.multi_parts = False - lines = [] - for element in elements: - if element.has_form("List", None): - lines.append(element.elements) - else: - raise BoxExpressionError - self.lines = [ - [graphics.coords(graphics, point) for point in line] for line in lines - ] - - def extent(self) -> list: - lw = self.style.get_line_width(face_element=False) - result = [] - for line in self.lines: - for c in line: - x, y = c.pos() - result.extend( - [ - (x - lw, y - lw), - (x - lw, y + lw), - (x + lw, y - lw), - (x + lw, y + lw), - ] - ) - return result + raise BoxExpressionError class _Size(_GraphicsDirective): @@ -831,6 +615,7 @@ def _norm(p, q): return dx, dy, length +# belongs to mathics.format.box.graph? class _Line: def make_draw_svg(self, style): def draw(points): @@ -963,317 +748,6 @@ def arrows(self, points, heads): # heads has to be sorted by pos yield shape -def total_extent(extents): - xmin = xmax = ymin = ymax = None - for extent in extents: - for x, y in extent: - if xmin is None or x < xmin: - xmin = x - if xmax is None or x > xmax: - xmax = x - if ymin is None or y < ymin: - ymin = y - if ymax is None or y > ymax: - ymax = y - return xmin, xmax, ymin, ymax - - -def _style(graphics, item): - head = item.get_head() - if head in style_heads: - klass = get_class(head) - style = klass.create_as_style(klass, graphics, item) - elif head in (SymbolEdgeForm, SymbolFaceForm): - style = graphics.style_class( - graphics, edge=head is SymbolEdgeForm, face=head is SymbolFaceForm - ) - if len(item.elements) > 1: - raise BoxExpressionError - if item.elements: - if item.elements[0].has_form("List", None): - for dir in item.elements[0].elements: - style.append(dir, allow_forms=False) - else: - style.append(item.elements[0], allow_forms=False) - else: - raise BoxExpressionError - return style - - -class Style: - def __init__(self, graphics, edge=False, face=False): - self.styles = [] - self.options = {} - self.graphics = graphics - self.edge = edge - self.face = face - self.klass = graphics.style_class - - def append(self, item, allow_forms=True): - self.styles.append(_style(self.graphics, item)) - - def set_option(self, name, value): - self.options[name] = value - - def extend(self, style): - self.styles.extend(style.styles) - - def clone(self): - result = self.klass(self.graphics, edge=self.edge, face=self.face) - result.styles = self.styles[:] - result.options = self.options.copy() - return result - - def get_default_face_color(self): - return RGBColor(components=(0, 0, 0, 1)) - - def get_default_edge_color(self): - return RGBColor(components=(0, 0, 0, 1)) - - def get_style( - self, style_class, face_element=None, default_to_faces=True, consider_forms=True - ): - if face_element is not None: - default_to_faces = consider_forms = face_element - edge_style = face_style = None - if style_class == _ColorObject: - if default_to_faces: - face_style = self.get_default_face_color() - else: - edge_style = self.get_default_edge_color() - elif style_class == _Thickness: - if not default_to_faces: - edge_style = AbsoluteThickness(self.graphics, value=1.6) - for item in self.styles: - if isinstance(item, style_class): - if default_to_faces: - face_style = item - else: - edge_style = item - elif isinstance(item, Style): - if consider_forms: - if item.edge: - edge_style, _ = item.get_style( - style_class, default_to_faces=False, consider_forms=False - ) - elif item.face: - _, face_style = item.get_style( - style_class, default_to_faces=True, consider_forms=False - ) - return edge_style, face_style - - def get_option(self, name): - return self.options.get(name, None) - - def get_line_width(self, face_element=True) -> float: - if self.graphics.pixel_width is None: - return 0.0 - edge_style, _ = self.get_style( - _Thickness, default_to_faces=face_element, consider_forms=face_element - ) - if edge_style is None: - return 0.0 - return edge_style.get_thickness() / 2.0 - - -def _flatten(elements): - for element in elements: - if element.get_head() is SymbolList: - flattened = element.flatten_with_respect_to_head(SymbolList) - if flattened.get_head() is SymbolList: - for x in flattened.elements: - yield x - else: - yield flattened - else: - yield element - - -class _GraphicsElements: - style_class = Style - - def __init__(self, content, evaluation): - self.evaluation = evaluation - self.elements = [] - - builtins = evaluation.definitions.builtin - - def get_options(name): - builtin = builtins.get(name) - if builtin is None: - return None - return builtin.options - - def stylebox_style(style, specs): - new_style = style.clone() - for spec in _flatten(specs): - head = spec.get_head() - if head in style_and_form_heads: - new_style.append(spec) - elif head is Symbol("System`Rule") and len(spec.elements) == 2: - option, expr = spec.elements - if not isinstance(option, Symbol): - raise BoxExpressionError - - name = option.get_name() - create = style_options.get(name, None) - if create is None: - raise BoxExpressionError - - new_style.set_option(name, create(style.graphics, expr)) - else: - raise BoxExpressionError - return new_style - - failed = [] - - def convert(content, style): - if content.has_form("List", None): - items = content.elements - else: - items = [content] - style = style.clone() - for item in items: - if item is SymbolNull: - continue - head = item.get_head() - if head in style_and_form_heads: - try: - style.append(item) - except ColorError: - failed.append(head) - elif head is Symbol("System`StyleBox"): - if len(item.elements) < 1: - failed.append(item.head) - for element in convert( - item.elements[0], stylebox_style(style, item.elements[1:]) - ): - yield element - elif head.name[-3:] == "Box": # and head[:-3] in element_heads: - element_class = get_class(head) - if element_class is None: - failed.append(head) - continue - options = get_options(head.name[:-3]) - if options: - data, options = _data_and_options(item.elements, options) - new_item = Expression(head, *data) - try: - element = element_class(self, style, new_item, options) - except (BoxExpressionError, CoordinatesError): - failed.append(head) - continue - else: - try: - element = element_class(self, style, item) - except (BoxExpressionError, CoordinatesError): - failed.append(head) - continue - yield element - elif head is SymbolList: - for element in convert(item, style): - yield element - else: - failed.append(head) - continue - - # if failed: - # yield build_error_box2(style) - # raise BoxExpressionError(messages) - - self.elements = list(convert(content, self.style_class(self))) - if failed: - messages = "\n".join( - [f"{str(h)} is not a valid primitive or directive." for h in failed] - ) - self.tooltip_text = messages - self.background_color = ERROR_BACKGROUND_COLOR - logging.warning(messages) - - def create_style(self, expr): - style = self.style_class(self) - - def convert(expr): - if expr.has_form(("List", "Directive"), None): - for item in expr.elements: - convert(item) - else: - style.append(expr) - - convert(expr) - return style - - -class GraphicsElements(_GraphicsElements): - coords = Coords - - def __init__(self, content, evaluation, neg_y=False): - super(GraphicsElements, self).__init__(content, evaluation) - self.neg_y = neg_y - self.xmin = self.ymin = self.pixel_width = None - self.pixel_height = self.extent_width = self.extent_height = None - self.view_width = None - self.content = content - - def translate(self, coords): - if self.pixel_width is not None: - w = self.extent_width if self.extent_width > 0 else 1 - h = self.extent_height if self.extent_height > 0 else 1 - result = [ - (coords[0] - self.xmin) * self.pixel_width / w, - (coords[1] - self.ymin) * self.pixel_height / h, - ] - if self.neg_y: - result[1] = self.pixel_height - result[1] - return tuple(result) - else: - return (coords[0], coords[1]) - - def translate_absolute(self, d): - if self.pixel_width is None: - return (0, 0) - else: - lw = 96.0 / 72 - return (d[0] * lw, (-1 if self.neg_y else 1) * d[1] * lw) - - def translate_relative(self, x): - if self.pixel_width is None: - return 0 - else: - return x * self.pixel_width - - def extent(self, completely_visible_only=False): - if completely_visible_only: - ext = total_extent( - [ - element.extent() - for element in self.elements - if element.is_completely_visible - ] - ) - else: - ext = total_extent([element.extent() for element in self.elements]) - xmin, xmax, ymin, ymax = ext - if xmin == xmax: - if xmin is None: - return 0, 0, 0, 0 - xmin = 0 - xmax *= 2 - if ymin == ymax: - if ymin is None: - return 0, 0, 0, 0 - ymin = 0 - ymax *= 2 - return xmin, xmax, ymin, ymax - - def set_size( - self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height - ): - self.xmin, self.ymin = xmin, ymin - self.extent_width, self.extent_height = extent_width, extent_height - self.pixel_width, self.pixel_height = pixel_width, pixel_height - - class Circle(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/Circle.html @@ -1580,7 +1054,7 @@ class Tiny(Builtin): summary_text = "tiny size style or option setting" -element_heads = frozenset( +ELEMENT_HEADS = frozenset( symbol_set( Symbol("System`Arrow"), Symbol("System`BezierCurve"), @@ -1590,21 +1064,22 @@ class Tiny(Builtin): Symbol("System`Cylinder"), Symbol("System`Disk"), Symbol("System`FilledCurve"), - Symbol("System`Inset"), - Symbol("System`Line"), - Symbol("System`Point"), - Symbol("System`Polygon"), + SymbolInset, + SymbolLine, + SymbolPoint, + SymbolPolygon, Symbol("System`Rectangle"), Symbol("System`RegularPolygon"), Symbol("System`Sphere"), - Symbol("System`Style"), - Symbol("System`Text"), + SymbolStyle, + SymbolText, Symbol("System`Tube"), Symbol("System`UniformPolyhedron"), ) ) -styles = system_symbols_dict( + +STYLES = system_symbols_dict( { "RGBColor": RGBColor, "XYZColor": XYZColor, @@ -1624,16 +1099,11 @@ class Tiny(Builtin): } ) -style_options = system_symbols_dict( - {"FontColor": _style, "ImageSizeMultipliers": (lambda *x: x[1])} -) -style_heads = frozenset(styles.keys()) - -style_and_form_heads = frozenset( - style_heads.union(symbol_set(SymbolEdgeForm, SymbolFaceForm)) +STYLE_HEADS = frozenset(STYLES.keys()) +STYLE_AND_FORM_HEADS = frozenset( + STYLE_HEADS.union(symbol_set(SymbolEdgeForm, SymbolFaceForm)) ) - GLOBALS.update( system_symbols_dict( { @@ -1648,13 +1118,13 @@ class Tiny(Builtin): ) ) -GLOBALS.update(styles) +GLOBALS.update(STYLES) GRAPHICS_SYMBOLS = { SymbolList, SymbolRule, Symbol("System`VertexColors"), - *element_heads, - *[Symbol(element.name + "Box") for element in element_heads], - *style_heads, + *ELEMENT_HEADS, + *[Symbol(element.name + "Box") for element in ELEMENT_HEADS], + *STYLE_HEADS, } diff --git a/mathics/builtin/kernel_sessions.py b/mathics/builtin/kernel_sessions.py index 64046bb56..78105c991 100644 --- a/mathics/builtin/kernel_sessions.py +++ b/mathics/builtin/kernel_sessions.py @@ -9,9 +9,9 @@ class Out(Builtin): r""" - :WMA: https://reference.wolfram.com/language/ref/\$Out + :WMA: https://reference.wolfram.com/language/ref/Out
    -
    '%$k$' or 'Out'[$k$] +
    '$\%k$' or 'Out'[$k$]
    gives the result of the $k$-th input line.
    '%' @@ -55,9 +55,9 @@ class Out(Builtin): "Out[k_Integer?Negative]": "Out[$Line + k]", "Out[]": "Out[$Line - 1]", "MakeBoxes[Out[k_Integer?((-10 <= # < 0)&)]," - " f:StandardForm|TraditionalForm|InputForm|OutputForm]": r'StringJoin[ConstantArray["%%", -k]]', + " f:StandardForm|TraditionalForm]": r'StringJoin[ConstantArray["%%", -k]]', "MakeBoxes[Out[k_Integer?Positive]," - " f:StandardForm|TraditionalForm|InputForm|OutputForm]": r'"%%" <> ToString[k]', + " f:StandardForm|TraditionalForm]": r'"%%" <> ToString[k]', } summary_text = "result of the Kth input line" diff --git a/mathics/builtin/layout.py b/mathics/builtin/layout.py index 4a5f34c70..6caa1170f 100644 --- a/mathics/builtin/layout.py +++ b/mathics/builtin/layout.py @@ -11,13 +11,19 @@ from mathics.builtin.box.layout import GridBox, PaneBox, RowBox, to_boxes from mathics.builtin.makeboxes import MakeBoxes -from mathics.core.atoms import Real, String +from mathics.core.atoms import Integer, Real, String from mathics.core.builtin import Builtin, Operator, PostfixOperator, PrefixOperator from mathics.core.expression import Evaluation, Expression from mathics.core.list import ListExpression -from mathics.core.systemsymbols import SymbolMakeBoxes, SymbolSubscriptBox +from mathics.core.symbols import Symbol +from mathics.core.systemsymbols import ( + SymbolMakeBoxes, + SymbolPostfix, + SymbolPrefix, + SymbolSubscriptBox, +) from mathics.eval.lists import list_boxes -from mathics.format.box import format_element +from mathics.format.box import eval_infix, eval_postprefix, format_element, parenthesize class Center(Builtin): @@ -40,12 +46,15 @@ class Format(Builtin):
    'Format'[$expr$] -
    holds values specifying how $expr$ should be printed. +
    used on the left-hand side of an assignment to specify how $expr$ should be printed.
    - Assign values to 'Format' to control how particular expressions - should be formatted when printed to the user. + First, we set up a 'Format' definition for 'f' to display its arguments as if it were equivalent to an infix operator "~": + >> Format[f[x___]] := Infix[{x}, "~"] + + Now, to see this format in use: + >> f[1, 2, 3] = 1 ~ 2 ~ 3 >> f[1] @@ -62,12 +71,57 @@ class Format(Builtin): Formats must be attached to the head of an expression: >> f /: Format[g[f]] = "my f"; : Tag f not found or too deep for an assigned rule. + + Format can be used to specify the request format: + >> Format[Integrate[F[x], x], TeXForm] + = \\int F(x) \\, dx + + Format evaluates its first element before applying the format: + >> Format[Integrate[Cos[x], x], TeXForm] + = ... + but the result keeps the structure: + >> % //FullForm + = Format[Sin[x], TeXForm] + + If the second parameter is omitted, 'Format' is ignored: + >> Format[F[x]] + = F[x] + + If the second argument is not one of '$PrintForms', a message \ + is shown, and the argument is discarded: + >> Format[F[x], NoFormat] + : Value of option FormatType -> NoFormat is not valid. + = F[x] + + Mathics3 'Format' output can differ slightly from WMA in what we hope \ + is a more useful way. + + Use 'InputForm' if you want to get a 'Format' definition that can be used as \ + Mathics3 input: + + >> Format[{a->Integrate[F[x], x]}, StandardForm] //InputForm + = Format[{a -> Integrate[F[x], x]}, StandardForm] + + In WMA, you might not get something that can be used as input. + + Similarly, use 'Fullform' to get a valid FullForm equivalent expression: + + >> Format[{a->Integrate[F[x], x]}, StandardForm] //FullForm + = Format[{Rule[a, Integrate[F[x], x]]}, StandardForm] """ messages = {"fttp": "Format type `1` is not a symbol."} summary_text = ( "settable low-level translator from various forms to evaluatable expressions" ) + rules = {"MakeBoxes[Format[expr_], fmt_]": "MakeBoxes[expr, fmt]"} + + def eval_Makeboxes(self, expr, form, evaluation): + """MakeBoxes[Format[expr_, form_], _]""" + if form not in evaluation.definitions.printforms: + evaluation.message("FormatType", "ftype", form) + return format_element(expr, evaluation) + return format_element(expr, evaluation, form) class Grid(Builtin): @@ -121,7 +175,7 @@ class Grid(Builtin): def eval_makeboxes(self, array, f, evaluation: Evaluation, options) -> Expression: """MakeBoxes[Grid[array_List, OptionsPattern[Grid]], - f:StandardForm|TraditionalForm|OutputForm]""" + f:StandardForm|TraditionalForm]""" elements = array.elements @@ -172,8 +226,20 @@ class Infix(Builtin): = a + b - c """ + rules = { + ( + "MakeBoxes[Infix[head_[elements___]], " + " f:StandardForm|TraditionalForm]" + ): ('MakeBoxes[Infix[head[elements], StringForm["~`1`~", head]], f]'), + } summary_text = "infix form" + def eval_makeboxes_infix( + self, expr, operator, precedence: Integer, grouping, form: Symbol, evaluation + ): + """MakeBoxes[Infix[expr_, operator_, precedence_:None, grouping_:None], form:StandardForm|TraditionalForm]""" + return eval_infix(self, expr, operator, precedence, grouping, form, evaluation) + class Left(Builtin): """ @@ -221,7 +287,7 @@ class Pane(Builtin): A Pane is treated as an unbroken rectangular region for purposes of line breaking. >> Pane[37!] - = 13763753091226345046315979581580902400000000 + = Pane[13763753091226345046315979581580902400000000] In TeXForm, $Pane$ produce minipage environments: >> {{Pane[a,3], Pane[expt, 3]}}//TableForm//TeXForm @@ -278,6 +344,13 @@ class Postfix(PostfixOperator): operator_display = None summary_text = "postfix form" + def eval_makeboxes_postfix(self, expr, h, precedence, form, evaluation): + """MakeBoxes[Postfix[expr_, h_, precedence_:None], + form:StandardForm|TraditionalForm]""" + return eval_postprefix( + self, SymbolPostfix, expr, h, precedence, form, evaluation + ) + class Precedence(Builtin): """ @@ -332,8 +405,20 @@ class PrecedenceForm(Builtin):
    'PrecedenceForm'[$expr$, $prec$]
    format $expr$ parenthesized as it would be if it contained an operator of precedence $prec$.
    + + >> PrecedenceForm[x/y, 12] - z + = -z + (x / y) + """ + def eval_outerprecedenceform(self, expr, precedence, form, evaluation): + """MakeBoxes[PrecedenceForm[expr_, precedence_], + form:StandardForm|TraditionalForm]""" + + py_precedence = precedence.get_int_value() + boxes = format_element(expr, evaluation, form) + return parenthesize(py_precedence, expr, boxes, True) + summary_text = "parenthesize with a precedence" @@ -370,6 +455,13 @@ class Prefix(PrefixOperator): operator_display = None summary_text = "prefix form" + def eval_makeboxes_prefix(self, expr, h, precedence, form, evaluation): + """MakeBoxes[Prefix[expr_, h_, precedence_:None], + form:StandardForm|TraditionalForm]""" + return eval_postprefix( + self, SymbolPrefix, expr, h, precedence, form, evaluation + ) + class Right(Builtin): """ @@ -398,7 +490,7 @@ class Row(Builtin): def eval_makeboxes(self, items, sep, form, evaluation: Evaluation): """MakeBoxes[Row[{items___}, sep_:""], - form:StandardForm|TraditionalForm|OutputForm]""" + form:StandardForm|TraditionalForm]""" items = items.get_sequence() if not isinstance(sep, String): @@ -456,7 +548,12 @@ class Style(Builtin): summary_text = "wrapper for styles and style options to apply" options = {"ImageSizeMultipliers": "Automatic"} - + rules = { + "MakeBoxes[Style[expr_, OptionsPattern[Style]], f_]": ( + "StyleBox[MakeBoxes[expr, f], " + "ImageSizeMultipliers -> OptionValue[ImageSizeMultipliers]]" + ), + } rules = { "MakeBoxes[Style[expr_, OptionsPattern[Style]], f_]": ( "StyleBox[MakeBoxes[expr, f], " diff --git a/mathics/builtin/list/associations.py b/mathics/builtin/list/associations.py index ebe90b410..72282cbe6 100644 --- a/mathics/builtin/list/associations.py +++ b/mathics/builtin/list/associations.py @@ -40,8 +40,8 @@ class Association(Builtin): >> <|a -> x, b -> y|> = <|a -> x, b -> y|> - >> Association[{a -> x, b -> y}] - = <|a -> x, b -> y|> + >> Association[{a -> x^2, b -> y}] + = <|a -> x ^ 2, b -> y|> Associations can be nested: >> <|a -> x, b -> y, <|a -> z, d -> t|>|> @@ -56,7 +56,7 @@ class Association(Builtin): def eval_makeboxes(self, rules, f, evaluation: Evaluation): """MakeBoxes[<|rules___|>, - f:StandardForm|TraditionalForm|OutputForm|InputForm]""" + (f:StandardForm|TraditionalForm)]""" def validate(exprs): for expr in exprs: diff --git a/mathics/builtin/list/constructing.py b/mathics/builtin/list/constructing.py index 59be9e1ba..9bcd1b64f 100644 --- a/mathics/builtin/list/constructing.py +++ b/mathics/builtin/list/constructing.py @@ -12,7 +12,6 @@ from itertools import permutations from typing import Optional, Tuple -from mathics.builtin.box.layout import RowBox from mathics.core.atoms import ByteArray, Integer, Integer1, is_integer_rational_or_real from mathics.core.attributes import A_HOLD_FIRST, A_LISTABLE, A_LOCKED, A_PROTECTED from mathics.core.builtin import BasePattern, Builtin, IterationFunction @@ -24,7 +23,7 @@ from mathics.core.list import ListExpression from mathics.core.symbols import Atom, Symbol from mathics.core.systemsymbols import SymbolNormal, SymbolTuples -from mathics.eval.lists import get_tuples, list_boxes +from mathics.eval.lists import get_tuples class Array(Builtin): @@ -165,13 +164,6 @@ def eval(self, elements, evaluation: Evaluation): elements_part_of_elements__ = elements.get_sequence() return ListExpression(*elements_part_of_elements__) - def eval_makeboxes(self, items, f, evaluation): - """MakeBoxes[{items___}, - f:StandardForm|TraditionalForm|OutputForm|InputForm|FullForm]""" - - items = items.get_sequence() - return RowBox(*list_boxes(items, f, evaluation, "{", "}")) - class Normal(Builtin): """ diff --git a/mathics/builtin/list/eol.py b/mathics/builtin/list/eol.py index 89c061144..0599d4194 100644 --- a/mathics/builtin/list/eol.py +++ b/mathics/builtin/list/eol.py @@ -1168,14 +1168,12 @@ class Part(Builtin): def eval_makeboxes(self, list, i, f, evaluation): """MakeBoxes[Part[list_, i___], - f:StandardForm|TraditionalForm|OutputForm|InputForm]""" + (f:StandardForm|TraditionalForm)]""" i = i.get_sequence() list = Expression(SymbolMakeBoxes, list, f).evaluate(evaluation) - if f.get_name() in ("System`OutputForm", "System`InputForm"): - open, close = "[[", "]]" - else: - open, close = "\u301a", "\u301b" + # FIXME: pick up values LeftDoubleBracket and RightDoubleBracket from named-characters.yaml + open, close = "\u301a", "\u301b" indices = list_boxes(i, f, evaluation, open, close) result = RowBox(list, *indices) return result diff --git a/mathics/builtin/mainloop.py b/mathics/builtin/mainloop.py index 8bce8b01a..2c69b6dd2 100644 --- a/mathics/builtin/mainloop.py +++ b/mathics/builtin/mainloop.py @@ -93,6 +93,7 @@ class In(Builtin): . In[2] = x = x + 1 . . In[1] = x = 1 + . """ attributes = A_LISTABLE | A_PROTECTED diff --git a/mathics/builtin/makeboxes.py b/mathics/builtin/makeboxes.py index 3ac80f9ab..e10c3f2f4 100644 --- a/mathics/builtin/makeboxes.py +++ b/mathics/builtin/makeboxes.py @@ -3,44 +3,17 @@ Low-level Format definitions """ - -from mathics.core.atoms import Integer -from mathics.core.attributes import A_HOLD_ALL_COMPLETE, A_READ_PROTECTED -from mathics.core.builtin import Builtin, Predefined -from mathics.core.symbols import Symbol -from mathics.format.box import ( - eval_generic_makeboxes, - eval_infix, - eval_makeboxes_fullform, - eval_postprefix, - format_element, - parenthesize, -) +from mathics.core.attributes import A_HOLD_ALL_COMPLETE +from mathics.core.builtin import Builtin +from mathics.core.expression import Expression +from mathics.core.systemsymbols import SymbolHoldForm +from mathics.format.box import format_element # TODO: Differently from the current implementation, MakeBoxes should only # accept as its format field the symbols in `$BoxForms`. This is something to # fix in a following step, changing the way in which Format and MakeBoxes work. -class BoxForms_(Predefined): - r""" - :WMA link:https://reference.wolfram.com/language/ref/\$BoxForms.html - -
    -
    '\$BoxForms' -
    contains the list of box formats. -
    - - >> $BoxForms - = ... - """ - - attributes = A_READ_PROTECTED - name = "$BoxForms" - rules = {"$BoxForms": "{StandardForm, TraditionalForm}"} - summary_text = "the list of box formats" - - class MakeBoxes(Builtin): """ :WMA link:https://reference.wolfram.com/language/ref/MakeBoxes.html @@ -89,56 +62,31 @@ class MakeBoxes(Builtin): """ attributes = A_HOLD_ALL_COMPLETE - + messages = { + "boxfmt": ( + "`1` in `2` is not a box formatting type. " + "A box formatting type is any member of $BoxForms." + ) + } rules = { - "MakeBoxes[Infix[head_[elements___]], " - " f:StandardForm|TraditionalForm|OutputForm]": ( - 'MakeBoxes[Infix[head[elements], StringForm["~`1`~", head]], f]' - ), "MakeBoxes[expr_]": "MakeBoxes[expr, StandardForm]", # The following rule is temporal. "MakeBoxes[expr_, form:(TeXForm|MathMLForm)]": "MakeBoxes[form[expr], StandardForm]", - ( - "MakeBoxes[(form:StandardForm|TraditionalForm)" - "[expr_], StandardForm|TraditionalForm|OutputForm]" - ): ("MakeBoxes[expr, form]"), - # BoxForms goes as second argument - "MakeBoxes[(form:StandardForm|TraditionalForm|OutputForm)[expr_], OutputForm]": "MakeBoxes[expr, form]", - "MakeBoxes[PrecedenceForm[expr_, prec_], f_]": "MakeBoxes[expr, f]", - "MakeBoxes[Style[expr_, OptionsPattern[Style]], f_]": ( - "StyleBox[MakeBoxes[expr, f], " - "ImageSizeMultipliers -> OptionValue[ImageSizeMultipliers]]" - ), } summary_text = "settable low-level translator from expression to display boxes" - def eval_fullform(self, expr, evaluation): - """MakeBoxes[expr_, FullForm]""" - return eval_makeboxes_fullform(expr, evaluation) - def eval_general(self, expr, f, evaluation): - """MakeBoxes[expr_, - f:TraditionalForm|StandardForm|OutputForm]""" - return eval_generic_makeboxes(expr, f, evaluation) + """MakeBoxes[expr_, f:StandardForm|TraditionalForm]""" + return format_element(expr, evaluation, f) - def eval_outerprecedenceform(self, expr, precedence, form, evaluation): - """MakeBoxes[PrecedenceForm[expr_, precedence_], - form:StandardForm|TraditionalForm|OutputForm]""" + def eval_general_custom(self, expr, mbexpr, f, evaluation): + """mbexpr:MakeBoxes[expr_, f_]""" + if f not in evaluation.definitions.boxforms: + expr = Expression(SymbolHoldForm, mbexpr) + evaluation.message("MakeBoxes", "boxfmt", f, expr) + return None - py_precedence = precedence.get_int_value() - boxes = MakeBoxes(expr, form) - return parenthesize(py_precedence, expr, boxes, True) - - def eval_postprefix(self, p, expr, h, precedence, form, evaluation): - """MakeBoxes[(p:Prefix|Postfix)[expr_, h_, precedence_:None], - form:StandardForm|TraditionalForm|OutputForm]""" - return eval_postprefix(self, p, expr, h, precedence, form, evaluation) - - def eval_infix( - self, expr, operator, precedence: Integer, grouping, form: Symbol, evaluation - ): - """MakeBoxes[Infix[expr_, operator_, precedence_:None, grouping_:None], form:StandardForm|TraditionalForm|OutputForm]""" - return eval_infix(self, expr, operator, precedence, grouping, form, evaluation) + return format_element(expr, evaluation, f) class ToBoxes(Builtin): @@ -162,13 +110,20 @@ class ToBoxes(Builtin): = SuperscriptBox["a", "b"] """ + messages = { + "boxfmt": ( + "`1` in `2` is not a box formatting type. " + "A box formatting type is any member of $BoxForms." + ) + } summary_text = "produce the display boxes of an evaluated expression" - def eval(self, expr, form, evaluation): - "ToBoxes[expr_, form_:StandardForm]" + def eval(self, expr, tbexpr, form, evaluation): + "tbexpr:ToBoxes[expr_, form_:StandardForm]" + if form not in evaluation.definitions.boxforms: + expr = Expression(SymbolHoldForm, tbexpr) + evaluation.message("ToBoxes", "boxfmt", form, expr) + return None - form_name = form.get_name() - if form_name is None: - evaluation.message("ToBoxes", "boxfmt", form) boxes = format_element(expr, evaluation, form) return boxes diff --git a/mathics/builtin/messages.py b/mathics/builtin/messages.py index b91f6e52f..b16f8c765 100644 --- a/mathics/builtin/messages.py +++ b/mathics/builtin/messages.py @@ -191,6 +191,7 @@ class General(Builtin): # "dgbgn": "Entering Dialog; use Return[] to exit.", "dgend": "Exiting Dialog.", "digit": "Digit at position `1` in `2` is too large to be used in base `3`.", + "dis": "Execution of external commands is disabled.", "exact": "Argument `1` is not an exact number.", "fnsym": ( "First argument in `1` is not a symbol " "or a string naming a symbol." @@ -335,12 +336,9 @@ class MessageName(InfixOperator): messages = {"messg": "Message cannot be set to `1`. It must be set to a string."} rules = { "MakeBoxes[MessageName[symbol_Symbol, tag_String], " - "f:StandardForm|TraditionalForm|OutputForm]": ( + "f:StandardForm|TraditionalForm]": ( 'RowBox[{MakeBoxes[symbol, f], "::", MakeBoxes[tag, f]}]' ), - "MakeBoxes[MessageName[symbol_Symbol, tag_String], InputForm]": ( - 'RowBox[{MakeBoxes[symbol, InputForm], "::", tag}]' - ), } summary_text = "associate a message name with a tag" diff --git a/mathics/builtin/numbers/calculus.py b/mathics/builtin/numbers/calculus.py index 501258da7..79afa04fc 100644 --- a/mathics/builtin/numbers/calculus.py +++ b/mathics/builtin/numbers/calculus.py @@ -417,13 +417,12 @@ class Derivative(PostfixOperator, SympyFunction): r' "\[Prime]\[Prime]", If[{n} === {1}, "\[Prime]", ' r' RowBox[{"(", Sequence @@ Riffle[{n}, ","], ")"}]]]]' ), - "MakeBoxes[Derivative[n:1|2][f_], form:OutputForm]": """RowBox[{MakeBoxes[f, form], If[n==1, "'", "''"]}]""", # The following rules should be applied in the eval method, instead of relying on the pattern matching # mechanism. "Derivative[0...][f_]": "f", "Derivative[n__Integer][Derivative[m__Integer][f_]] /; Length[{m}] " "== Length[{n}]": "Derivative[Sequence @@ ({n} + {m})][f]", - "Derivative[n__Integer][Alternatives[_Integer|_Rational|_Real|_Complex]]": "0 &", + "Derivative[n__Integer][_Integer|_Rational|_Real|_Complex]": "0 &", # The following rule tries to evaluate a derivative of a pure function by applying it to a list # of symbolic elements and use the rules in `D`. # The rule just applies if f is not a locked symbol, and it does not have a previous definition @@ -492,7 +491,7 @@ def __init__(self, *args, **kwargs): super(Derivative, self).__init__(*args, **kwargs) def eval_locked_symbols(self, n, **kwargs): - """Derivative[n__Integer][Alternatives[True|False|Symbol|TooBig|$Aborted|Removed|Locked|$PrintLiteral|$Off]]/; True""" + """Derivative[n__Integer][True|False|Symbol|TooBig|$Aborted|Removed|Locked|$PrintLiteral|$Off]/; True""" # Conditionals always come first... # Prevents the evaluation for True, False, and other Locked symbols # as function names. This produces a recursion error in the evaluation rule for Derivative. @@ -1008,8 +1007,8 @@ class Integrate(SympyFunction): = Integrate[1, {x, Infinity, 0}] Here how is an example of converting integral equation to TeX: - >> Integrate[f[x], {x, a, b}] // TeXForm - = \int_a^b f\left(x\right) \, dx + >> Integrate[f[x^2], {x, a, b}] // TeXForm + = \int_a^b f\left(x^2\right) \, dx Sometimes there is a loss of precision during integration. You can check the precision of your result with the following sequence \ @@ -1087,10 +1086,11 @@ def from_sympy(self, elements: Tuple[BaseElement, ...]) -> Expression: def eval(self, f, xs, evaluation: Evaluation, options: dict): # type: ignore[override] "Integrate[f_, xs__, OptionsPattern[]]" f_sympy = f.to_sympy() - if f_sympy.is_infinite: - return Expression(SymbolIntegrate, Integer1, xs).evaluate(evaluation) * f if f_sympy is None or isinstance(f_sympy, SympyExpression): return + + if f_sympy.is_infinite: + return Expression(SymbolIntegrate, Integer1, xs).evaluate(evaluation) * f xs = xs.get_sequence() vars = [] prec = None diff --git a/mathics/builtin/options.py b/mathics/builtin/options.py index 4e650bfbd..629a0a906 100644 --- a/mathics/builtin/options.py +++ b/mathics/builtin/options.py @@ -58,10 +58,10 @@ class All(Predefined): In :Plot: - /doc/reference-of-built-in-symbols/graphics-and-drawing/plotting-data/plot, \ + /doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/general-graphical-plots/plot, \ setting the :Mesh: -/doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values/mesh \ +/doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/drawing-options-and-option-values/mesh \ option to 'All' will show the specific plot points: >> Plot[x^2, {x, -1, 1}, MaxRecursion->5, Mesh->All] @@ -171,6 +171,20 @@ def matched(): return ListExpression(*list(matched())) +class FormatType(Predefined): + """ + :WMA link:https://reference.wolfram.com/language/ref/FormatType.html +
    +
    'FormatType' +
    is an option for output streams, graphics and functions like 'Text' \ + that specifies the default format. +
    + """ + + messages = {"ftype": "Value of option FormatType -> `` is not valid."} + summary_text = "specify the request format" + + class None_(Predefined): """ :WMA link:https://reference.wolfram.com/language/ref/None.html @@ -182,7 +196,7 @@ class None_(Predefined): Plot3D shows the mesh grid between computed points by default. This the :Mesh: -/doc/reference-of-built-in-symbols/graphics-and-drawing/drawing-options-and-option-values/mesh \ +/doc/reference-of-built-in-symbols/plotting-graphing-and-drawing/drawing-options-and-option-values/mesh \ However, you hide the mesh by setting the 'Mesh' option value to 'None': diff --git a/mathics/builtin/patterns/basic.py b/mathics/builtin/patterns/basic.py index 6aeb59175..3e6f44716 100644 --- a/mathics/builtin/patterns/basic.py +++ b/mathics/builtin/patterns/basic.py @@ -90,13 +90,9 @@ class Blank(_Blank): """ rules = { + ("MakeBoxes[Verbatim[Blank][], " "f:StandardForm|TraditionalForm]"): '"_"', ( - "MakeBoxes[Verbatim[Blank][], " - "f:StandardForm|TraditionalForm|OutputForm|InputForm]" - ): '"_"', - ( - "MakeBoxes[Verbatim[Blank][head_Symbol], " - "f:StandardForm|TraditionalForm|OutputForm|InputForm]" + "MakeBoxes[Verbatim[Blank][head_Symbol], " "f:StandardForm|TraditionalForm]" ): ('"_" <> MakeBoxes[head, f]'), } summary_text = "match to any single expression" @@ -154,8 +150,8 @@ class BlankNullSequence(_Blank): """ rules = { - "MakeBoxes[Verbatim[BlankNullSequence][], f:StandardForm|TraditionalForm|OutputForm|InputForm]": '"___"', - "MakeBoxes[Verbatim[BlankNullSequence][head_Symbol], f:StandardForm|TraditionalForm|OutputForm|InputForm]": '"___" <> MakeBoxes[head, f]', + "MakeBoxes[Verbatim[BlankNullSequence][], f:StandardForm|TraditionalForm]": '"___"', + "MakeBoxes[Verbatim[BlankNullSequence][head_Symbol], f:StandardForm|TraditionalForm]": '"___" <> MakeBoxes[head, f]', } summary_text = "match to a sequence of zero or more elements" @@ -245,8 +241,8 @@ class BlankSequence(_Blank): """ rules = { - "MakeBoxes[Verbatim[BlankSequence][], f:StandardForm|TraditionalForm|OutputForm|InputForm]": '"__"', - "MakeBoxes[Verbatim[BlankSequence][head_Symbol], f:StandardForm|TraditionalForm|OutputForm|InputForm]": '"__" <> MakeBoxes[head, f]', + "MakeBoxes[Verbatim[BlankSequence][], f:StandardForm|TraditionalForm]": '"__"', + "MakeBoxes[Verbatim[BlankSequence][head_Symbol], f:StandardForm|TraditionalForm]": '"__" <> MakeBoxes[head, f]', } summary_text = "match to a non-empty sequence of elements" diff --git a/mathics/builtin/patterns/composite.py b/mathics/builtin/patterns/composite.py index 9b6a73a00..d869efc48 100644 --- a/mathics/builtin/patterns/composite.py +++ b/mathics/builtin/patterns/composite.py @@ -441,7 +441,7 @@ class Pattern(PatternObject): ( "MakeBoxes[Verbatim[Pattern][symbol_Symbol, blank_Blank|" "blank_BlankSequence|blank_BlankNullSequence], " - "f:StandardForm|TraditionalForm|InputForm|OutputForm]" + "(f:StandardForm|TraditionalForm)]" ): "MakeBoxes[symbol, f] <> MakeBoxes[blank, f]", # 'StringForm["`1``2`", HoldForm[symbol], blank]', } diff --git a/mathics/builtin/patterns/defaults.py b/mathics/builtin/patterns/defaults.py index ceef0f6c0..fbfdcd115 100644 --- a/mathics/builtin/patterns/defaults.py +++ b/mathics/builtin/patterns/defaults.py @@ -19,41 +19,75 @@ class Optional(InfixOperator, PatternObject): - """ - - :WMA link:https://reference.wolfram.com/language/ref/Optional.html + """:WMA link:https://reference.wolfram.com/language/ref/Optional.html
    'Optional'[$pattern$, $default$]
    '$pattern$ : $default$' -
    is a pattern which matches $pattern$, which if omitted - should be replaced by $default$. +
    is a pattern matching $pattern$; when $pattern$ is omitted, \ + $default$ is substituted for $pattern$.
    + Optional is used to specify optional arguments in function signatures. + + Set up a default value of 1 for the pattern 'y_' in function 'f': + >> f[x_, y_:1] := {x, y} - >> f[1, 2] - = {1, 2} + + Above, we put no spaces before or after ':', but they can be added. So: + + >> f[x_, y_: 1] := {x, y} + + is the same as the above. + + When we specify a value for the 'y' parameter, it has the value provided: + >> f[a, 2] + = {a, 2} + + But if the 'y' parameter is missing, we replace the parameter \ + using the default given in the delayed assignment above: + >> f[a] = {a, 1} - Note that '$symb$ : $pattern$' represents a 'Pattern' object. However, there is no - disambiguity, since $symb$ has to be a symbol in this case. + Both 'Optional' and :Pattern: + /doc/reference-of-built-in-symbols/rules-and-patterns/composite-patterns/pattern/ \ + use ':' as their operator symbol. And both operators are used to represent a pattern. - >> x:_ // FullForm - = Pattern[x, Blank[]] - >> _:d // FullForm - = Optional[Blank[], d] - >> x:_+y_:d // FullForm - = Pattern[x, Plus[Blank[], Optional[Pattern[y, Blank[]], d]]] + The way to disambiguate which of the two is used is by the first or left operand. When \ + this is a $symbol$, like 'y', the ':' operator indicates a 'Pattern': - 's_.' is equivalent to 'Optional[s_]' and represents an optional parameter which, if omitted, - gets its value from 'Default'. - >> FullForm[s_.] - = Optional[Pattern[s, Blank[]]] + >> y : 1 // FullForm + = Pattern[y, 1] + + In contrast, we have a pattern to the left of the colon, like 'y_' we have an 'Optional' expression: + + >> y_ : 1 // FullForm + = Optional[Pattern[y, Blank[]], 1] + + The special form 'y_.' is equivalent to 'Optional[y_]': + + >> FullForm[y_.] + = Optional[Pattern[y, Blank[]]] + + In this situation, when the is 'y' parameter omitted, the value comes from \ + :Default:/doc/reference-of-built-in-symbols/options-management/default/: + + >> Default[g] = 4 + = 4 + + >> g[x_, y_.] := {x, y} + + >> g[a] + = {a, 4} + + Note that the 'Optional' operator binds more tightly than the \ + 'Pattern'. Keep this in mind when there is more than one colon, \ + juxtaposed, each representing different operators: + + >> x : _+y_ : d // FullForm + = Pattern[x, Plus[Blank[], Optional[Pattern[y, Blank[]], d]]] - >> Default[h, k_] := k - >> h[a] /. h[x_, y_.] -> {x, y} - = {a, 2} """ arg_counts = [1, 2] @@ -65,8 +99,29 @@ class Optional(InfixOperator, PatternObject): } grouping = "Right" rules = { - "MakeBoxes[Verbatim[Optional][Verbatim[Pattern][symbol_Symbol, Verbatim[_]]], f:StandardForm|TraditionalForm|InputForm|OutputForm]": 'MakeBoxes[symbol, f] <> "_."', - "MakeBoxes[Verbatim[Optional][Verbatim[_]], f:StandardForm|TraditionalForm|InputForm|OutputForm]": '"_."', + ( + "MakeBoxes[Verbatim[Optional][" + "Verbatim[Pattern][symbol_Symbol," + "(kind:(Verbatim[Blank]|Verbatim[BlankSequence]|Verbatim[BlankNullSequence])[])]], " + "(f:StandardForm|TraditionalForm)]" + ): 'MakeBoxes[symbol, f] <> ToString[kind, f] <>"."', + ( + "MakeBoxes[Verbatim[Optional][" + "(kind:(Verbatim[Blank]|Verbatim[BlankSequence]|Verbatim[BlankNullSequence])[])], " + "(f:StandardForm|TraditionalForm)]" + ): 'ToString[kind, f]<>"."', + # Two arguments + ( + "MakeBoxes[Verbatim[Optional][" + "Verbatim[Pattern][symbol_Symbol," + "(kind:(Verbatim[Blank]|Verbatim[BlankSequence]|Verbatim[BlankNullSequence])[]), value_]], " + "(f:StandardForm|TraditionalForm)]" + ): 'RowBox[{MakeBoxes[symbol, f], ToString[kind, f], ":",MakeBoxes[value, f]}]', + ( + "MakeBoxes[Verbatim[Optional][" + "(kind:(Verbatim[Blank]|Verbatim[BlankSequence]|Verbatim[BlankNullSequence])[]), value_], " + "(f:StandardForm|TraditionalForm)]" + ): 'RowBox[{ToString[kind, f], ":", MakeBoxes[value, f]}]', } summary_text = "an optional argument with a default value" diff --git a/mathics/builtin/system.py b/mathics/builtin/system.py index 397ebdf56..9fcb58b45 100644 --- a/mathics/builtin/system.py +++ b/mathics/builtin/system.py @@ -6,14 +6,16 @@ import _thread import gc +import inspect import os import platform import subprocess import sys +from functools import wraps from pympler.asizeof import asizeof -from mathics import version_string +from mathics import settings, version_string from mathics.core.atoms import Integer, Integer0, IntegerM1, Real, String from mathics.core.attributes import A_CONSTANT from mathics.core.builtin import Builtin, Predefined @@ -40,6 +42,40 @@ sort_order = "mathics.builtin.global-system-information" +def not_in_sandboxed_environment(func): + """ + A decorator for eval() and evaluate() methods which + checks that mathics.settings.ENABLE_SYSTEM_COMMANDS is set. + In other words, check to see if we are not in a sandboxed + environment. + + If we are sandboxed, a "dis" message is printed and we return $Failed. + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): + # Get the function signature once during decoration + sig = inspect.signature(func) + + # Here is we check to see if we are in a sandoxed environment. + if not settings.ENABLE_SYSTEM_COMMANDS: + # Bind args/kwargs to the function's parameter names + bound_args = sig.bind(self, *args, **kwargs) + # Apply default values for any missing arguments + bound_args.apply_defaults() + + # Access "evaluation" by name, regardless of its position + self = bound_args.arguments.get("self") + evaluation = bound_args.arguments.get("evaluation") + + evaluation.message(self.__class__.__name__, "dis") + return SymbolFailed + + return func(self, *args, **kwargs) + + return wrapper + + class Breakpoint(Builtin): """:Python breakpoint():https://docs.python.org/3/library/functions.html#breakpoint @@ -63,10 +99,9 @@ class Breakpoint(Builtin): Here is how to use 'mathics.disabled_breakpoint': - >> SetEnvironment["PYTHONBREAKPOINT" -> "mathics.disabled_breakpoint"]; + X> SetEnvironment["PYTHONBREAKPOINT" -> "mathics.disabled_breakpoint"]; - >> Breakpoint[] - = Hit disabled breakpoint. + X> Breakpoint[] = Breakpoint[] The environment variable 'PYTHONBREAKPOINT' can be changed at runtime to switch \ @@ -75,8 +110,10 @@ class Breakpoint(Builtin): summary_text = "invoke Python breakpoint()" + @not_in_sandboxed_environment def eval(self, evaluation: Evaluation): "Breakpoint[]" + breakpoint() @@ -88,7 +125,7 @@ class CommandLine(Predefined):
    is a list of strings passed on the command line to launch the Mathics3 session.
    - >> $CommandLine + S> $CommandLine = {...} """ @@ -98,6 +135,7 @@ class CommandLine(Predefined): "session was launched" ) + @not_in_sandboxed_environment def evaluate(self, evaluation: Evaluation) -> Expression: return ListExpression(*(String(arg) for arg in sys.argv)) @@ -123,8 +161,10 @@ class Environment(Builtin): summary_text = "list the system environment variables" + @not_in_sandboxed_environment def eval(self, var, evaluation: Evaluation): "Environment[var_String]" + env_var = var.get_string_value() if env_var not in os.environ: return SymbolFailed @@ -168,11 +208,15 @@ class GetEnvironment(Builtin): /doc/reference-of-built-in-symbols/global-system-information/setenvironment/. """ - messages = {"name": "`1` is not ALL or a string or a list of strings."} + messages = { + "name": "`1` is not ALL or a string or a list of strings.", + } summary_text = "retrieve the value of a system environment variable" + @not_in_sandboxed_environment def eval(self, var, evaluation: Evaluation): "GetEnvironment[var___]" + if isinstance(var, String): env_var = var.value tup = ( @@ -305,8 +349,12 @@ class MachineName(Predefined): name = "$MachineName" summary_text = "get the name of computer that Mathics3 is running" + @not_in_sandboxed_environment def evaluate(self, evaluation: Evaluation) -> String: - return String(platform.uname().node) + try: + return String(platform.uname().node) + except Exception: + return String("unknown") class MathicsVersion(Predefined): @@ -477,14 +525,14 @@ class ParentProcessID(Predefined): system under which it is run. - >> $ParentProcessID + S> $ParentProcessID = ... - """ name = "$ParentProcessID" summary_text = "get process id of the process that invoked Mathics3" + @not_in_sandboxed_environment def evaluate(self, evaluation: Evaluation) -> Integer: return Integer(os.getppid()) @@ -499,13 +547,14 @@ class ProcessID(Predefined): which it is run. - >> $ProcessID + S> $ProcessID = ... """ name = "$ProcessID" summary_text = "get process id of the Mathics process" + @not_in_sandboxed_environment def evaluate(self, evaluation: Evaluation) -> Integer: return Integer(os.getpid()) @@ -573,8 +622,10 @@ class Run(Builtin): summary_text = "run a system command" + @not_in_sandboxed_environment def eval(self, command, evaluation: Evaluation): "Run[command_String]" + command_str = command.to_python() return Integer(subprocess.call(command_str, shell=True)) @@ -588,13 +639,14 @@ class ScriptCommandLine(Predefined):
    is a list of string arguments when running the kernel is script mode. - >> $ScriptCommandLine + S> $ScriptCommandLine = {...} """ summary_text = "list of command line arguments" name = "$ScriptCommandLine" + @not_in_sandboxed_environment def evaluate(self, evaluation: Evaluation): try: dash_index = sys.argv.index("--") @@ -681,11 +733,15 @@ class SetEnvironment(Builtin): /doc/reference-of-built-in-symbols/global-system-information/getenvironment/. """ - messages = {"value": "`1` must be a string or None."} + messages = { + "value": "`1` must be a string or None.", + } summary_text = "set system environment variable(s)" + @not_in_sandboxed_environment def eval(self, rule, evaluation): "SetEnvironment[rule_Rule]" + env_var_name, env_var_value = rule.elements # WMA does not give an error message if env_var_name is not a String - weird. if not isinstance(env_var_name, String): @@ -695,11 +751,13 @@ def eval(self, rule, evaluation): evaluation.message("SetEnvironment", "value", env_var_value) return SymbolFailed - os.environ[env_var_name.value] = ( - None if None is SymbolNone else env_var_value.value - ) + if env_var_value is SymbolNone: + os.environ.pop(env_var_name.value, None) + else: + os.environ[env_var_name.value] = env_var_value.value return SymbolNull + @not_in_sandboxed_environment def eval_list(self, rules: Expression, evaluation: Evaluation): "SetEnvironment[{rules__}]" @@ -832,6 +890,7 @@ class UserName(Predefined): name = "$UserName" summary_text = "get login name of the user that invoked the current session" + @not_in_sandboxed_environment def evaluate(self, evaluation: Evaluation) -> String: try: user = os.getlogin() @@ -896,13 +955,14 @@ class SystemMemory(Predefined):
    Returns the total amount of physical memory. - >> $SystemMemory + S> $SystemMemory = ... """ name = "$SystemMemory" summary_text = "get the total amount of physical memory in the system" + @not_in_sandboxed_environment def evaluate(self, evaluation: Evaluation) -> Integer: totalmem = psutil.virtual_memory().total return Integer(totalmem) @@ -916,59 +976,64 @@ class MemoryAvailable(Builtin):
    Returns the amount of the available physical memory. - >> MemoryAvailable[] + S> MemoryAvailable[] = ... The relationship between \\$SystemMemory, MemoryAvailable, and MemoryInUse: - >> $SystemMemory > MemoryAvailable[] > MemoryInUse[] + S> $SystemMemory > MemoryAvailable[] > MemoryInUse[] = True """ summary_text = "get the available amount of physical memory in the system" + @not_in_sandboxed_environment def eval(self, evaluation: Evaluation) -> Integer: """MemoryAvailable[]""" + totalmem = psutil.virtual_memory().available return Integer(totalmem) else: - class SystemMemory(Predefined): + class MemoryAvailable(Builtin): """ - :WMA link:https://reference.wolfram.com/language/ref/SystemMemory.html + :WMA link:https://reference.wolfram.com/language/ref/MemoryAvailable.html
    -
    '$SystemMemory' -
    Returns the total amount of physical memory when Python module "psutil" is installed. +
    'MemoryAvailable' +
    Returns the amount of the available physical memory when Python module "psutil" is installed. This system however doesn't have that installed, so -1 is returned instead.
    - >> $SystemMemory + S> MemoryAvailable[] = -1 """ - summary_text = "the total amount of physical memory in the system" - name = "$SystemMemory" + summary_text = "get the available amount of physical memory in the system" + + @not_in_sandboxed_environment + def eval(self, evaluation: Evaluation) -> Integer: + """MemoryAvailable[]""" - def evaluate(self, evaluation: Evaluation) -> Integer: return IntegerM1 - class MemoryAvailable(Builtin): + class SystemMemory(Predefined): """ - :WMA link:https://reference.wolfram.com/language/ref/MemoryAvailable.html + :WMA link:https://reference.wolfram.com/language/ref/SystemMemory.html
    -
    'MemoryAvailable' -
    Returns the amount of the available physical when Python module "psutil" is installed. +
    '$SystemMemory' +
    Returns the total amount of physical memory when Python module "psutil" is installed. This system however doesn't have that installed, so -1 is returned instead.
    - >> MemoryAvailable[] + S> $SystemMemory = -1 """ - summary_text = "get the available amount of physical memory in the system" + summary_text = "the total amount of physical memory in the system" + name = "$SystemMemory" - def eval(self, evaluation: Evaluation) -> Integer: - """MemoryAvailable[]""" + @not_in_sandboxed_environment + def evaluate(self, evaluation: Evaluation) -> Integer: return IntegerM1 diff --git a/mathics/core/atoms/strings.py b/mathics/core/atoms/strings.py index a58a0a0fe..1fbe8d0e6 100644 --- a/mathics/core/atoms/strings.py +++ b/mathics/core/atoms/strings.py @@ -9,7 +9,7 @@ from mathics.core.element import BoxElementMixin from mathics.core.keycomparable import BASIC_ATOM_STRING_ELT_ORDER -from mathics.core.symbols import Atom, Symbol, SymbolTrue, symbol_set +from mathics.core.symbols import Atom, Symbol, SymbolFalse, SymbolTrue, symbol_set from mathics.core.systemsymbols import SymbolFullForm, SymbolInputForm SymbolString = Symbol("String") @@ -41,8 +41,17 @@ def atom_to_boxes(self, f, evaluation): inner = str(self.value) if f in SYSTEM_SYMBOLS_INPUT_OR_FULL_FORM: - inner = '"' + inner.replace("\\", "\\\\") + '"' - return _boxed_string(inner, **{"System`ShowStringCharacters": SymbolTrue}) + inner = inner.replace("\\", "\\\\") + inner = inner.replace('"', '\\"') + inner = f'"{inner}"' + return _boxed_string( + inner, + **{ + "System`NumberMarks": SymbolTrue, + "System`ShowSpecialCharacters": SymbolFalse, + "System`ShowStringCharacters": SymbolTrue, + }, + ) return String('"' + inner + '"') def do_copy(self) -> "String": @@ -84,6 +93,10 @@ def is_literal(self) -> bool: """ return True + @property + def is_multiline(self) -> bool: + return "\n" in self.value + def sameQ(self, rhs) -> bool: """Mathics SameQ""" return isinstance(rhs, String) and self.value == rhs.value diff --git a/mathics/core/builtin.py b/mathics/core/builtin.py index dc037ec50..dc8a9ac0e 100644 --- a/mathics/core/builtin.py +++ b/mathics/core/builtin.py @@ -347,7 +347,7 @@ def contextify_form_name(f): """Handle adding 'System`' to a form name, unless it's "" (meaning the rule applies to all forms). """ - return "" if f == "" else ensure_context(f) + return f if f in ("", "_MakeBoxes") else ensure_context(f) if isinstance(pattern, tuple): forms, pattern = pattern @@ -383,6 +383,9 @@ def contextify_form_name(f): formatvalues[form].append( Rule(pattern, parse_builtin_rule(replace), system=True) ) + + formatvalues.setdefault("_MakeBoxes", []).extend(box_rules) + for form, formatrules in formatvalues.items(): formatrules.sort(key=lambda x: x.pattern_precedence) @@ -434,10 +437,6 @@ def contextify_form_name(f): else: definitions.builtin[name] = definition - makeboxes_def = definitions.builtin["System`MakeBoxes"] - for rule in box_rules: - makeboxes_def.add_rule(rule) - # This method is used to produce generic argument mismatch errors # (tags: "argx", "argr", "argrx", "argt", or "argtu") for builtin # functions that define this as an eval method. e.g. For example @@ -1353,7 +1352,6 @@ def __init__(self, *args, **kwargs): "MakeBoxes[{0}, form:StandardForm|TraditionalForm]".format( op_pattern ): formatted, - f"MakeBoxes[{op_pattern}, form:InputForm|OutputForm]": formatted, } default_rules.update(self.rules) self.rules = default_rules diff --git a/mathics/core/convert/op.py b/mathics/core/convert/op.py index f93b0d7a8..0ba25c99f 100644 --- a/mathics/core/convert/op.py +++ b/mathics/core/convert/op.py @@ -15,7 +15,7 @@ # Load the conversion tables from disk -characters_path = osp.join(ROOT_DIR, "data", "op-tables.json") +characters_path = osp.join(ROOT_DIR, "data", "character-tables.json") assert osp.exists( characters_path ), f"ASCII operator to Unicode tables are missing from {characters_path}" @@ -24,15 +24,18 @@ ascii_operator_to_symbol = OPERATOR_CONVERSION_TABLES["ascii-operator-to-symbol"] builtin_constants = OPERATOR_CONVERSION_TABLES["builtin-constants"] +named_characters = OPERATOR_CONVERSION_TABLES["named-characters"] operator_to_unicode = OPERATOR_CONVERSION_TABLES["operator-to-unicode"] operator_to_ascii = OPERATOR_CONVERSION_TABLES["operator-to-ascii"] unicode_operator_to_ascii = { val: operator_to_ascii[key] for key, val in operator_to_unicode.items() } -unicode_to_amslatex = OPERATOR_CONVERSION_TABLES["unicode-to-amslatex"] +UNICODE_TO_AMSLATEX = OPERATOR_CONVERSION_TABLES.get("unicode-to-amslatex", {}) +UNICODE_TO_LATEX = OPERATOR_CONVERSION_TABLES.get("unicode-to-latex", {}) -amstex_operators = { + +AMSTEX_OPERATORS = { "\u2032": "'", "\u2032\u2032": "''", "\u2062": " ", @@ -92,7 +95,7 @@ def hex_form_code(char_str): return hex(ord(char_str))[2:] for candidate_dict in ( - unicode_to_amslatex, + UNICODE_TO_AMSLATEX, # amstex_operators, unicode_operator_to_ascii, ): @@ -106,10 +109,12 @@ def hex_form_code(char_str): # if it is already an ascii, return without changes. if unicode_op.isascii(): return unicode_op - - # the `unicode_op` cannot be converted into an ascii string. Show a - # warning and return a `\symbol{code}` expression. - logging.warning( - "Unicode op" + unicode_op + "(" + hex_form_code(unicode_op) + ") not found." - ) - return '\\symbol{"' + hex_form_code(unicode_op) + "}" + try: + return r"\text{" + UNICODE_TO_LATEX[unicode_op] + "}" + except KeyError: + # the `unicode_op` cannot be converted into an ascii string. Show a + # warning and return a `\symbol{code}` expression. + logging.warning( + "Unicode op" + unicode_op + "(" + hex_form_code(unicode_op) + ") not found." + ) + return '\\symbol{"' + hex_form_code(unicode_op) + "}" diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index 359c6817e..47355204f 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -23,11 +23,19 @@ from mathics.core.util import canonic_filename from mathics.settings import ROOT_DIR -# The contents of $OutputForms. FormMeta in mathics.base.forms adds to this. -OutputForms: Set[Symbol] = set() +# Collections of format symbols. Here we load some basic cases. +# More symbols are populated from FormMeta classes (see `mathics.builtin.forms.base`) -# The contents of $PrintForms. FormMeta in mathics.base.forms adds to this. -PrintForms: Set[Symbol] = set() +# The contents of $BoxForms. +BOX_FORMS: Set[Symbol] = set() + +# The contents of $PrintForms. +PRINT_FORMS: Set[Symbol] = set() +# PRINT_FORMS.update(BOX_FORMS) + +# The contents of $OutputForms. +OUTPUT_FORMS: Set[Symbol] = set() +# OUTPUT_FORMS.update(PRINT_FORMS) class Definition: @@ -167,19 +175,9 @@ def __init__( self.trace_show_rewrite = False self.timing_trace_evaluation = False - # Importing "mathics.format.render" populates the Symbol of the - # PrintForms and OutputForms sets. - # - # If "importlib" is used instead of "import", then we get: - # TypeError: boxes_to_text() takes 1 positional argument but - # 2 were given - # Rocky: this smells of something not quite right in terms of - # modularity. - - import mathics.format.render # noqa - - self.printforms = list(PrintForms) - self.outputforms = list(OutputForms) + self.boxforms = list(BOX_FORMS) + self.printforms = list(PRINT_FORMS) + self.outputforms = list(OUTPUT_FORMS) if add_builtin: load_builtin_definitions(self, builtin_filename, extension_modules) @@ -787,6 +785,19 @@ def set_options(self, name: str, options) -> None: def unset(self, name: str, expr: BaseElement) -> bool: """Remove the rule corresponding to the expression `expr` in the definition of Symbol `name`""" + # These special symbol names are stored + # as attributes of the `Definitions` object. + # If this grows, maybe we should split this code. + if name == "System`$BoxForms": + self.boxforms = list(BOX_FORMS) + return True + if name == "System`$PrintForms": + self.printforms = list(PRINT_FORMS) + return True + if name == "System`$OutputForms": + self.outputforms = list(OUTPUT_FORMS) + return True + definition = self.get_user_definition(self.lookup_name(name)) result = definition.remove_rule(expr) self.mark_changed(definition) diff --git a/mathics/core/element.py b/mathics/core/element.py index fad9c4860..ec0161cb7 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -407,6 +407,10 @@ class BoxElementMixin(ImmutableValueMixin): elements """ + @property + def is_multiline(self) -> bool: + return True + def boxes_to_format(self, format: str, **options) -> str: from mathics.core.formatter import boxes_to_format diff --git a/mathics/core/expression.py b/mathics/core/expression.py index b527c636c..35a34c978 100644 --- a/mathics/core/expression.py +++ b/mathics/core/expression.py @@ -1434,7 +1434,12 @@ def round_to_float( def sameQ(self, other: BaseElement) -> bool: """Mathics3 SameQ""" if not isinstance(other, Expression): - return False + # TODO: consider the alternative + # if not hasattr(other, "to_expression"): + # return False + # other = other.to_expression() + # + return other.sameQ(self) if self is other: return True diff --git a/mathics/core/load_builtin.py b/mathics/core/load_builtin.py index dfff80c10..29479e09f 100644 --- a/mathics/core/load_builtin.py +++ b/mathics/core/load_builtin.py @@ -133,11 +133,8 @@ def definition_contribute(definitions): Load the Definition objects associated to all the builtins on `Definitions` """ - # let MakeBoxes contribute first - _builtins["System`MakeBoxes"].contribute(definitions) for name, item in _builtins.items(): - if name != "System`MakeBoxes": - item.contribute(definitions) + item.contribute(definitions) from mathics.core.definitions import Definition from mathics.core.expression import ensure_context @@ -203,10 +200,14 @@ def import_and_load_builtins(): """ # TODO: Check if this is the expected behavior, or it the structures # must be cleaned. + if len(mathics3_builtins_modules) > 0: logging.warning("``import_and_load_builtins`` should be called just once...") return + # Load render the routines + importlib.import_module("mathics.format.render") + builtin_path = osp.join( osp.dirname( __file__, diff --git a/mathics/core/parser/__init__.py b/mathics/core/parser/__init__.py index cdc9ffc6f..cb8178d4e 100644 --- a/mathics/core/parser/__init__.py +++ b/mathics/core/parser/__init__.py @@ -18,7 +18,7 @@ MathicsMultiLineFeeder, MathicsSingleLineFeeder, ) -from mathics.core.parser.operators import all_operator_names +from mathics.core.parser.operators import all_operator_names, operator_precedences from mathics.core.parser.util import parse, parse_builtin_rule __all__ = [ @@ -29,6 +29,7 @@ "MathicsSingleLineFeeder", "all_operator_names", "is_symbol_name", + "operator_precedences", "parse", "parse_builtin_rule", ] diff --git a/mathics/core/parser/operators.py b/mathics/core/parser/operators.py index 7ebc1216c..6418f5187 100644 --- a/mathics/core/parser/operators.py +++ b/mathics/core/parser/operators.py @@ -35,7 +35,9 @@ nonassoc_binary_operators = OPERATOR_DATA["non-associative-binary-operators"] operator_precedences = OPERATOR_DATA["operator-precedences"] operator_to_amslatex = OPERATOR_DATA["operator-to-amslatex"] -operator_to_string = OPERATOR_DATA["operator-to-string"] +operator_to_string = OPERATOR_DATA.get( + "operator-to-string", OPERATOR_DATA.get("operator-to_string", {}) +) postfix_operators = OPERATOR_DATA["postfix-operators"] prefix_operators = OPERATOR_DATA["prefix-operators"] right_binary_operators = OPERATOR_DATA["right-binary-operators"] diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index 2fd08b279..f1bb74fca 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -53,6 +53,7 @@ SymbolBlankNullSequence = Symbol("System`BlankNullSequence") SymbolBlankSequence = Symbol("System`BlankSequence") SymbolBlend = Symbol("System`Blend") +SymbolBoxForms = Symbol("System`$BoxForms") SymbolBoxRatios = Symbol("System`BoxRatios") SymbolBoxStyle = Symbol("System`BoxStyle") SymbolBoxed = Symbol("System`Boxed") @@ -158,6 +159,8 @@ SymbolInner = Symbol("System`Inner") SymbolInputForm = Symbol("System`InputForm") SymbolInputStream = Symbol("System`InputStream") +SymbolInset = Symbol("System`Inset") +SymbolInsetBox = Symbol("System`InsetBox") SymbolInteger = Symbol("System`Integer") SymbolIntegrate = Symbol("System`Integrate") SymbolKey = Symbol("System`Key") @@ -222,6 +225,7 @@ SymbolOverflow = Symbol("System`Overflow") SymbolOwnValues = Symbol("System`OwnValues") SymbolPackages = Symbol("System`$Packages") +SymbolParentForm = Symbol("System`ParentForm") SymbolPart = Symbol("System`Part") SymbolPath = Symbol("System`$Path") SymbolPattern = Symbol("System`Pattern") @@ -315,6 +319,7 @@ SymbolTan = Symbol("System`Tan") SymbolTanh = Symbol("System`Tanh") SymbolTeXForm = Symbol("System`TeXForm") +SymbolText = Symbol("System`Text") SymbolThreshold = Symbol("System`Threshold") SymbolThrow = Symbol("System`Throw") SymbolTicks = Symbol("System`Ticks") diff --git a/mathics/data/.gitignore b/mathics/data/.gitignore index ba1ae5260..9c07e628a 100644 --- a/mathics/data/.gitignore +++ b/mathics/data/.gitignore @@ -1,5 +1,5 @@ /.python-version /doc_latex_data.pcl /doctest_latex_data.pcl -/op-tables.json +/character-tables.json /operator-tables.json diff --git a/mathics/doc/doc_entries.py b/mathics/doc/doc_entries.py index e6dd99474..a16f2acf2 100644 --- a/mathics/doc/doc_entries.py +++ b/mathics/doc/doc_entries.py @@ -317,7 +317,7 @@ class DocTest: the documentation. * `X>` Shows the example in the docs, but disables testing the example. * `S>` Shows the example in the docs, but disables testing if environment - variable SANDBOX is set. + variable MATHICS3_SANDBOX is set. * `=` Compares the result text. * `:` Compares an (error) message. `|` Prints output. @@ -362,8 +362,11 @@ def strip_sentinal(line: str): self.private = testcase[0] == "#" # Ignored test cases are NOT executed, but shown as part of the docs - # Sandboxed test cases are NOT executed if environment SANDBOX is set - if testcase[0] == "X" or (testcase[0] == "S" and getenv("SANDBOX", False)): + # Sandboxed test cases are NOT executed if environment MATHICS3_SANDBOX is set + from mathics import settings + + is_sandbox = not settings.ENABLE_SYSTEM_COMMANDS + if testcase[0] == "X" or (testcase[0] == "S" and is_sandbox): self.ignore = True # substitute '>' again so we get the correct formatting testcase[0] = ">" diff --git a/mathics/doc/documentation/1-Manual.mdoc b/mathics/doc/documentation/1-Manual.mdoc index 3e545a2e6..80e6cde1f 100644 --- a/mathics/doc/documentation/1-Manual.mdoc +++ b/mathics/doc/documentation/1-Manual.mdoc @@ -890,29 +890,36 @@ There are several methods to display expressions in 2-D: >> Subscript[a, 1, 2] // TeXForm = a_{1, 2} -If you want even more low-level control over expression display, override 'MakeBoxes': >> MakeBoxes[b, TraditionalForm] = "c"; >> b = b ## This will be displayed as c in the browser and LaTeX documentation. -This will even apply to 'TeXForm', because 'TeXForm' implies 'TraditionalForm': +In the browser, this will even apply to 'TeXForm', because 'TeXForm' implies 'TraditionalForm': >> b // TeXForm - = c + = ... + +Notice however that in the CLI, default output formatted in 'OutputForm', +which do not take into account 'MakeBoxes' rules. The same happens if we +ask explicitly for this form: -Except some other form is applied first: >> b // OutputForm // TeXForm - = b -'MakeBoxes' for another form: + = \text{b} +In a similar way, in the CLI, we can ask for TraditionalForm explicitly + >> b // TraditionalForm // TeXForm + = c + +'MakeBoxes' for another form: >> MakeBoxes[TeXForm[b], form_] = "d"; >> b // TeXForm - = d + = ... + You can cause a much bigger mess by overriding 'MakeBoxes' than by sticking to 'Format', e.g. generate invalid XML: - >> MakeBoxes[MathMLForm[c], form_] = "> c // MathMLForm + >> MakeBoxes[MathMLForm[c], form_] := "> c // MathMLForm //StandardForm = > {1, 2, 3} = {1, 2, 3} - #> {1, 2, 3} // TeXForm - = \left[1 2 3\right] + >> {1, 2, 3} // TeXForm + = [1 2 3] However, this will not be accepted as input to \Mathics anymore: >> [1 2 3] - : Expression cannot begin with "[1 2 3]" (line 1 of ""). + : ... >> Clear[MakeBoxes] @@ -963,7 +970,7 @@ The desired effect can be achieved in the following way: = squared[1, 2] #> squared[1, 2] // TeXForm - = \text{squared}\left[1, 2\right]^2 + = \text{squared}[1, 2]^2 You can view the box structure of a formatted expression using 'ToBoxes': @@ -1108,7 +1115,7 @@ Three-dimensional plots are supported as well: Here are some examples written by Jan Pöschko that come from the very first version.
    -Let\'s sketch the function +Let's sketch the function >> f[x_] := 4 x / (x ^ 2 + 3 x + 5) The derivatives are: diff --git a/mathics/doc/latex_doc.py b/mathics/doc/latex_doc.py index 968aa9a11..41408d101 100644 --- a/mathics/doc/latex_doc.py +++ b/mathics/doc/latex_doc.py @@ -540,7 +540,7 @@ class LaTeXDocTest(DocTest): the documentation. * `X>` Shows the example in the docs, but disables testing the example. * `S>` Shows the example in the docs, but disables testing if environment - variable SANDBOX is set. + variable MATHICS3_SANDBOX is set. * `=` Compares the result text. * `:` Compares an (error) message. `|` Prints output. diff --git a/mathics/docpipeline.py b/mathics/docpipeline.py index 6200150e4..403f3372f 100644 --- a/mathics/docpipeline.py +++ b/mathics/docpipeline.py @@ -274,7 +274,7 @@ def create_output(test_pipeline, tests): if output_format in ("latex", "xml"): def out_wrapper(expr): - return f"{expr} // StandardForm" + return f"StandardForm[{expr}]" else: diff --git a/mathics/eval/assignments/assignment.py b/mathics/eval/assignments/assignment.py index fa074b447..07cda6447 100644 --- a/mathics/eval/assignments/assignment.py +++ b/mathics/eval/assignments/assignment.py @@ -18,6 +18,7 @@ from mathics.core.atoms import Integer, Integer1 from mathics.core.attributes import A_LOCKED, attribute_string_to_number from mathics.core.builtin import Builtin +from mathics.core.definitions import BOX_FORMS from mathics.core.element import BaseElement from mathics.core.evaluation import ( MAX_RECURSION_DEPTH, @@ -217,6 +218,30 @@ def reduce_attributes_from_list(x_att: int, y_att: str) -> int: return True +def eval_assign_boxforms(self, lhs, rhs, evaluation) -> bool: + if not rhs.has_form("List", None): + evaluation.message("$BoxForms", "formset", rhs) + return False + elements = rhs.elements + if not all(form in elements for form in BOX_FORMS): + evaluation.message("$BoxForms", "formset", rhs) + return False + if not all(isinstance(form, Symbol) for form in elements): + evaluation.message("$BoxForms", "formset", rhs) + return False + + definitions = evaluation.definitions + # Add the new elements to printforms and outputforms + for element in elements: + if element not in definitions.printforms: + definitions.printforms.append(element) + if element not in definitions.outputforms: + definitions.outputforms.append(element) + + definitions.boxforms = elements + return True + + def eval_assign_context( self: Builtin, lhs: BaseElement, @@ -661,19 +686,26 @@ def eval_assign_makeboxes( True if the assignment was successful. """ - # FIXME: the below is a big hack. - # Currently MakeBoxes boxing is implemented as a bunch of rules. - # See mathics.core.builtin contribute(). - # I think we want to change this so it works like normal SetDelayed - # That is: - # MakeBoxes[CubeRoot, StandardForm] := RadicalBox[3, StandardForm] - # rather than: - # MakeBoxes[CubeRoot, StandardForm] -> RadicalBox[3, StandardForm] + if not lhs.has_form("MakeBoxes", 2): + evaluation.message("MakeBoxes", "argrx", Integer(len(lhs.elements))) + raise AssignmentException(lhs, None) + target, form = lhs.elements + # Check second argument + makeboxes_rule = Rule(lhs, rhs, system=False) + tags = [] if tags is None else tags + if upset: + tags = tags + [target.get_lookup_name()] + else: + if not tags: + tags = ["System`MakeBoxes"] + definitions = evaluation.definitions - definitions.add_rule("System`MakeBoxes", makeboxes_rule, "downvalues") - # makeboxes_defs = evaluation.definitions.builtin["System`MakeBoxes"] - # makeboxes_defs.add_rule(makeboxes_rule) + for tag in tags: + if is_protected(tag, definitions): + evaluation.message(self.get_name(), "wrsym", Symbol(tag)) + return False + definitions.add_format(tag, makeboxes_rule, "_MakeBoxes") return True @@ -1556,6 +1588,7 @@ def get_lookup_name(expr): EVAL_ASSIGN_SPECIAL_SYMBOLS = { + "System`$BoxForms": eval_assign_boxforms, "System`$Context": eval_assign_context, "System`$ContextPath": eval_assign_context_path, "System`$HistoryLength": eval_assign_line_number_and_history_length, diff --git a/mathics/eval/drawing/colors.py b/mathics/eval/drawing/colors.py index 3c6f6f010..88ca209ef 100644 --- a/mathics/eval/drawing/colors.py +++ b/mathics/eval/drawing/colors.py @@ -181,9 +181,11 @@ def gradient_palette( ] -def palette_color_directive(palette, i): +def palette_color_directive(palette, i, opacity=None): """returns a directive in a form suitable for GraphicsGenerator.add_directives""" """ for setting the color of an entire entity such as a line or surface """ rgb = palette[i % len(palette)] rgb = [c / 255.0 for c in rgb] + if opacity is not None: + rgb.append(opacity) return [SymbolRGBColor, *rgb] diff --git a/mathics/eval/drawing/plot.py b/mathics/eval/drawing/plot.py index 6e15244f0..71af3403d 100644 --- a/mathics/eval/drawing/plot.py +++ b/mathics/eval/drawing/plot.py @@ -48,8 +48,8 @@ ) ListPlotType = Enum("ListPlotType", ListPlotNames) -RealPoint6 = Real(0.6) -RealPoint2 = Real(0.2) +SixTenths = Real(0.6) +TwoTenths = Real(0.2) try: @@ -393,7 +393,7 @@ def eval_ListPlot( graphics = [] for index, plot_group in enumerate(plot_groups): - graphics.append(Expression(SymbolHue, Real(hue), RealPoint6, RealPoint6)) + graphics.append(Expression(SymbolHue, Real(hue), SixTenths, SixTenths)) for segment in plot_group: if not is_joined_plot and list_plot_type == ListPlotType.ListStepPlot: line_segments = [ @@ -410,7 +410,7 @@ def eval_ListPlot( if filling is not None: graphics.append( Expression( - SymbolHue, Real(hue), RealPoint6, RealPoint6, RealPoint2 + SymbolHue, Real(hue), SixTenths, SixTenths, TwoTenths ) ) fill_area = list(segment) @@ -644,7 +644,7 @@ def find_excl(excl): if exclusions == SymbolNone: # Join all the Lines points = [[(xx, yy) for line in points for xx, yy in line]] - graphics.append(Expression(SymbolHue, Real(hue), RealPoint6, RealPoint6)) + graphics.append(Expression(SymbolHue, Real(hue), SixTenths, SixTenths)) graphics.append(Expression(SymbolLine, from_python(points))) for line in points: @@ -690,7 +690,7 @@ def find_excl(excl): if mesh != "None": for hue, points in zip(function_hues, mesh_points): - graphics.append(Expression(SymbolHue, Real(hue), RealPoint6, RealPoint6)) + graphics.append(Expression(SymbolHue, Real(hue), SixTenths, SixTenths)) mesh_points = [to_mathics_list(xx, yy) for xx, yy in points] graphics.append(Expression(SymbolPoint, ListExpression(*mesh_points))) diff --git a/mathics/eval/drawing/plot3d.py b/mathics/eval/drawing/plot3d.py index c603902bc..9e7596cfe 100644 --- a/mathics/eval/drawing/plot3d.py +++ b/mathics/eval/drawing/plot3d.py @@ -515,6 +515,13 @@ def eval_ContourPlot( return None +def eval_ContourPlot3D( + plot_options, + evaluation: Evaluation, +): + return None + + def eval_ParametricPlot3D( plot_options, evaluation: Evaluation, diff --git a/mathics/eval/drawing/plot3d_vectorized.py b/mathics/eval/drawing/plot3d_vectorized.py index 0074eb7dd..c89ca439a 100644 --- a/mathics/eval/drawing/plot3d_vectorized.py +++ b/mathics/eval/drawing/plot3d_vectorized.py @@ -14,6 +14,7 @@ from mathics.core.systemsymbols import ( SymbolAbsoluteThickness, SymbolEqual, + SymbolFull, SymbolNone, SymbolRGBColor, SymbolSubtract, @@ -222,6 +223,33 @@ def emit(graphics, i, xyzs, _, quads): return make_surfaces(plot_options, evaluation, dim=2, is_complex=False, emit=emit) +def equations_to_contours(plot_options): + """ + For contour plots, convert functions of the form f==g to f-g, + and adjust contours to [0] and disable background + """ + for i, f in enumerate(plot_options.functions): + if hasattr(f, "head") and f.head == SymbolEqual: + f = Expression(SymbolSubtract, *f.elements[0:2]) + plot_options.functions[i] = f + plot_options.contours = [0] + plot_options.background = False + + +def choose_contour_levels(plot_options, vmin, vmax, default): + levels = plot_options.contours + if isinstance(levels, str): + # TODO: need to pick "nice" number so levels have few digits + # an odd number ensures there is a contour at 0 if range is balanced + levels = default + if isinstance(levels, int): + # computed contour levels have equal distance between them, + # and half that between first/last contours and vmin/vmax + dv = (vmax - vmin) / levels + levels = vmin + np.arange(levels) * dv + dv / 2 + return levels + + @Timer("eval_ContourPlot") def eval_ContourPlot( plot_options, @@ -230,21 +258,14 @@ def eval_ContourPlot( import skimage.measure # whether to show a background similar to density plot, except quantized - background = len(plot_options.functions) == 1 - contour_levels = plot_options.contours + plot_options.background = len(plot_options.functions) == 1 - # convert fs of the form a==b to a-b, inplicit contour level 0 - plot_options.functions = list(plot_options.functions) # so we can modify it - for i, f in enumerate(plot_options.functions): - if hasattr(f, "head") and f.head == SymbolEqual: - f = Expression(SymbolSubtract, *f.elements[0:2]) - plot_options.functions[i] = f - contour_levels = [0] - background = False + # adjust functions, contours, and background for functions of the form f==g + equations_to_contours(plot_options) def emit(graphics, i, xyzs, _, quads): # set line color - if background: + if plot_options.background: # showing a background, so just black lines color_directive = [SymbolRGBColor, 0, 0, 0] else: @@ -259,17 +280,8 @@ def emit(graphics, i, xyzs, _, quads): zs = xyzs[:, 2] # this is a linear list matching with quads # process contour_levels - levels = contour_levels zmin, zmax = np.min(zs), np.max(zs) - if isinstance(levels, str): - # TODO: need to pick "nice" number so levels have few digits - # an odd number ensures there is a contour at 0 if range is balanced - levels = 9 - if isinstance(levels, int): - # computed contour levels have equal distance between them, - # and half that between first/last contours and zmin/zmax - dz = (zmax - zmin) / levels - levels = zmin + np.arange(levels) * dz + dz / 2 + levels = choose_contour_levels(plot_options, zmin, zmax, default=9) # one contour line per contour level for level in levels: @@ -285,7 +297,7 @@ def emit(graphics, i, xyzs, _, quads): graphics.add_linexyzs(segment) # background is solid colors between contour lines - if background: + if plot_options.background: with Timer("contour background"): # use masks and fancy indexing to assign (lo+hi)/2 to all zs between lo and hi zs[zs <= levels[0]] = zmin @@ -301,6 +313,81 @@ def emit(graphics, i, xyzs, _, quads): return make_surfaces(plot_options, evaluation, dim=2, is_complex=False, emit=emit) +@Timer("eval_ContourPlot3D") +def eval_ContourPlot3D( + plot_options, + evaluation: Evaluation, +): + import skimage.measure + + # adjust functions, contours, and background for functions of the form f==g + equations_to_contours(plot_options) + + # pull out options + _, xmin, xmax = plot_options.ranges[0] + _, ymin, ymax = plot_options.ranges[1] + _, zmin, zmax = plot_options.ranges[2] + nx, ny, nz = plot_options.plot_points + names = [strip_context(str(r[0])) for r in plot_options.ranges] + + # compile the functions + with Timer("compile"): + compiled_functions = compile_exprs(evaluation, plot_options.functions, names) + + # construct (nx,ny,nz) grid + xs = np.linspace(xmin, xmax, nx) + ys = np.linspace(ymin, ymax, ny) + zs = np.linspace(zmin, zmax, nz) + xs, ys, zs = np.meshgrid(xs, ys, zs) + + # just one function + function = compiled_functions[0] + + # compute function values fs over the grid + with Timer("compute fs"): + fs = function(**{n: v for n, v in zip(names, [xs, ys, zs])}) + + # process contour_levels + fmin, fmax = np.min(fs), np.max(fs) + levels = choose_contour_levels(plot_options, fmin, fmax, default=7) + + # find contour for each level and emit it + graphics = GraphicsGenerator(dim=3) + for i, level in enumerate(levels): + color_directive = palette_color_directive(palette3, i) + graphics.add_directives(color_directive) + + # find contour for this level + with Timer("3d contours"): + verts, faces, normals, values = skimage.measure.marching_cubes(fs, level) + verts[:, (0, 1)] = verts[:, (1, 0)] # skimage bug? + + # marching_cubes gives back coordinates relative to grid unit, so rescale to x, y, z + offset = [xmin, ymin, zmin] + scale = [ + (xmax - xmin) / (nx - 1), + (ymax - ymin) / (ny - 1), + (zmax - zmin) / (nz - 1), + ] + verts = np.array(offset) + verts * np.array(scale) + + # WL is 1-based + faces += 1 + + # emit faces as GraphicsComplex + graphics.add_complex(verts, lines=None, polys=faces, colors=None) + + # emit mesh as GraphicsComplex + # TODO: this should share vertices with previous GraphicsComplex + if plot_options.mesh is SymbolFull: + # TODO: each segment emitted twice - is there reasonable way to fix? + lines = np.array([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]]) + graphics.add_directives([SymbolRGBColor, 0, 0, 0]) + graphics.add_complex(verts, lines=lines, polys=None, colors=None) + + return graphics + + @Timer("complex colors") def complex_colors(zs, s=None): # hue depends on phase diff --git a/mathics/eval/lists.py b/mathics/eval/lists.py index 130d9ffb1..f20173ed6 100644 --- a/mathics/eval/lists.py +++ b/mathics/eval/lists.py @@ -1,4 +1,3 @@ -from mathics.builtin.box.layout import RowBox from mathics.core.atoms import String from mathics.core.convert.expression import to_expression from mathics.core.exceptions import PartDepthError, PartRangeError @@ -61,6 +60,8 @@ def get_tuples(items): def list_boxes(items, f, evaluation, open=None, close=None): + from mathics.builtin.box.layout import RowBox + result = [ Expression(SymbolMakeBoxes, item, f).evaluate(evaluation) for item in items ] diff --git a/mathics/eval/strings.py b/mathics/eval/strings.py index 05f986637..55d4567b3 100644 --- a/mathics/eval/strings.py +++ b/mathics/eval/strings.py @@ -182,7 +182,8 @@ def eval_StringForm_MakeBoxes(strform, items, form, evaluation): # character: if not remaining: evaluation.message("StringForm", "sfq", strform) - raise ValueError + return strform.value + # part must be an index or an empty string. # If is an empty string, pick the next element: if part == "": @@ -194,7 +195,8 @@ def eval_StringForm_MakeBoxes(strform, items, form, evaluation): Integer(num_items), strform, ) - return ValueError + return strform.value + result.append(items[curr_indx]) curr_indx += 1 quote_open = False @@ -206,14 +208,16 @@ def eval_StringForm_MakeBoxes(strform, items, form, evaluation): evaluation.message( "StringForm", "sfr", Integer0, Integer(num_items), strform ) - raise + return strform.value + # indx must be greater than 0, and not greater than # the number of items if indx <= 0 or indx > len(items): evaluation.message( "StringForm", "sfr", Integer(indx), Integer(len(items)), strform ) - raise ValueError + return strform.value + result.append(items[indx - 1]) curr_indx = indx quote_open = False diff --git a/mathics/format/box/__init__.py b/mathics/format/box/__init__.py index ceaf952cf..48688dd37 100644 --- a/mathics/format/box/__init__.py +++ b/mathics/format/box/__init__.py @@ -6,7 +6,6 @@ from mathics.format.box.makeboxes import ( _boxed_string, eval_generic_makeboxes, - eval_makeboxes, eval_makeboxes_fullform, format_element, to_boxes, @@ -35,7 +34,6 @@ "eval_baseform", "eval_generic_makeboxes", "eval_infix", - "eval_makeboxes", "eval_makeboxes_fullform", "eval_mathmlform", "eval_postprefix", diff --git a/mathics/format/box/common.py b/mathics/format/box/common.py new file mode 100644 index 000000000..c2529af64 --- /dev/null +++ b/mathics/format/box/common.py @@ -0,0 +1,23 @@ +""" +Util functions for box processing. +""" + +from typing import Tuple + +from mathics.builtin.box.expression import BoxExpression +from mathics.builtin.options import filter_non_default_values, options_to_rules +from mathics.core.element import BaseElement + + +def elements_to_expressions( + self: BoxExpression, elements: Tuple[BaseElement], options: dict +) -> Tuple[BaseElement]: + """ + Return a tuple of Mathics3 normal atoms or expressions. + """ + opts = sorted(options_to_rules(options, filter_non_default_values(self))) + expr_elements = [ + elem.to_expression() if isinstance(elem, BoxExpression) else elem + for elem in elements + ] + return tuple(expr_elements + opts) diff --git a/mathics/format/box/formatvalues.py b/mathics/format/box/formatvalues.py index 0e34328a5..90210bcfd 100644 --- a/mathics/format/box/formatvalues.py +++ b/mathics/format/box/formatvalues.py @@ -64,7 +64,7 @@ def do_format_element( Applies formats associated to the expression and removes superfluous enclosing formats. """ - from mathics.core.definitions import OutputForms + from mathics.core.definitions import OUTPUT_FORMS head: BaseElement @@ -77,7 +77,7 @@ def do_format_element( # If the expression is enclosed by a Format # takes the form from the expression and # removes the format from the expression. - if head in OutputForms and len(elements) == 1 and isinstance(head, Symbol): + if head in OUTPUT_FORMS and len(elements) == 1 and isinstance(head, Symbol): expr = elements[0] if not form.sameQ(head): form = head @@ -145,7 +145,7 @@ def format_expr(expr): # If the expression is not atomic or of certain # specific cases, iterate over the elements. head = expr.get_head() - if head in OutputForms: + if head in OUTPUT_FORMS: # If the expression was of the form # Form[expr, opts] # then the format was not stripped. Then, diff --git a/mathics/format/box/graphics.py b/mathics/format/box/graphics.py new file mode 100644 index 000000000..ed17fe733 --- /dev/null +++ b/mathics/format/box/graphics.py @@ -0,0 +1,1001 @@ +# -*- coding: utf-8 -*- +""" +Boxing Symbols for 2D Graphics +""" +import logging +from math import ceil, floor, log10 +from typing import Any, List, Optional, Tuple + +from mathics.builtin.colors.color_directives import ColorError, RGBColor, _ColorObject +from mathics.builtin.drawing.graphics_internals import get_class +from mathics.builtin.graphics import ( + ELEMENT_HEADS, + GRAPHICS_SYMBOLS, + STYLE_AND_FORM_HEADS, + STYLE_HEADS, + AbsoluteThickness, + _Thickness, +) +from mathics.core.atoms import Integer, Real, String +from mathics.core.convert.expression import to_expression, to_mathics_list +from mathics.core.element import BoxElementMixin +from mathics.core.evaluation import Evaluation +from mathics.core.exceptions import BoxExpressionError +from mathics.core.expression import Expression +from mathics.core.formatter import lookup_method +from mathics.core.symbols import ( + Symbol, + SymbolFalse, + SymbolList, + SymbolNull, + SymbolTrue, + system_symbols_dict, +) +from mathics.core.systemsymbols import ( + SymbolAutomatic, + SymbolEdgeForm, + SymbolFaceForm, + SymbolGraphics, + SymbolInset, + SymbolStyle, + SymbolText, +) +from mathics.eval.nevaluator import eval_N +from mathics.format.box.makeboxes import apply_makeboxes_rules + +SymbolRegularPolygonBox = Symbol("RegularPolygonBox") + +ERROR_BACKGROUND_COLOR = RGBColor(components=[1, 0.3, 0.3, 0.25]) + + +class Style: + def __init__(self, graphics, edge=False, face=False): + self.styles = [] + self.options = {} + self.graphics = graphics + self.edge = edge + self.face = face + self.klass = graphics.style_class + + def append(self, item, allow_forms=True): + self.styles.append(_style(self.graphics, item)) + + def set_option(self, name, value): + self.options[name] = value + + def extend(self, style): + self.styles.extend(style.styles) + + def clone(self): + result = self.klass(self.graphics, edge=self.edge, face=self.face) + result.styles = self.styles[:] + result.options = self.options.copy() + return result + + def get_default_face_color(self): + return RGBColor(components=(0, 0, 0, 1)) + + def get_default_edge_color(self): + return RGBColor(components=(0, 0, 0, 1)) + + def get_style( + self, style_class, face_element=None, default_to_faces=True, consider_forms=True + ): + if face_element is not None: + default_to_faces = consider_forms = face_element + edge_style = face_style = None + if style_class == _ColorObject: + if default_to_faces: + face_style = self.get_default_face_color() + else: + edge_style = self.get_default_edge_color() + elif style_class == _Thickness: + if not default_to_faces: + edge_style = AbsoluteThickness(self.graphics, value=1.6) + for item in self.styles: + if isinstance(item, style_class): + if default_to_faces: + face_style = item + else: + edge_style = item + elif isinstance(item, Style): + if consider_forms: + if item.edge: + edge_style, _ = item.get_style( + style_class, default_to_faces=False, consider_forms=False + ) + elif item.face: + _, face_style = item.get_style( + style_class, default_to_faces=True, consider_forms=False + ) + return edge_style, face_style + + def get_option(self, name): + return self.options.get(name, None) + + def get_line_width(self, face_element=True) -> float: + if self.graphics.pixel_width is None: + return 0.0 + edge_style, _ = self.get_style( + _Thickness, default_to_faces=face_element, consider_forms=face_element + ) + if edge_style is None: + return 0.0 + return edge_style.get_thickness() / 2.0 + + +class _GraphicsElements: + style_class = Style + + def __init__(self, content, evaluation): + self.evaluation = evaluation + self.elements = [] + + builtins = evaluation.definitions.builtin + + def get_options(name): + builtin = builtins.get(name) + if builtin is None: + return None + return builtin.options + + def stylebox_style(style, specs): + new_style = style.clone() + for spec in _flatten(specs): + head = spec.get_head() + if head in STYLE_AND_FORM_HEADS: + new_style.append(spec) + elif head is Symbol("System`Rule") and len(spec.elements) == 2: + option, expr = spec.elements + if not isinstance(option, Symbol): + raise BoxExpressionError + + name = option.get_name() + create = STYLE_OPTIONS.get(name, None) + if create is None: + raise BoxExpressionError + + new_style.set_option(name, create(style.graphics, expr)) + else: + raise BoxExpressionError + return new_style + + failed = [] + + def convert(content, style): + if content.has_form("List", None): + items = content.elements + else: + items = [content] + style = style.clone() + for item in items: + if item is SymbolNull: + continue + head = item.get_head() + if head in STYLE_AND_FORM_HEADS: + try: + style.append(item) + except ColorError: + failed.append(head) + elif head is Symbol("System`StyleBox"): + if len(item.elements) < 1: + failed.append(item.head) + for element in convert( + item.elements[0], stylebox_style(style, item.elements[1:]) + ): + yield element + elif head.name[-3:] == "Box": # and head[:-3] in element_heads: + element_class = get_class(head) + if element_class is None: + failed.append(head) + continue + options = get_options(head.name[:-3]) + if options: + data, options = _data_and_options(item.elements, options) + new_item = Expression(head, *data) + try: + element = element_class(self, style, new_item, options) + except (BoxExpressionError, CoordinatesError): + failed.append(head) + continue + else: + try: + element = element_class(self, style, item) + except (BoxExpressionError, CoordinatesError): + failed.append(head) + continue + yield element + elif head is SymbolList: + for element in convert(item, style): + yield element + else: + failed.append(head) + continue + + # if failed: + # yield build_error_box2(style) + # raise BoxExpressionError(messages) + + self.elements = list(convert(content, self.style_class(self))) + if failed: + messages = "\n".join( + [f"{str(h)} is not a valid primitive or directive." for h in failed] + ) + self.tooltip_text = messages + self.background_color = ERROR_BACKGROUND_COLOR + logging.warning(messages) + + def create_style(self, expr): + style = self.style_class(self) + + def convert(expr): + if expr.has_form(("List", "Directive"), None): + for item in expr.elements: + convert(item) + else: + style.append(expr) + + convert(expr) + return style + + +class CoordinatesError(BoxExpressionError): + pass + + +class Coords: + def __init__(self, graphics, expr=None, pos=None, d=None): + self.graphics = graphics + self.p = pos + self.d = d + if expr is not None: + if expr.has_form("Offset", 1, 2): + self.d = coords(expr.elements[0]) + if len(expr.elements) > 1: + self.p = coords(expr.elements[1]) + else: + self.p = None + else: + self.p = coords(expr) + + def pos(self): + p = self.graphics.translate(self.p) + p = (cut(p[0]), cut(p[1])) + if self.d is not None: + d = self.graphics.translate_absolute(self.d) + return (p[0] + d[0], p[1] + d[1]) + return p + + def add(self, x, y): + p = (self.p[0] + x, self.p[1] + y) + return Coords(self.graphics, pos=p, d=self.d) + + +class GraphicsElements(_GraphicsElements): + coords = Coords + + def __init__(self, content, evaluation, neg_y=False): + super(GraphicsElements, self).__init__(content, evaluation) + self.neg_y = neg_y + self.xmin = self.ymin = self.pixel_width = None + self.pixel_height = self.extent_width = self.extent_height = None + self.view_width = None + self.content = content + + def translate(self, coords): + if self.pixel_width is not None: + w = self.extent_width if self.extent_width > 0 else 1 + h = self.extent_height if self.extent_height > 0 else 1 + result = [ + (coords[0] - self.xmin) * self.pixel_width / w, + (coords[1] - self.ymin) * self.pixel_height / h, + ] + if self.neg_y: + result[1] = self.pixel_height - result[1] + return tuple(result) + else: + return (coords[0], coords[1]) + + def translate_absolute(self, d): + if self.pixel_width is None: + return (0, 0) + else: + lw = 96.0 / 72 + return (d[0] * lw, (-1 if self.neg_y else 1) * d[1] * lw) + + def translate_relative(self, x): + if self.pixel_width is None: + return 0 + else: + return x * self.pixel_width + + def extent(self, completely_visible_only=False): + if completely_visible_only: + ext = total_extent( + [ + element.extent() + for element in self.elements + if element.is_completely_visible + ] + ) + else: + ext = total_extent([element.extent() for element in self.elements]) + xmin, xmax, ymin, ymax = ext + if xmin == xmax: + if xmin is None: + return 0, 0, 0, 0 + xmin = 0 + xmax *= 2 + if ymin == ymax: + if ymin is None: + return 0, 0, 0, 0 + ymin = 0 + ymax *= 2 + return xmin, xmax, ymin, ymax + + def set_size( + self, xmin, ymin, extent_width, extent_height, pixel_width, pixel_height + ): + self.xmin, self.ymin = xmin, ymin + self.extent_width, self.extent_height = extent_width, extent_height + self.pixel_width, self.pixel_height = pixel_width, pixel_height + + +def _data_and_options(elements, defined_options): + data = [] + options = defined_options.copy() + for element in elements: + if element.get_head_name() == "System`Rule": + if len(element.elements) != 2: + raise BoxExpressionError + name, value = element.elements + name_head = name.get_head_name() + if name_head == "System`Symbol": + py_name = name.get_name() + elif name_head == "System`String": + py_name = "System`" + name.get_string_value() + else: # unsupported name type + raise BoxExpressionError + options[py_name] = value + else: + data.append(element) + return data, options + + +def _extract_graphics(graphics, format, evaluation): + graphics_box = apply_makeboxes_rules(graphics, evaluation) + # builtin = GraphicsBox(expression=False) + elements, calc_dimensions = prepare_elements( + graphics_box, graphics_box.content, {"_evaluation": evaluation}, neg_y=True + ) + xmin, xmax, ymin, ymax, _, _, _, _ = calc_dimensions() + + # xmin, xmax have always been moved to 0 here. the untransformed + # and unscaled bounds are found in elements.xmin, elements.ymin, + # elements.extent_width, elements.extent_height. + + # now compute the position of origin (0, 0) in the transformed + # coordinate space. + + ex = elements.extent_width + ey = elements.extent_height + + sx = (xmax - xmin) / ex + sy = (ymax - ymin) / ey + + ox = -elements.xmin * sx + xmin + oy = -elements.ymin * sy + ymin + + # generate code for svg or asy. + + if format in ("asy", "svg"): + format_fn = lookup_method(elements, format) + code = format_fn(elements) + else: + raise NotImplementedError + + return xmin, xmax, ymin, ymax, ox, oy, ex, ey, code + + +def _flatten(elements): + for element in elements: + if element.get_head() is SymbolList: + flattened = element.flatten_with_respect_to_head(SymbolList) + if flattened.get_head() is SymbolList: + for x in flattened.elements: + yield x + else: + yield flattened + else: + yield element + + +def _style(graphics, item): + head = item.get_head() + if head in STYLE_HEADS: + klass = get_class(head) + style = klass.create_as_style(klass, graphics, item) + elif head in (SymbolEdgeForm, SymbolFaceForm): + style = graphics.style_class( + graphics, edge=head is SymbolEdgeForm, face=head is SymbolFaceForm + ) + if len(item.elements) > 1: + raise BoxExpressionError + if item.elements: + if item.elements[0].has_form("List", None): + for dir in item.elements[0].elements: + style.append(dir, allow_forms=False) + else: + style.append(item.elements[0], allow_forms=False) + else: + raise BoxExpressionError + return style + + +def coords(value): + if value.has_form("List", 2): + x, y = value.elements[0].round_to_float(), value.elements[1].round_to_float() + if x is None or y is None: + raise CoordinatesError + return (x, y) + raise CoordinatesError + + +def cut(value): + "Cut values in graphics primitives (not displayed otherwise in SVG)" + border = 10**8 + if value < -border: + value = -border + elif value > border: + value = border + return value + + +# FIXME: this doesn't always properly align with overlaid SVG plots +def axis_ticks(xmin: float, xmax: float) -> Tuple[List[float], List[float], int]: + """ + Compute the positions of the axis ticks + """ + + def round_to_zero(value): + if value == 0: + return 0 + elif value < 0: + return ceil(value) + else: + return floor(value) + + def round_step(value): + if not value: + return 1, 1 + sub_steps = 5 + try: + shift = 10.0 ** floor(log10(value)) + except ValueError: + return 1, 1 + value = value / shift + if value < 1.5: + value = 1 + elif value < 3: + value = 2 + sub_steps = 4 + elif value < 8: + value = 5 + else: + value = 10 + return value * shift, sub_steps + + step_x, sub_x = round_step((xmax - xmin) / 5.0) + step_x_small = step_x / sub_x + steps_x = int(floor((xmax - xmin) / step_x)) + steps_x_small = int(floor((xmax - xmin) / step_x_small)) + + start_k_x = int(ceil(xmin / step_x)) + start_k_x_small = int(ceil(xmin / step_x_small)) + + if xmin <= 0 <= xmax: + origin_k_x = 0 + else: + origin_k_x = start_k_x + origin_x = origin_k_x * step_x + + ticks = [] + ticks_small = [] + for k in range(start_k_x, start_k_x + steps_x + 1): + if k != origin_k_x: + x = k * step_x + if x > xmax: + break + ticks.append(x) + for k in range(start_k_x_small, start_k_x_small + steps_x_small + 1): + if k % sub_x != 0: + x = k * step_x_small + if x > xmax: + break + ticks_small.append(x) + + return ticks, ticks_small, origin_x + + +def create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax) -> tuple: + # Note that Asymptote has special commands for drawing axes, like "xaxis" + # "yaxis", "xtick" "labelx", "labely". Extend our language + # here and use those in render-like routines. + from mathics.builtin.box.graphics import InsetBox, LineBox + + use_log_for_y_axis = graphics_options.get("System`LogPlot", SymbolFalse).to_python() + axes_option = graphics_options.get("System`Axes") + + if axes_option is SymbolTrue: + axes = (True, True) + elif axes_option.has_form("List", 2): + axes = ( + axes_option.elements[0] is SymbolTrue, + axes_option.elements[1] is SymbolTrue, + ) + else: + axes = (False, False) + + # The Style option pushes its setting down into graphics components + # like ticks, axes, and labels. + ticks_style_option = graphics_options.get("System`TicksStyle") + axes_style_option = graphics_options.get("System`AxesStyle") + label_style = graphics_options.get("System`LabelStyle") + + if ticks_style_option.has_form("List", 2): + ticks_style = ticks_style_option.elements + else: + ticks_style = [ticks_style_option] * 2 + + if axes_style_option.has_form("List", 2): + axes_style = axes_style_option.elements + else: + axes_style = [axes_style_option] * 2 + + ticks_style = [elements.create_style(s) for s in ticks_style] + axes_style = [elements.create_style(s) for s in axes_style] + label_style = elements.create_style(label_style) + ticks_style[0].extend(axes_style[0]) + ticks_style[1].extend(axes_style[1]) + + def add_element(element): + element.is_completely_visible = True + elements.elements.append(element) + + # Units seem to be in point size units + + ticks_x, ticks_x_small, origin_x = axis_ticks(xmin, xmax) + ticks_y, ticks_y_small, origin_y = axis_ticks(ymin, ymax) + + axes_extra = 6 + + tick_small_size = 3 + tick_large_size = 5 + + tick_label_d = 2 + + ticks_x_int = all(floor(x) == x for x in ticks_x) + ticks_y_int = all(floor(x) == x for x in ticks_y) + + for ( + index, + ( + min, + max, + p_self0, + p_other0, + p_origin, + ticks, + ticks_small, + ticks_int, + is_logscale, + ), + ) in enumerate( + [ + ( + xmin, + xmax, + lambda y: (0, y), + lambda x: (x, 0), + lambda x: (x, origin_y), + ticks_x, + ticks_x_small, + ticks_x_int, + False, + ), + ( + ymin, + ymax, + lambda x: (x, 0), + lambda y: (0, y), + lambda y: (origin_x, y), + ticks_y, + ticks_y_small, + ticks_y_int, + use_log_for_y_axis, + ), + ] + ): + # Where should the placement of tick mark labels go? + if index == 0: + # x labels go under tick marks + alignment = "bottom" + elif index == 1: + # y labels go to the left of tick marks + alignment = "left" + else: + alignment = None + + if axes[index]: + add_element( + LineBox( + elements, + axes_style[index], + lines=[ + [ + Coords( + elements, pos=p_origin(min), d=p_other0(-axes_extra) + ), + Coords(elements, pos=p_origin(max), d=p_other0(axes_extra)), + ] + ], + ) + ) + ticks_lines = [] + + tick_label_style = ticks_style[index].clone() + tick_label_style.extend(label_style) + + for x in ticks: + ticks_lines.append( + [ + Coords(elements, pos=p_origin(x)), + Coords(elements, pos=p_origin(x), d=p_self0(tick_large_size)), + ] + ) + + # FIXME: for log plots we labels should appear + # as 10^x rather than say 1000000. + tick_value = 10**x if is_logscale else x + if ticks_int: + content = String(str(int(tick_value))) + elif tick_value == floor(x): + content = String("%.1f" % tick_value) # e.g. 1.0 (instead of 1.) + else: + content = String("%g" % tick_value) # fix e.g. 0.6000000000000001 + + add_element( + InsetBox( + elements, + tick_label_style, + content=content, + pos=Coords(elements, pos=p_origin(x), d=p_self0(-tick_label_d)), + opos=p_self0(1), + opacity=1.0, + alignment=alignment, + ) + ) + for x in ticks_small: + pos = p_origin(x) + ticks_lines.append( + [ + Coords(elements, pos=pos), + Coords(elements, pos=pos, d=p_self0(tick_small_size)), + ] + ) + add_element(LineBox(elements, axes_style[0], lines=ticks_lines)) + return axes + + # Old code? + # if axes[1]: + # add_element(LineBox(elements, axes_style[1], lines=[[Coords(elements, pos=(origin_x,ymin), d=(0,-axes_extra)), + # Coords(elements, pos=(origin_x,ymax), d=(0,axes_extra))]])) + # ticks = [] + # tick_label_style = ticks_style[1].clone() + # tick_label_style.extend(label_style) + # for k in range(start_k_y, start_k_y+steps_y+1): + # if k != origin_k_y: + # y = k * step_y + # if y > ymax: + # break + # pos = (origin_x,y) + # ticks.append([Coords(elements, pos=pos), + # Coords(elements, pos=pos, d=(tick_large_size,0))]) + # add_element(InsetBox(elements, tick_label_style, content=Real(y), pos=Coords(elements, pos=pos, + # d=(-tick_label_d,0)), opos=(1,0))) + # for k in range(start_k_y_small, start_k_y_small+steps_y_small+1): + # if k % sub_y != 0: + # y = k * step_y_small + # if y > ymax: + # break + # pos = (origin_x,y) + # ticks.append([Coords(elements, pos=pos), + # Coords(elements, pos=pos, d=(tick_small_size,0))]) + # add_element(LineBox(elements, axes_style[1], lines=ticks)) + + +def get_image_size( + options: dict, graphics_options: dict, max_width +) -> Tuple[Optional[int], Optional[int], Any, Any]: + base_width: Optional[int] + base_height: Optional[int] + image_size_multipliers: Optional[Tuple[float, float]] + + inside_row = options.pop("inside_row", False) + inside_list = options.pop("inside_list", False) + image_size_multipliers = options.pop("image_size_multipliers", None) + + aspect_ratio = graphics_options["System`AspectRatio"] + + if image_size_multipliers is None: + image_size_multipliers = (0.5, 0.25) + + if aspect_ratio is SymbolAutomatic: + aspect = None + else: + aspect = aspect_ratio.round_to_float() + + image_size = graphics_options["System`ImageSize"] + if isinstance(image_size, Integer): + base_width = image_size.get_int_value() + base_height = None # will be computed later in calc_dimensions + elif image_size.has_form("System`List", 2): + base_width, base_height = ( + [x.round_to_float() for x in image_size.elements] + [0, 0] + )[:2] + if base_width is None or base_height is None: + raise BoxExpressionError + aspect = base_height / base_width + else: + image_size = image_size.get_name() + base_width, base_height = { + "System`Automatic": (400, 350), + "System`Tiny": (100, 100), + "System`Small": (200, 200), + "System`Medium": (400, 350), + "System`Large": (600, 500), + }.get(image_size, (None, None)) + if base_width is None: + raise BoxExpressionError + if max_width is not None and base_width > max_width: + base_width = max_width + + if inside_row: + multi = image_size_multipliers[1] + elif inside_list: + multi = image_size_multipliers[0] + else: + multi = 1 + + return base_width, base_height, multi, aspect + + +def prepare_elements(self, content, options, neg_y=False, max_width=None): + if not content: + raise BoxExpressionError + graphics_options = self.box_options.copy() + graphics_options.update(options) + background = graphics_options["System`Background"] + if isinstance(background, Symbol) and background.get_name() == "System`Automatic": + self.background_color = None + else: + try: + self.background_color = _ColorObject.create(background) + except ColorError: + pass + + base_width, base_height, size_multiplier, size_aspect = get_image_size( + options, graphics_options, max_width + ) + + plot_range = graphics_options["System`PlotRange"].to_python() + if plot_range == "System`Automatic": + plot_range = ["System`Automatic", "System`Automatic"] + + if not isinstance(plot_range, list) or len(plot_range) != 2: + raise BoxExpressionError + + evaluation = options.get("_evaluation", None) + if evaluation is None: + evaluation = self.evaluation + elements = GraphicsElements(content, evaluation, neg_y) + if hasattr(elements, "background_color"): + self.background_color = elements.background_color + if hasattr(elements, "tooltip_text"): + self.tooltip_text = elements.tooltip_text + + axes = [] # to be filled further down + + def calc_dimensions(final_pass=True): + """ + calc_dimensions gets called twice: In the first run + (final_pass = False, called inside prepare_elements), the extent + of all user-defined graphics is determined. + Axes are created accordingly. + In the second run (final_pass = True, called from outside), + the dimensions of these axes are taken into account as well. + This is also important to size absolutely sized objects correctly + (e.g. values using AbsoluteThickness). + """ + + # always need to compute extent if size aspect is automatic + if "System`Automatic" in plot_range or size_aspect is None: + xmin, xmax, ymin, ymax = elements.extent() + else: + xmin = xmax = ymin = ymax = None + + if ( + final_pass + and any(x for x in axes) + and plot_range != ["System`Automatic", "System`Automatic"] + ): + # Take into account the dimensions of axes and axes labels + # (they should be displayed completely even when a specific + # PlotRange is given). + exmin, exmax, eymin, eymax = elements.extent(completely_visible_only=True) + else: + exmin = exmax = eymin = eymax = None + + def get_range(min, max): + if max < min: + min, max = max, min + elif min == max: + if min < 0: + min, max = 2 * min, 0 + elif min > 0: + min, max = 0, 2 * min + else: + min, max = -1, 1 + return min, max + + try: + if plot_range[0] == "System`Automatic": + if xmin is None and xmax is None: + xmin = 0 + xmax = 1 + elif xmin == xmax: + xmin -= 1 + xmax += 1 + elif isinstance(plot_range[0], list) and len(plot_range[0]) == 2: + xmin, xmax = list(map(float, plot_range[0])) + xmin, xmax = get_range(xmin, xmax) + xmin = elements.translate((xmin, 0))[0] + xmax = elements.translate((xmax, 0))[0] + if exmin is not None and exmin < xmin: + xmin = exmin + if exmax is not None and exmax > xmax: + xmax = exmax + else: + raise BoxExpressionError + + if plot_range[1] == "System`Automatic": + if ymin is None and ymax is None: + ymin = 0 + ymax = 1 + elif ymin == ymax: + ymin -= 1 + ymax += 1 + elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2: + ymin, ymax = list(map(float, plot_range[1])) + ymin, ymax = get_range(ymin, ymax) + ymin = elements.translate((0, ymin))[1] + ymax = elements.translate((0, ymax))[1] + if ymin > ymax: + ymin, ymax = ymax, ymin + if eymin is not None and eymin < ymin: + ymin = eymin + if eymax is not None and eymax > ymax: + ymax = eymax + else: + raise BoxExpressionError + except (ValueError, TypeError): + raise BoxExpressionError + + w = 0 if (xmin is None or xmax is None) else xmax - xmin + h = 0 if (ymin is None or ymax is None) else ymax - ymin + + if size_aspect is None: + aspect = h / w + else: + aspect = size_aspect + + height = base_height + if height is None: + height = base_width * aspect + width = height / aspect + if width > base_width: + width = base_width + height = width * aspect + + width *= size_multiplier + height *= size_multiplier + + return xmin, xmax, ymin, ymax, w, h, width, height + + xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions(final_pass=False) + + elements.set_size(xmin, ymin, w, h, width, height) + + xmin -= w * 0.02 + xmax += w * 0.02 + ymin -= h * 0.02 + ymax += h * 0.02 + + axes.extend(create_axes(self, elements, graphics_options, xmin, xmax, ymin, ymax)) + + return elements, calc_dimensions + + +def primitives_to_boxes( + content: Expression, evaluation: Evaluation, box_suffix: str = "Box" +) -> Expression | BoxElementMixin: + """ + Convert a primitive into the corresponding + box expression. + If `content` is a ListExpression containing primitives, return a + ListExpression of the corresponding BoxElements. + """ + + head = content.get_head() + + if head is SymbolList: + return to_mathics_list( + *content.elements, + elements_conversion_fn=lambda item: primitives_to_boxes( + item, evaluation, box_suffix + ), + ) + elif head is SymbolStyle: + return to_expression( + "StyleBox", + *[ + primitives_to_boxes(item, evaluation, box_suffix) + for item in content.elements + ], + ) + + if head in ELEMENT_HEADS: + if head is SymbolText: + head = SymbolInset + atoms = content.get_atoms(include_heads=False) + if any( + not isinstance(atom, (Integer, Real)) and atom not in GRAPHICS_SYMBOLS + for atom in atoms + ): + if head is SymbolInset: + inset = content.elements[0] + if inset.get_head() is SymbolGraphics: + # TODO: consider to use format_element instead of + # apply_makeboxes_rules + inset = apply_makeboxes_rules(inset.elements[0], evaluation) + n_elements = [inset] + [ + eval_N(element, evaluation) for element in content.elements[1:] + ] + else: + n_elements = [ + eval_N(element, evaluation) for element in content.elements + ] + else: + n_elements = content.elements + return Expression(Symbol(head.name + box_suffix), *n_elements) + return content + + +def total_extent(extents): + xmin = xmax = ymin = ymax = None + for extent in extents: + for x, y in extent: + if xmin is None or x < xmin: + xmin = x + if xmax is None or x > xmax: + xmax = x + if ymin is None or y < ymin: + ymin = y + if ymax is None or y > ymax: + ymax = y + return xmin, xmax, ymin, ymax + + +STYLE_OPTIONS = system_symbols_dict( + {"FontColor": _style, "ImageSizeMultipliers": (lambda *x: x[1])} +) diff --git a/mathics/format/box/graphics3d.py b/mathics/format/box/graphics3d.py new file mode 100644 index 000000000..e40523680 --- /dev/null +++ b/mathics/format/box/graphics3d.py @@ -0,0 +1,423 @@ +# -*- coding: utf-8 -*- +""" +Boxing Symbols for 3D Graphics +""" + +import logging +import numbers +from typing import Tuple + +from mathics.builtin.colors.color_directives import ColorError, _ColorObject +from mathics.builtin.drawing.graphics3d import Coords3D, Graphics3DElements +from mathics.builtin.drawing.graphics_internals import get_class +from mathics.core.element import BaseElement +from mathics.core.exceptions import BoxExpressionError +from mathics.core.symbols import Symbol, SymbolTrue +from mathics.eval.nevaluator import eval_N +from mathics.format.box.graphics import axis_ticks, get_image_size + + +def create_axes( + self, elements, graphics_options, xmin, xmax, ymin, ymax, zmin, zmax, boxscale +): + axes = graphics_options.get("System`Axes") + if axes is SymbolTrue: + axes = (True, True, True) + elif axes.has_form("List", 3): + axes = (element is SymbolTrue for element in axes.elements) + else: + axes = (False, False, False) + ticks_style = graphics_options.get("System`TicksStyle") + axes_style = graphics_options.get("System`AxesStyle") + label_style = graphics_options.get("System`LabelStyle") + + # FIXME: Doesn't handle GrayScale + if ticks_style.has_form("List", 1, 2, 3): + ticks_style = ticks_style.elements + elif ticks_style.has_form("RGBColor", None): + ticks_style = [ticks_style] * 3 + else: + ticks_style = [] + + if axes_style.has_form("List", 1, 2, 3): + axes_style = axes_style.elements + else: + axes_style = [axes_style] * 3 + + # FIXME: Not quite right. We only handle color + ticks_style = [ + elements.create_style(s).get_style(_ColorObject, face_element=False)[0] + for s in ticks_style + ] + + axes_style = [elements.create_style(s) for s in axes_style] + label_style = elements.create_style(label_style) + + # For later + # ticks_style[0].extend(axes_style[0]) + # ticks_style[1].extend(axes_style[1]) + # ticks_style[2].extend(axes_style[2]) + + ticks = [ + axis_ticks(xmin, xmax), + axis_ticks(ymin, ymax), + axis_ticks(zmin, zmax), + ] + + # Add zero if required, since axis_ticks does not + if xmin <= 0 <= xmax: + ticks[0][0].append(0.0) + if ymin <= 0 <= ymax: + ticks[1][0].append(0.0) + if zmin <= 0 <= zmax: + ticks[2][0].append(0.0) + + # Convert ticks to nice strings e.g 0.100000000000002 -> '0.1' and + # scale ticks appropriately + ticks = [ + [ + [boxscale[i] * x for x in t[0]], + [boxscale[i] * x for x in t[1]], + ["%g" % x for x in t[0]], + ] + for i, t in enumerate(ticks) + ] + + return axes, ticks, ticks_style + + +def expr_to_list_of_3d_points(expr: BaseElement) -> Tuple[Coords3D, ...]: + points = expr.to_python() + if not all(isinstance(point, (tuple, list)) for point in points): + points = [points] + if not all( + len(point) == 3 and all(isinstance(p, numbers.Real) for p in point) + for point in points + ): + raise BoxExpressionError + return tuple(Coords3D(pos=point) for point in points) + + +def get_boundbox_lines(self, xmin, xmax, ymin, ymax, zmin, zmax): + return [ + [(xmin, ymin, zmin), (xmax, ymin, zmin)], + [(xmin, ymax, zmin), (xmax, ymax, zmin)], + [(xmin, ymin, zmax), (xmax, ymin, zmax)], + [(xmin, ymax, zmax), (xmax, ymax, zmax)], + [(xmin, ymin, zmin), (xmin, ymax, zmin)], + [(xmax, ymin, zmin), (xmax, ymax, zmin)], + [(xmin, ymin, zmax), (xmin, ymax, zmax)], + [(xmax, ymin, zmax), (xmax, ymax, zmax)], + [(xmin, ymin, zmin), (xmin, ymin, zmax)], + [(xmax, ymin, zmin), (xmax, ymin, zmax)], + [(xmin, ymax, zmin), (xmin, ymax, zmax)], + [(xmax, ymax, zmin), (xmax, ymax, zmax)], + ] + + +def prepare_elements(self, content, options, max_width=None): + if not content: + raise BoxExpressionError + + graphics_options = self.box_options.copy() + graphics_options.update(options) + + background = graphics_options["System`Background"] + if isinstance(background, Symbol) and background.get_name() == "System`Automatic": + self.background_color = None + else: + try: + self.background_color = _ColorObject.create(background) + except ColorError: + logging.warning(f"{str(background)} is not a valid color spec.") + self.background_color = None + + evaluation = options.get("_evaluation", self.evaluation) + + base_width, base_height, size_multiplier, size_aspect = get_image_size( + options, graphics_options, max_width + ) + + # TODO: Handle ImageScaled[], and Scaled[] + lighting_option = graphics_options["System`Lighting"] + lighting = lighting_option.to_python() # can take symbols or strings + self.lighting = [] + + if lighting == "System`Automatic": + self.lighting = [ + {"type": "Ambient", "color": [0.3, 0.2, 0.4]}, + { + "type": "Directional", + "color": [0.8, 0.0, 0.0], + "position": [2, 0, 2], + }, + { + "type": "Directional", + "color": [0.0, 0.8, 0.0], + "position": [2, 2, 2], + }, + { + "type": "Directional", + "color": [0.0, 0.0, 0.8], + "position": [0, 2, 2], + }, + ] + elif lighting == "Neutral": # Lighting->"Neutral" + self.lighting = [ + {"type": "Ambient", "color": [0.3, 0.3, 0.3]}, + { + "type": "Directional", + "color": [0.3, 0.3, 0.3], + "position": [2, 0, 2], + }, + { + "type": "Directional", + "color": [0.3, 0.3, 0.3], + "position": [2, 2, 2], + }, + { + "type": "Directional", + "color": [0.3, 0.3, 0.3], + "position": [0, 2, 2], + }, + ] + elif lighting == "System`None": + pass + + elif isinstance(lighting, list) and all( + isinstance(light, list) for light in lighting + ): + for light in lighting: + if light[0] in ['"Ambient"', '"Directional"', '"Point"', '"Spot"']: + try: + head = light[1].get_head_name() + except AttributeError: + break + color = get_class(head)(light[1]) + if light[0] == '"Ambient"': + self.lighting.append({"type": "Ambient", "color": color.to_rgba()}) + elif light[0] == '"Directional"': + position = [0, 0, 0] + if isinstance(light[2], list): + if len(light[2]) == 3: + position = light[2] + if len(light[2]) == 2 and all( # noqa + isinstance(p, list) and len(p) == 3 for p in light[2] + ): + position = [ + light[2][0][i] - light[2][1][i] for i in range(3) + ] + self.lighting.append( + { + "type": "Directional", + "color": color.to_rgba(), + "position": position, + } + ) + elif light[0] == '"Point"': + position = [0, 0, 0] + if isinstance(light[2], list) and len(light[2]) == 3: + position = light[2] + self.lighting.append( + { + "type": "Point", + "color": color.to_rgba(), + "position": position, + } + ) + elif light[0] == '"Spot"': + position = [0, 0, 1] + target = [0, 0, 0] + if isinstance(light[2], list): + if len(light[2]) == 2: + if ( + isinstance(light[2][0], list) + and len(light[2][0]) == 3 # noqa + ): + position = light[2][0] + if ( + isinstance(light[2][1], list) + and len(light[2][1]) == 3 # noqa + ): + target = light[2][1] + if len(light[2]) == 3: + position = light[2] + angle = light[3] + self.lighting.append( + { + "type": "Spot", + "color": color.to_rgba(), + "position": position, + "target": target, + "angle": angle, + } + ) + + else: + evaluation.message("Graphics3D", "invlight", lighting_option) + + # ViewPoint Option + viewpoint_option = graphics_options["System`ViewPoint"] + viewpoint = eval_N(viewpoint_option, evaluation).to_python() + + if isinstance(viewpoint, list) and len(viewpoint) == 3: + if all(isinstance(x, numbers.Real) for x in viewpoint): + # TODO Infinite coordinates e.g. {0, 0, Infinity} + pass + else: + try: + viewpoint = { + "Above": [0, 0, 2], + "Below": [0, 0, -2], + "Front": [0, -2, 0], + "Back": [0, 2, 0], + "Left": [-2, 0, 0], + "Right": [2, 0, 0], + }[viewpoint] + except KeyError: + # evaluation.message() + # TODO + viewpoint = [1.3, -2.4, 2] + + assert ( + isinstance(viewpoint, list) + and len(viewpoint) == 3 + and all(isinstance(x, numbers.Real) for x in viewpoint) + ) + self.viewpoint = viewpoint + + # TODO Aspect Ratio + # aspect_ratio = self.graphics_options['AspectRatio'].to_python() + + boxratios = graphics_options["System`BoxRatios"].to_python() + if boxratios == "System`Automatic": + boxratios = ["System`Automatic"] * 3 + + if not isinstance(boxratios, list) or len(boxratios) != 3: + raise BoxExpressionError + + plot_range = graphics_options["System`PlotRange"].to_python() + if plot_range == "System`Automatic": + plot_range = ["System`Automatic"] * 3 + if not isinstance(plot_range, list) or len(plot_range) != 3: + raise BoxExpressionError + + elements = Graphics3DElements(content, evaluation) + # If one of the primitives or directives fails to be + # converted into a box expression, then the background color + # is set to pink, overwriting the options. + if hasattr(elements, "background_color"): + self.background_color = elements.background_color + + def calc_dimensions(final_pass=True): + # TODO: the code below is broken in any other case but Automatic + # because it calls elements.translate which is not implemented. + # Plots may pass specific plot ranges, triggering this deficiency + # and causing tests to fail The following line avoids this, + # and it should not change the behavior of any case which did + # previously fail with an exception. + # + # This code should be DRYed (together with the very similar code + # for the 2d case), and the missing .translate method added. + plot_range = ["System`Automatic"] * 3 + + if "System`Automatic" in plot_range: + xmin, xmax, ymin, ymax, zmin, zmax = elements.extent() + else: + xmin = xmax = ymin = ymax = zmin = zmax = None + + try: + if plot_range[0] == "System`Automatic": + if xmin is None and xmax is None: + xmin = 0 + xmax = 1 + elif xmin == xmax: + xmin -= 1 + xmax += 1 + elif isinstance(plot_range[0], list) and len(plot_range[0]) == 2: + xmin, xmax = list(map(float, plot_range[0])) + xmin = elements.translate((xmin, 0, 0))[0] + xmax = elements.translate((xmax, 0, 0))[0] + else: + raise BoxExpressionError + + if plot_range[1] == "System`Automatic": + if ymin is None and ymax is None: + ymin = 0 + ymax = 1 + elif ymin == ymax: + ymin -= 1 + ymax += 1 + elif isinstance(plot_range[1], list) and len(plot_range[1]) == 2: + ymin, ymax = list(map(float, plot_range[1])) + ymin = elements.translate((0, ymin, 0))[1] + ymax = elements.translate((0, ymax, 0))[1] + else: + raise BoxExpressionError + + if plot_range[2] == "System`Automatic": + if zmin is None and zmax is None: + zmin = 0 + zmax = 1 + elif zmin == zmax: + zmin -= 1 + zmax += 1 + elif isinstance(plot_range[1], list) and len(plot_range[2]) == 2: + zmin, zmax = list(map(float, plot_range[2])) + zmin = elements.translate((0, 0, zmin))[2] + zmax = elements.translate((0, 0, zmax))[2] + else: + raise BoxExpressionError + except (ValueError, TypeError): + raise BoxExpressionError + + boxscale = [1.0, 1.0, 1.0] + if boxratios[0] != "System`Automatic": + boxscale[0] = boxratios[0] / (xmax - xmin) + if boxratios[1] != "System`Automatic": + boxscale[1] = boxratios[1] / (ymax - ymin) + if boxratios[2] != "System`Automatic": + boxscale[2] = boxratios[2] / (zmax - zmin) + + if final_pass: + xmin *= boxscale[0] + xmax *= boxscale[0] + ymin *= boxscale[1] + ymax *= boxscale[1] + zmin *= boxscale[2] + zmax *= boxscale[2] + + # Rescale lighting + for i, light in enumerate(self.lighting): + if self.lighting[i]["type"] != "Ambient": + self.lighting[i]["position"] = [ + light["position"][j] * boxscale[j] for j in range(3) + ] + if self.lighting[i]["type"] == "Spot": + self.lighting[i]["target"] = [ + light["target"][j] * boxscale[j] for j in range(3) + ] + + w = 0 if (xmin is None or xmax is None) else xmax - xmin + h = 0 if (ymin is None or ymax is None) else ymax - ymin + + return xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h + + xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h = calc_dimensions( + final_pass=False + ) + + axes, ticks, ticks_style = create_axes( + self, + elements, + graphics_options, + xmin, + xmax, + ymin, + ymax, + zmin, + zmax, + boxscale, + ) + + return elements, axes, ticks, ticks_style, calc_dimensions, boxscale diff --git a/mathics/format/box/makeboxes.py b/mathics/format/box/makeboxes.py index 3b75fa918..4df7cbf1e 100644 --- a/mathics/format/box/makeboxes.py +++ b/mathics/format/box/makeboxes.py @@ -9,47 +9,43 @@ from typing import List from mathics.core.atoms import Complex, Rational, String -from mathics.core.element import BaseElement, BoxElementMixin +from mathics.core.definitions import BOX_FORMS +from mathics.core.element import BaseElement, BoxElementMixin, EvalMixin from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression from mathics.core.symbols import ( Atom, Symbol, + SymbolFalse, SymbolFullForm, + SymbolHoldForm, SymbolList, SymbolMakeBoxes, + SymbolTrue, ) from mathics.core.systemsymbols import ( # SymbolRule, SymbolRuleDelayed, + SymbolAborted, SymbolComplex, - SymbolInputForm, + SymbolParentForm, SymbolRational, SymbolStandardForm, + SymbolTraditionalForm, ) +from mathics.eval.lists import list_boxes from mathics.format.box.formatvalues import do_format from mathics.format.box.precedence import parenthesize +PRINT_FORMS_CALLBACK = {} -def to_boxes(x, evaluation: Evaluation, options={}) -> BoxElementMixin: - """ - This function takes the expression ``x`` - and tries to reduce it to a ``BoxElementMixin`` - expression using an evaluation object. - """ - if isinstance(x, BoxElementMixin): - return x - if isinstance(x, Atom): - x = x.atom_to_boxes(SymbolStandardForm, evaluation) - return to_boxes(x, evaluation, options) - if isinstance(x, Expression): - if x.has_form("MakeBoxes", None): - x_boxed = x.evaluate(evaluation) - else: - x_boxed = eval_makeboxes(x, evaluation) - if isinstance(x_boxed, BoxElementMixin): - return x_boxed - if isinstance(x_boxed, Atom): - return to_boxes(x_boxed, evaluation, options) - return eval_makeboxes_fullform(x, evaluation) + +def is_print_form_callback(head_name: str): + """Decorator for register print form callbacks""" + + def _register(func): + PRINT_FORMS_CALLBACK[head_name] = func + return func + + return _register # this temporarily replaces the _BoxedString class @@ -59,10 +55,100 @@ def _boxed_string(string: str, **options): return StyleBox(String(string), **options) +@is_print_form_callback("System`StandardForm") +def eval_makeboxes_standard_form(expr, evaluation): + from mathics.builtin.box.layout import FormBox, TagBox + + boxed = apply_makeboxes_rules(expr, evaluation, SymbolStandardForm) + boxed = FormBox(boxed, SymbolStandardForm) + boxed = TagBox(boxed, SymbolStandardForm, **{"System`Editable": SymbolTrue}) + return boxed + + +@is_print_form_callback("System`TraditionalForm") +def eval_makeboxes_traditional_form(expr, evaluation): + from mathics.builtin.box.layout import FormBox, TagBox + + boxed = apply_makeboxes_rules(expr, evaluation, SymbolTraditionalForm) + boxed = FormBox(boxed, SymbolTraditionalForm) + boxed = TagBox(boxed, SymbolTraditionalForm, **{"System`Editable": SymbolTrue}) + return boxed + + +def apply_makeboxes_rules( + expr: BaseElement, evaluation: Evaluation, form: Symbol = SymbolStandardForm +) -> BoxElementMixin: + """ + This function takes the definitions provided by the evaluation + object, and produces a boxed fullform for expr. + + Basically: MakeBoxes[expr, form] + """ + parent_form = form + + if form not in evaluation.definitions.boxforms: + expr = Expression( + SymbolFullForm, + Expression(SymbolHoldForm, Expression(SymbolMakeBoxes, expr, form)), + ) + evaluation.message("MakeBoxes", "boxfmt", form, expr) + elif form not in BOX_FORMS: + parent_form = find_parent_form(form, evaluation) + + def yield_rules(): + # Look + for lookup in (expr.get_lookup_name(), "System`MakeBoxes"): + definition = evaluation.definitions.get_definition(lookup) + for rule in definition.formatvalues.get("_MakeBoxes", []): + yield rule + + mb_expr = Expression(SymbolMakeBoxes, expr, form) + boxed = mb_expr + for rule in yield_rules(): + try: + boxed = rule.apply(mb_expr, evaluation, fully=False) + except OverflowError: + evaluation.message("General", "ovfl") + boxed = mb_expr + continue + if boxed is mb_expr or boxed is None or boxed.sameQ(mb_expr): + continue + if boxed is SymbolAborted: + return String("Aborted") + if isinstance(boxed, EvalMixin): + return boxed.evaluate(evaluation) + if isinstance(boxed, BoxElementMixin): + return boxed + + # Try with the ParentForm + if parent_form is not form: + return apply_makeboxes_rules(expr, evaluation, parent_form) + + return eval_generic_makeboxes(expr, form, evaluation) + + # TODO: evaluation is needed because `atom_to_boxes` uses it. Can we remove this # argument? +@is_print_form_callback("System`FullForm") def eval_makeboxes_fullform( - element: BaseElement, evaluation: Evaluation + element: BaseElement, evaluation: Evaluation, **kwargs +) -> BoxElementMixin: + from mathics.builtin.box.layout import StyleBox, TagBox + + result = eval_makeboxes_fullform_recursive(element, evaluation, **kwargs) + style_box = StyleBox( + result, + **{ + "System`ShowSpecialCharacters": SymbolFalse, + "System`ShowStringCharacters": SymbolTrue, + "System`NumberMarks": SymbolTrue, + }, + ) + return TagBox(style_box, SymbolFullForm) + + +def eval_makeboxes_fullform_recursive( + element: BaseElement, evaluation: Evaluation, **kwargs ) -> BoxElementMixin: """Same as MakeBoxes[FullForm[expr_], f_]""" from mathics.builtin.box.expression import BoxExpression @@ -88,7 +174,7 @@ def eval_makeboxes_fullform( head, elements = expr.head, expr.elements boxed_elements = tuple( - (eval_makeboxes_fullform(element, evaluation) for element in elements) + (eval_makeboxes_fullform_recursive(element, evaluation) for element in elements) ) # In some places it would be less verbose to use special outputs for # `List`, `Rule` and `RuleDelayed`. WMA does not that, but we do it for @@ -104,7 +190,7 @@ def eval_makeboxes_fullform( result_elements = [left] else: left, right, sep = (String(ch) for ch in ("[", "]", ",")) - result_elements = [eval_makeboxes_fullform(head, evaluation), left] + result_elements = [eval_makeboxes_fullform_recursive(head, evaluation), left] if len(boxed_elements) > 1: arguments: List[BoxElementMixin] = [] @@ -121,16 +207,24 @@ def eval_makeboxes_fullform( def eval_generic_makeboxes(expr, f, evaluation): """MakeBoxes[expr_, - f:TraditionalForm|StandardForm|OutputForm|InputForm]""" + f:TraditionalForm|StandardForm]""" from mathics.builtin.box.layout import RowBox if isinstance(expr, BoxElementMixin): expr = expr.to_expression() if isinstance(expr, Atom): return expr.atom_to_boxes(f, evaluation) + if expr.has_form("List", None): + return RowBox(*list_boxes(expr.elements, f, evaluation, "{", "}")) else: head = expr.head elements = expr.elements + if head in evaluation.definitions.boxforms and len(elements) == 1: + return apply_makeboxes_rules(elements[0], evaluation, head) + + printform_callback = PRINT_FORMS_CALLBACK.get(head.get_name(), None) + if printform_callback is not None: + return printform_callback(elements[0], evaluation) f_name = f.get_name() if f_name == "System`TraditionalForm": @@ -154,6 +248,7 @@ def eval_generic_makeboxes(expr, f, evaluation): "System`InputForm", "System`OutputForm", ): + raise ValueError sep = ", " else: sep = "," @@ -178,24 +273,16 @@ def eval_generic_makeboxes(expr, f, evaluation): return RowBox(*result) -def eval_makeboxes( - expr, evaluation: Evaluation, form=SymbolStandardForm -) -> BoxElementMixin: - """ - 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. By now, much of the formatting - # relies in rules of the form `MakeBoxes[expr, OutputForm]` - # which is wrong. - if form is SymbolFullForm: - return eval_makeboxes_fullform(expr, evaluation) - if form is SymbolInputForm: - expr = Expression(form, expr) - form = SymbolStandardForm - return Expression(SymbolMakeBoxes, expr, form).evaluate(evaluation) +def find_parent_form(form, evaluation): + """Recursively find the ParentForm of the BoxForm `form`""" + parent_form = form + while parent_form not in BOX_FORMS: + parent_form_expr = Expression(SymbolParentForm, form) + parent_form = parent_form_expr.evaluate(evaluation) + if parent_form_expr.sameQ(parent_form): + evaluation.message("ParentForm", "deflt", parent_form_expr) + return SymbolStandardForm + return parent_form def format_element( @@ -206,9 +293,33 @@ def format_element( """ evaluation.is_boxing = True formatted_expr = do_format(element, evaluation, form) - # print(" FormatValues->", formatted_expr) - result_box = eval_makeboxes(formatted_expr, evaluation, form) - # print(" box rules->", result_box) + if form not in evaluation.definitions.boxforms: + formatted_expr = Expression(form, formatted_expr) + form = SymbolStandardForm + result_box = apply_makeboxes_rules(formatted_expr, evaluation, form) if isinstance(result_box, BoxElementMixin): return result_box - return eval_makeboxes_fullform(element, evaluation) + return eval_makeboxes_fullform_recursive(element, evaluation) + + +def to_boxes(x, evaluation: Evaluation, options={}) -> BoxElementMixin: + """ + This function takes the expression ``x`` + and tries to reduce it to a ``BoxElementMixin`` + expression using an evaluation object. + """ + if isinstance(x, BoxElementMixin): + return x + if isinstance(x, Atom): + x = x.atom_to_boxes(SymbolStandardForm, evaluation) + return to_boxes(x, evaluation, options) + if isinstance(x, Expression): + if x.has_form("MakeBoxes", 1, 2): + x_boxed = x.evaluate(evaluation) + if isinstance(x_boxed, BoxElementMixin): + return x_boxed + if isinstance(x_boxed, Atom): + return to_boxes(x_boxed, evaluation, options) + else: + return apply_makeboxes_rules(x, evaluation) + return eval_makeboxes_fullform_recursive(x, evaluation) diff --git a/mathics/format/box/outputforms.py b/mathics/format/box/outputforms.py index a653ee5f8..60d187f4d 100644 --- a/mathics/format/box/outputforms.py +++ b/mathics/format/box/outputforms.py @@ -1,18 +1,33 @@ import re from mathics.core.atoms import Integer, String +from mathics.core.element import BaseElement, BoxElementMixin +from mathics.core.evaluation import Evaluation from mathics.core.expression import BoxError, Expression from mathics.core.list import ListExpression -from mathics.core.symbols import SymbolFalse, SymbolFullForm, SymbolList -from mathics.core.systemsymbols import SymbolRowBox, SymbolTraditionalForm +from mathics.core.symbols import ( + Symbol, + SymbolFalse, + SymbolFullForm, + SymbolList, + SymbolTrue, +) +from mathics.core.systemsymbols import ( + SymbolMathMLForm, + SymbolTeXForm, + SymbolTraditionalForm, +) from mathics.eval.testing_expressions import expr_min -from mathics.format.box.makeboxes import format_element +from mathics.format.box.makeboxes import format_element, is_print_form_callback MULTI_NEWLINE_RE = re.compile(r"\n{2,}") -def eval_mathmlform(expr, evaluation) -> Expression: +@is_print_form_callback("System`MathMLForm") +def eval_mathmlform(expr: BaseElement, evaluation: Evaluation) -> BoxElementMixin: "MakeBoxes[MathMLForm[expr_], form_]" + from mathics.builtin.box.layout import InterpretationBox + boxes = format_element(expr, evaluation, SymbolTraditionalForm) try: mathml = boxes.boxes_to_mathml(evaluation=evaluation) @@ -34,14 +49,23 @@ def eval_mathmlform(expr, evaluation) -> Expression: mathml = '%s' % mathml mathml = '%s' % mathml # convert_box(boxes) - return Expression(SymbolRowBox, ListExpression(String(mathml))) + return InterpretationBox( + String(f'"{mathml}"'), + Expression(SymbolMathMLForm, expr), + **{"System`AutoDelete": SymbolTrue, "System`Editable": SymbolTrue}, + ) -def eval_tableform(self, table, f, evaluation, options): +def eval_tableform( + self, table: BaseElement, f: Symbol, evaluation: Evaluation, options +): """MakeBoxes[TableForm[table_], f_]""" from mathics.builtin.box.layout import GridBox from mathics.builtin.tensors import get_dimensions + if not isinstance(table, Expression): + return format_element(table, evaluation, f) + dims = len(get_dimensions(table, head=SymbolList)) depth = self.get_option(options, "TableDepth", evaluation, pop=True) options["System`TableDepth"] = depth @@ -93,7 +117,10 @@ def transform_item(item): return result -def eval_texform(expr, evaluation) -> Expression: +@is_print_form_callback("System`TeXForm") +def eval_texform(expr: BaseElement, evaluation: Evaluation) -> BoxElementMixin: + from mathics.builtin.box.layout import InterpretationBox + boxes = format_element(expr, evaluation, SymbolTraditionalForm) try: # Here we set ``show_string_characters`` to False, to reproduce @@ -114,4 +141,8 @@ def eval_texform(expr, evaluation) -> Expression: Expression(SymbolFullForm, expr).evaluate(evaluation), ) tex = "" - return Expression(SymbolRowBox, ListExpression(String(tex))) + return InterpretationBox( + String(f'"{tex}"'), + Expression(SymbolTeXForm, expr), + **{"System`AutoDelete": SymbolTrue, "System`Editable": SymbolTrue}, + ) diff --git a/mathics/format/form/inputform.py b/mathics/format/form/inputform.py index ea9969441..f20f02d63 100644 --- a/mathics/format/form/inputform.py +++ b/mathics/format/form/inputform.py @@ -35,9 +35,7 @@ from mathics.core.symbols import Atom from mathics.core.systemsymbols import ( SymbolInputForm, - SymbolLeft, SymbolNonAssociative, - SymbolNone, SymbolRight, ) from mathics.format.box.formatvalues import do_format # , format_element @@ -46,6 +44,8 @@ from .util import ( ARITHMETIC_OPERATOR_STRINGS, BLANKS_TO_STRINGS, + PARENTHESIZED_FIRST, + PARENTHESIZED_REST, _WrongFormattedExpression, collect_in_pre_post_arguments, get_operator_str, @@ -91,7 +91,7 @@ def render_input_form(expr: BaseElement, evaluation: Evaluation, **kwargs) -> st @register_inputform("System`Association") def _association_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs -): +) -> str: elements = expr.elements result = ", ".join( [render_input_form(elem, evaluation, **kwargs) for elem in elements] @@ -163,15 +163,13 @@ def _infix_expression_to_inputform_text( raise _WrongFormattedExpression # Process the first operand: - parenthesized = group in (SymbolNone, SymbolRight, SymbolNonAssociative) + parenthesized = group in PARENTHESIZED_FIRST operand = operands[0] result = str(render_input_form(operand, evaluation, **kwargs)) result = parenthesize(precedence, operand, result, parenthesized) - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - # Process the rest of operands + parenthesized = group in PARENTHESIZED_REST num_ops = len(ops_lst) for index, operand in enumerate(operands[1:]): curr_op = ops_lst[index % num_ops] @@ -210,7 +208,8 @@ def _prefix_expression_to_inputform_text( operand = operands[0] kwargs["encoding"] = kwargs.get("encoding", SYSTEM_CHARACTER_ENCODING) target_txt = render_input_form(operand, evaluation, **kwargs) - target_txt = parenthesize(precedence, operand, target_txt, True) + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) + target_txt = parenthesize(precedence, operand, target_txt, parenthesized) return str(op_head) + target_txt @@ -229,15 +228,16 @@ def _postfix_expression_to_inputform_text( if len(operands) != 1 or not isinstance(op_head, str): raise _WrongFormattedExpression operand = operands[0] + parenthesized = group in (None, SymbolRight, SymbolNonAssociative) inputform_txt = render_input_form(operand, evaluation, **kwargs) - target_txt = parenthesize(precedence, operand, inputform_txt, True) + target_txt = parenthesize(precedence, operand, inputform_txt, parenthesized) return target_txt + op_head @register_inputform("System`Blank") @register_inputform("System`BlankSequence") @register_inputform("System`BlankNullSequence") -def _blanks(expr: Expression, evaluation: Evaluation, **kwargs): +def _blanks(expr: Expression, evaluation: Evaluation, **kwargs) -> str: elements = expr.elements if len(elements) > 1: return _generic_to_inputform_text(expr, evaluation, **kwargs) @@ -252,8 +252,36 @@ def _blanks(expr: Expression, evaluation: Evaluation, **kwargs): return _generic_to_inputform_text(expr, evaluation, **kwargs) +@register_inputform("System`Optional") +def _optional(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + name: str = "" + post: str = "" + elements = expr.elements + if not expr.has_form("Optional", 1, 2): + raise _WrongFormattedExpression + if len(elements) == 2: + post = ":" + render_input_form(elements[1], evaluation, **kwargs) + else: + post = "." + + operand = elements[0] + if operand.has_form("Pattern", 2): + name = render_input_form(operand.elements[0], evaluation, **kwargs) + operand = operand.elements[1] + + if not operand.has_form(("Blank", "BlankNullSequence", "BlankSequence"), 0): + raise _WrongFormattedExpression + + blank_kind = operand.head + result = name + BLANKS_TO_STRINGS[blank_kind] + post + # `name__.` cannot be reentered if it is not wrapped in parenthesis: + if post == ".": + result = f"({result})" + return result + + @register_inputform("System`Pattern") -def _pattern(expr: Expression, evaluation: Evaluation, **kwargs): +def _pattern(expr: Expression, evaluation: Evaluation, **kwargs) -> str: elements = expr.elements if len(elements) != 2: return _generic_to_inputform_text(expr, evaluation, **kwargs) @@ -263,7 +291,7 @@ def _pattern(expr: Expression, evaluation: Evaluation, **kwargs): @register_inputform("System`Rule") @register_inputform("System`RuleDelayed") -def _rule_to_inputform_text(expr, evaluation: Evaluation, **kwargs): +def _rule_to_inputform_text(expr, evaluation: Evaluation, **kwargs) -> str: """Rule|RuleDelayed[{...}]""" head = expr.head elements = expr.elements @@ -280,7 +308,7 @@ def _rule_to_inputform_text(expr, evaluation: Evaluation, **kwargs): @register_inputform("System`Slot") def _slot_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs -): +) -> str: elements = expr.elements if len(elements) != 1: raise _WrongFormattedExpression @@ -298,7 +326,7 @@ def _slot_expression_to_inputform_text( @register_inputform("System`SlotSequence") def _slotsequence_expression_to_inputform_text( expr: Expression, evaluation: Evaluation, **kwargs -): +) -> str: elements = expr.elements if len(elements) != 1: raise _WrongFormattedExpression diff --git a/mathics/format/form/outputform.py b/mathics/format/form/outputform.py index e54ec33f5..002b04fbf 100644 --- a/mathics/format/form/outputform.py +++ b/mathics/format/form/outputform.py @@ -30,7 +30,6 @@ SymbolInfix, SymbolLeft, SymbolNonAssociative, - SymbolNone, SymbolOutputForm, SymbolPower, SymbolRight, @@ -51,6 +50,8 @@ from .inputform import render_input_form from .util import ( BLANKS_TO_STRINGS, + PARENTHESIZED_FIRST, + PARENTHESIZED_REST, PRECEDENCE_FUNCTION_APPLY, PRECEDENCE_PLUS, PRECEDENCE_POWER, @@ -244,6 +245,7 @@ def render_output_form(expr: BaseElement, evaluation: Evaluation, **kwargs): if format_expr is None: return "" + head = format_expr.get_head() lookup_name: str = head.get_name() or head.get_lookup_name() callback = EXPR_TO_OUTPUTFORM_TEXT_MAP.get(lookup_name, None) @@ -262,6 +264,21 @@ def render_output_form(expr: BaseElement, evaluation: Evaluation, **kwargs): return _default_render_output_form(format_expr, evaluation, **kwargs) +@register_outputform("System`Format") +def format_format(expr, evaluation, **kwargs): + """Format[expr_, form___]""" + elements = expr.elements + if len(elements) == 1: + return render_output_form(elements[0], evaluation, **kwargs) + if len(elements) == 2: + expr, form = elements + if form not in evaluation.definitions.printforms: + evaluation.message("FormatType", "ftype", form) + return render_output_form(expr, evaluation, **kwargs) + return other_forms(Expression(form, expr), evaluation, **kwargs) + raise _WrongFormattedExpression + + @register_outputform("System`Graphics") def graphics(expr: Expression, evaluation: Evaluation, **kwargs) -> str: if not isinstance(expr.head, Symbol): @@ -318,7 +335,6 @@ def other_forms(expr, evaluation, **kwargs): if not isinstance(expr.head, Symbol): raise _WrongFormattedExpression - print("format", expr) result = format_element(expr, evaluation, SymbolStandardForm, **kwargs) return result.boxes_to_text() @@ -373,15 +389,13 @@ def _infix_outputform_text(expr: Expression, evaluation: Evaluation, **kwargs) - # raise _WrongFormattedExpression # Process the first operand: - parenthesized = group in (SymbolNone, SymbolRight, SymbolNonAssociative) + parenthesized = group in PARENTHESIZED_FIRST operand = operands[0] result = str(render_output_form(operand, evaluation, **kwargs)) result = parenthesize(precedence, operand, result, parenthesized) - if group in (SymbolLeft, SymbolRight): - parenthesized = not parenthesized - # Process the rest of operands + parenthesized = group in PARENTHESIZED_REST num_ops = len(ops_lst) for index, operand in enumerate(operands[1:]): curr_op = ops_lst[index % num_ops] @@ -489,6 +503,32 @@ def _numberform_outputform(expr, evaluation, **kwargs): return render_output_form(target, evaluation, **kwargs) +# TODO: DRY ME with input form +@register_outputform("System`Optional") +def _optional(expr: Expression, evaluation: Evaluation, **kwargs) -> str: + name: str = "" + post: str = "" + elements = expr.elements + if not expr.has_form("Optional", 1, 2): + raise _WrongFormattedExpression + if len(elements) == 2: + post = ":" + render_output_form(elements[1], evaluation, **kwargs) + else: + post = "." + + operand = elements[0] + if operand.has_form("Pattern", 2): + name = render_output_form(operand.elements[0], evaluation, **kwargs) + operand = operand.elements[1] + + if not operand.has_form(("Blank", "BlankNullSequence", "BlankSequence"), 0): + raise _WrongFormattedExpression + + blank_kind = operand.head + result = name + BLANKS_TO_STRINGS[blank_kind] + post + return result + + @register_outputform("System`Out") def out_outputform(expr: Expression, evaluation: Evaluation, **kwargs): if not isinstance(expr.head, Symbol): @@ -618,13 +658,16 @@ def power_render_output_form( @register_outputform("System`PrecedenceForm") def precedenceform_render_output_form( - expr: Expression, evaluation: Evaluation, form: Symbol, **kwargs + expr: Expression, evaluation: Evaluation, **kwargs ) -> str: if not isinstance(expr.head, Symbol): raise _WrongFormattedExpression if len(expr.elements) == 2: - return render_output_form(expr.elements[0], evaluation, **kwargs) + arg_1, arg_2 = expr.elements + if not isinstance(arg_2, (Integer, Real)): + raise _WrongFormattedExpression + return render_output_form(arg_1, evaluation, **kwargs) raise _WrongFormattedExpression diff --git a/mathics/format/form/util.py b/mathics/format/form/util.py index 0bca29f07..a3a7a7584 100644 --- a/mathics/format/form/util.py +++ b/mathics/format/form/util.py @@ -3,6 +3,7 @@ Common routines and objects used in rendering PrintForms. """ + from typing import Final, FrozenSet, List, Optional, Tuple from mathics.core.atoms import Integer, String @@ -34,10 +35,10 @@ class _WrongFormattedExpression(Exception): ARITHMETIC_OPERATOR_STRINGS: Final[FrozenSet[str]] = frozenset( [ - *operator_to_string["Divide"], - *operator_to_string["NonCommutativeMultiply"], - *operator_to_string["Power"], - *operator_to_string["Times"], + *operator_to_string.get("Divide", ["/"]), + *operator_to_string.get("NonCommutativeMultiply", ["**"]), + *operator_to_string.get("Power", ["^"]), + *operator_to_string.get("Times", ["*"]), " ", ] ) @@ -50,6 +51,22 @@ class _WrongFormattedExpression(Exception): PRECEDENCE_POWER: Final[int] = PRECEDENCES.get("Power", 590) +# TODO: (rocky) See if we can accomplish parenthesization using precedence and association values as is common for this kind of thing. +# +# These constants are used to decide if two operands with the +# same precedence than the operation which are part of must be +# parenthesized or not. +# For example, `Sequence[a,b,c]` === a;;b;;c has "None" associativity, +# so `Sequence[-1;;-1;;-1]` is formatted as ``-1;;-1;;-1`` +# On the other hand, `Divide` is left associative, so +# `Divide[-1,-1]` is formatted as `-1 / (-1)`, while +# `Power`, which is right associative, format `Power[-1,-1]` +# as `(-1)^-1`. + +PARENTHESIZED_FIRST = {SymbolRight.name, SymbolNonAssociative.name} +PARENTHESIZED_REST = {SymbolLeft.name, SymbolNonAssociative.name} + + BLANKS_TO_STRINGS = { SymbolBlank: "_", SymbolBlankSequence: "__", @@ -64,7 +81,7 @@ def square_bracket(expr_str: str) -> str: def collect_in_pre_post_arguments( expr: Expression, evaluation: Evaluation, **kwargs -) -> Tuple[list, str | List[str], int, Optional[Symbol]]: +) -> Tuple[list, str | List[str], int, str]: """ Determine operands, operator(s), precedence, and grouping """ @@ -82,7 +99,7 @@ def collect_in_pre_post_arguments( raise _WrongFormattedExpression head = expr.head - group = None + group_name = "None" precedence = PRECEDENCE_BOX_GROUP operands = list(target.elements) @@ -92,13 +109,14 @@ def collect_in_pre_post_arguments( operator_spec = render_function(head, evaluation, **kwargs) if head is SymbolInfix: operator_spec = [ - f"{operator_to_string['Infix']}{operator_spec}{operator_to_string['Infix']}" + f"{operator_to_string.get('Infix', '')}{operator_spec}" + f"{operator_to_string.get('Infix', '')}" ] elif head is SymbolPrefix: - operator_spec = f"{operator_spec}{operator_to_string['Prefix']}" + operator_spec = f"{operator_spec}{operator_to_string.get('Prefix', '')}" elif head is SymbolPostfix: - operator_spec = f"{operator_to_string['Postfix']}{operator_spec}" - return operands, operator_spec, precedence, group + operator_spec = f"{operator_to_string.get('Postfix', '')}{operator_spec}" + return operands, operator_spec, precedence, group_name # At least two parameters: get the operator spec. ops = elements[1] @@ -121,10 +139,9 @@ def collect_in_pre_post_arguments( group = elements[3] if group not in (SymbolNone, SymbolLeft, SymbolRight, SymbolNonAssociative): raise _WrongFormattedExpression - if group is SymbolNone: - group = None + group_name = group.get_name() - return operands, operator_spec, precedence, group + return operands, operator_spec, precedence, group_name def get_operator_str(head, evaluation, **kwargs) -> str: @@ -218,12 +235,14 @@ def normalize_cols(rows, full_rows): for line in cell: col_widths[col] = max(col_widths[col], len(line)) rows = [ - row - if is_full_row - else [ - [line.ljust(col_widths[col]) for line in cell] - for col, cell in enumerate(row) - ] + ( + row + if is_full_row + else [ + [line.ljust(col_widths[col]) for line in cell] + for col, cell in enumerate(row) + ] + ) for row in rows ] return rows, col_widths diff --git a/mathics/format/form_rule/arithfns.py b/mathics/format/form_rule/arithfns.py new file mode 100644 index 000000000..deae84c6b --- /dev/null +++ b/mathics/format/form_rule/arithfns.py @@ -0,0 +1,139 @@ +""" +Format functions for arithmetic expressions. + +""" + +from mathics.builtin.arithmetic import create_infix +from mathics.core.atoms import ( + Complex, + Integer, + Integer1, + IntegerM1, + Number, + Rational, + Real, + String, +) +from mathics.core.convert.expression import to_expression +from mathics.core.convert.sympy import from_sympy +from mathics.core.evaluation import Evaluation +from mathics.core.expression import Expression +from mathics.core.list import ListExpression +from mathics.core.symbols import SymbolDivide, SymbolHoldForm, SymbolPower, SymbolTimes +from mathics.core.systemsymbols import SymbolInfix, SymbolLeft, SymbolMinus +from mathics.format.form.util import PRECEDENCE_PLUS, PRECEDENCE_TIMES + + +def format_plus(items, evaluation: Evaluation): + """format Times[___] using `op` as operator""" + + def negate(item): # -> Expression (see FIXME below) + if item.has_form("Times", 2, None): + if isinstance(item.elements[0], Number): + first, *rest = item.elements + first = -first + if first.sameQ(Integer1): + if len(rest) == 1: + return rest[0] + return Expression(SymbolTimes, *rest) + + return Expression(SymbolTimes, first, *rest) + else: + return Expression(SymbolTimes, IntegerM1, *item.elements) + elif isinstance(item, Number): + return from_sympy(-item.to_sympy()) + else: + return Expression(SymbolTimes, IntegerM1, item) + + def is_negative(value) -> bool: + if isinstance(value, Complex): + real, imag = value.to_sympy().as_real_imag() + if real <= 0 and imag <= 0: + return True + elif isinstance(value, Number) and value.to_sympy() < 0: + return True + return False + + elements = items.get_sequence() + values = [to_expression(SymbolHoldForm, element) for element in elements[:1]] + ops = [] + for element in elements[1:]: + if ( + element.has_form("Times", 1, None) and is_negative(element.elements[0]) + ) or is_negative(element): + element = negate(element) + op = "-" + else: + op = "+" + values.append(Expression(SymbolHoldForm, element)) + ops.append(String(op)) + return Expression( + SymbolInfix, + ListExpression(*values), + ListExpression(*ops), + Integer(PRECEDENCE_PLUS), + SymbolLeft, + ) + + +def format_times(items, evaluation, op="\u2062"): + """format Times[___] using `op` as operator""" + + def inverse(item): + if item.has_form("Power", 2) and isinstance( # noqa + item.elements[1], (Integer, Rational, Real) + ): + neg = -item.elements[1] + if neg.sameQ(Integer1): + return item.elements[0] + else: + return Expression(SymbolPower, item.elements[0], neg) + else: + return item + + items = items.get_sequence() + if len(items) < 2: + return + positive = [] + negative = [] + for item in items: + if ( + item.has_form("Power", 2) + and isinstance(item.elements[1], (Integer, Rational, Real)) + and item.elements[1].to_sympy() < 0 + ): # nopep8 + negative.append(inverse(item)) + elif isinstance(item, Rational): + numerator = item.numerator() + if not numerator.sameQ(Integer1): + positive.append(numerator) + negative.append(item.denominator()) + else: + positive.append(item) + + if positive and hasattr(positive[0], "value") and positive[0].value == -1: + del positive[0] + minus = True + else: + minus = False + positive = [Expression(SymbolHoldForm, item) for item in positive] + negative = [Expression(SymbolHoldForm, item) for item in negative] + if positive: + positive = create_infix(positive, op, PRECEDENCE_TIMES, "Left") + else: + positive = Integer1 + if negative: + negative = create_infix(negative, op, PRECEDENCE_TIMES, "Left") + result = Expression( + SymbolDivide, + Expression(SymbolHoldForm, positive), + Expression(SymbolHoldForm, negative), + ) + else: + result = positive + if minus: + result = Expression( + SymbolMinus, result + ) # Expression('PrecedenceForm', result, 481)) + result = Expression(SymbolHoldForm, result) + return result diff --git a/mathics/format/render/__init__.py b/mathics/format/render/__init__.py index dc361bf7c..a66f28e18 100644 --- a/mathics/format/render/__init__.py +++ b/mathics/format/render/__init__.py @@ -1,27 +1,22 @@ -""" -Lower-level formatting routines. - -Built-in Lower-level formatting includes Asymptote, MathML, SVG, -threejs, and plain text. We hope and expect other formatting to other -kinds backend renderers like matplotlib, can be done by following the -pattern used here. - -These routines typically get called in formatting Mathics3 Box objects. +"""Rendering routines. -The higher level *Forms* (e.g. TeXForm, MathMLForm) typically cause -specific formatters to get called, (e.g. latex, mathml). However, the -two concepts and levels are a little bit different. A given From can -cause invoke of several formatters, which the front-end can influence -based on its capabilities and back-end renders available to it. +Mathics3 Built-in rendering includes renderers to Asymptote, MathML, +SVG, threejs, and plain text. We hope and expect other formatting to +other kinds backend renderers, like matplotlib, can be done by +following the pattern used here. -For example, in graphics there may be several different kinds of -renderers, SVG, or Asymptote for a particular kind of graphics Box. -The front-end needs to decides which format it better suited for it. -The Box, however, is created via a particular high-level Form. +Input to the renders come from some sort of Mathics3 Box. -As another example, front-end may decide to use MathJaX to render -TeXForm if the front-end supports this and the user so desires that. +The higher level Forms (e.g. TeXForm, MathMLForm) typically cause +specific boxing routines to get invoked. From this and the capabilites +and desires of a front end, different rendering routines will invoked +for each kind boxes created. This, in turn, produces strings in +(AMS)LaTeX, MathML, SVG, asymptote, or plain text. +For example, to process the Mathics3 builtin BezierCurve, a +BezierCurveBox will get created. Mathics3 has SVG and an Asymptote +renderers for BezierCurveBoxes. Which one is used is decided on by +the front-end's needs. """ import glob diff --git a/mathics/format/render/asy.py b/mathics/format/render/asy.py index e1c918da1..2217b114a 100644 --- a/mathics/format/render/asy.py +++ b/mathics/format/render/asy.py @@ -1,28 +1,28 @@ # -*- coding: utf-8 -*- """ -Lower-level format of Mathics objects as Asymptote Vector graphics strings. +Mathics3 Graphics box rendering to Asymptote Vector graphics strings. """ import re from mathics.builtin.box.graphics import ( + ArcBox, ArrowBox, BezierCurveBox, FilledCurveBox, + GraphicsElementBox, InsetBox, LineBox, PointBox, PolygonBox, RectangleBox, - _ArcBox, - _RoundBox, + RoundBox, ) from mathics.builtin.box.graphics3d import ( Arrow3DBox, Cone3DBox, Cuboid3DBox, Cylinder3DBox, - Graphics3DElements, Line3DBox, Point3DBox, Polygon3DBox, @@ -30,13 +30,10 @@ Tube3DBox, ) from mathics.builtin.box.uniform_polyhedra import UniformPolyhedron3DBox -from mathics.builtin.graphics import ( - DEFAULT_POINT_FACTOR, - GraphicsElements, - PointSize, - RGBColor, -) +from mathics.builtin.drawing.graphics3d import Graphics3DElements +from mathics.builtin.graphics import DEFAULT_POINT_FACTOR, PointSize, RGBColor from mathics.core.formatter import add_conversion_fn, lookup_method +from mathics.format.box.graphics import GraphicsElements from mathics.format.render.asy_fns import ( asy_add_bezier_fn, asy_add_graph_import, @@ -87,16 +84,16 @@ def apply(self, asy): return self._template % (" * ".join(self.transforms), asy) -def arcbox(self: _ArcBox, **options) -> str: +def arcbox(box: ArcBox, **options) -> str: """ Aysmptote formatting for an arc of a circle or an ellipse. """ - if self.arc is None: + if box.arc is None: # We have a doughnut graph and this is the inner blank hole of that. # It is an empty circle - return _roundbox(self) + return roundbox(box) - x, y, rx, ry, sx, sy, ex, ey, _ = self._arc_params() + x, y, rx, ry, sx, sy, ex, ey, _ = box._arc_params() ry = max(ry, 0.1) # Avoid division by 0 yscale = ry / rx @@ -126,39 +123,39 @@ def create_arc_path(is_closed: bool, yscale: float) -> str: return arc_path - stroke_width = self.style.get_line_width(face_element=self.face_element) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + stroke_width = box.style.get_line_width(face_element=box.face_element) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, stroke_width=stroke_width, - is_face_element=bool(self.face_element), + is_face_element=bool(box.face_element), ) - command = "filldraw" if self.face_element else "draw" - arc_path = create_arc_path(self.face_element or False, yscale) + command = "filldraw" if box.face_element else "draw" + arc_path = create_arc_path(box.face_element or False, yscale) asy = f"""// ArcBox {command}({arc_path}, {pen});""" # print("### arcbox", asy) return asy -add_conversion_fn(_ArcBox, arcbox) +add_conversion_fn(ArcBox, arcbox) -def arrow_box(self: ArrowBox, **options) -> str: - width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def arrow_box(box: ArrowBox, **options) -> str: + width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, stroke_width=width, edge_opacity=edge_opacity_value + edge_color=box.edge_color, stroke_width=width, edge_opacity=edge_opacity_value ) - polyline = self.curve.make_draw_asy(pen) + polyline = box.curve.make_draw_asy(pen) arrow_pen = asy_create_pens( - face_color=self.edge_color, stroke_width=width, face_opacity=edge_opacity_value + face_color=box.edge_color, stroke_width=width, face_opacity=edge_opacity_value ) def polygon(points): @@ -166,10 +163,10 @@ def polygon(points): yield "--".join(["(%.5g,%5g)" % xy for xy in points]) yield "--cycle, % s);" % arrow_pen - extent = self.graphics.view_width or 0 - default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("asy", _ASYTransform) - asy = "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) + extent = box.graphics.view_width or 0 + default_arrow = box._default_arrow(polygon) + custom_arrow = box._custom_arrow("asy", _ASYTransform) + asy = "".join(box._draw(polyline, default_arrow, custom_arrow, extent)) # print("### arrowbox", asy) return asy @@ -189,26 +186,26 @@ def build_3d_pen_color(color, opacity=None): return color_str -def arrow3dbox(self, **options) -> str: +def arrow3dbox(box, **options) -> str: """ Asymptote 3D formatter for Arrow3DBox """ # Set style parameters. - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, stroke_width=1, edge_opacity=edge_opacity_value + edge_color=box.edge_color, stroke_width=1, edge_opacity=edge_opacity_value ) # Draw lines between all points except the last. lines_str = "--".join( - ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in self.lines[0][:-1]] + ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in box.lines[0][:-1]] ) asy = f"draw({lines_str}, {pen});\n" # Draw an arrow between the penultimate and the last point. last_line_str = "--".join( - ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in self.lines[0][-2:]] + ["({0},{1},{2})".format(*(coords.pos()[0])) for coords in box.lines[0][-2:]] ) asy += f"draw(({last_line_str}), {pen}, Arrow3);\n" @@ -219,22 +216,22 @@ def arrow3dbox(self, **options) -> str: add_conversion_fn(Arrow3DBox) -def bezier_curve_box(self: BezierCurveBox, **options) -> str: +def bezier_curve_box(box: BezierCurveBox, **options) -> str: """ Asymptote formatter for BezierCurveBox. """ - line_width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + line_width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, edge_opacity=edge_opacity_value, ) asy = "// BezierCurveBox\n" - asy += asy_add_graph_import(self) - asy += asy_add_bezier_fn(self) - for i, line in enumerate(self.lines): + asy += asy_add_graph_import(box) + asy += asy_add_bezier_fn(box) + for i, line in enumerate(box.lines): pts = [str(xy.pos()) for xy in line] for j in range(1, len(pts) - 1, 3): triple = ", ".join(pts[j - 1 : j + 3]) @@ -248,18 +245,18 @@ def bezier_curve_box(self: BezierCurveBox, **options) -> str: add_conversion_fn(BezierCurveBox, bezier_curve_box) -def cone3dbox(self: Cone3DBox, **options) -> str: - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity +def cone3dbox(box: Cone3DBox, **options) -> str: + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = "// Cone3DBox\n" i = 0 - while i < len(self.points) / 2: + while i < len(box.points) / 2: try: # See https://tex.stackexchange.com/questions/736116/how-to-draw-the-base-geometrical-face-of-a-cone-surface-by-asymptote/736120#736120 - cone_center = self.points[i * 2].pos()[0] - cone_tip = self.points[i * 2 + 1].pos()[0] + cone_center = box.points[i * 2].pos()[0] + cone_tip = box.points[i * 2 + 1].pos()[0] if cone_center is None or cone_tip is None: continue @@ -274,7 +271,7 @@ def cone3dbox(self: Cone3DBox, **options) -> str: asy += f""" triple cone_center = {tuple(cone_center)}; triple cone_tip = {tuple(cone_tip)}; - real cone_radius = {self.radius}; + real cone_radius = {box.radius}; real cone_height = {cone_height}; path3 cone_circle = circle(cone_center, cone_radius, cone_tip); @@ -293,17 +290,17 @@ def cone3dbox(self: Cone3DBox, **options) -> str: add_conversion_fn(Cone3DBox) -def cuboid3dbox(self: Cuboid3DBox, **options) -> str: - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity +def cuboid3dbox(box: Cuboid3DBox, **options) -> str: + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = "// Cuboid3DBox\n" i = 0 - while i < len(self.points) / 2: + while i < len(box.points) / 2: try: - point1 = self.points[i * 2].pos()[0] - point2 = self.points[i * 2 + 1].pos()[0] + point1 = box.points[i * 2].pos()[0] + point2 = box.points[i * 2 + 1].pos()[0] if point1 is None or point2 is None: continue @@ -331,23 +328,23 @@ def cuboid3dbox(self: Cuboid3DBox, **options) -> str: add_conversion_fn(Cuboid3DBox) -def cylinder3dbox(self: Cylinder3DBox, **options) -> str: - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity +def cylinder3dbox(box: Cylinder3DBox, **options) -> str: + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = "// Cylinder3DBox\n" # asy += "currentprojection=orthographic(3,1,4,center=true,zoom=.9);\n" i = 0 - while i < len(self.points) / 2: + while i < len(box.points) / 2: try: - point1 = self.points[i * 2].pos()[0] - point2 = self.points[i * 2 + 1].pos()[0] + point1 = box.points[i * 2].pos()[0] + point2 = box.points[i * 2 + 1].pos()[0] if point1 is None or point2 is None: continue - asy += f"real r={self.radius};\n" + asy += f"real r={box.radius};\n" asy += f"triple A={tuple(point1)}, B={tuple(point2)};\n" asy += "real h=abs(A-B);\n" asy += "revolution cyl=cylinder(A,r,h,B-A);\n" @@ -368,11 +365,11 @@ def cylinder3dbox(self: Cylinder3DBox, **options) -> str: add_conversion_fn(Cylinder3DBox) -def filled_curve_box(self, **options) -> str: - line_width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def filled_curve_box(box, **options) -> str: + line_width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, edge_opacity=edge_opacity_value, ) @@ -381,7 +378,7 @@ def filled_curve_box(self, **options) -> str: pen = "currentpen" def components(): - for component in self.components: + for component in box.components: transformed = [(k, [xy.pos() for xy in p]) for k, p in component] yield "fill(%s--cycle, %s);" % ("".join(asy_bezier(*transformed)), pen) @@ -391,12 +388,12 @@ def components(): add_conversion_fn(FilledCurveBox, filled_curve_box) -def graphics_elements(self, **options) -> str: +def graphics_elements(box: GraphicsElementBox, **options) -> str: """ - Asymptote formatting on a list of graphics elements. + Asymptote formatting on a GraphicsElementBox which may contain other GraphicsElementBox's. """ result = [] - for element in self.elements: + for element in box.elements: try: format_fn = lookup_method(element, "asy") except Exception: @@ -420,25 +417,25 @@ def graphics_elements(self, **options) -> str: add_conversion_fn(Graphics3DElements) -def inset_box(self, **options) -> str: +def inset_box(box: InsetBox, **options) -> str: """Asymptote formatting for boxing an Inset in a graphic.""" - x, y = self.pos.pos() + x, y = box.pos.pos() alignment = "SW" - if hasattr(self, "alignment"): - if self.alignment == "bottom": + if hasattr(box, "alignment"): + if box.alignment == "bottom": # This is typically done for labels under the x axis. alignment = "S" - elif self.alignment == "left": + elif box.alignment == "left": # This is typically done for labels to the left of the y axis. alignment = "W" - opacity_value = self.opacity.opacity if self.opacity else None - content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) + opacity_value = box.opacity.opacity if box.opacity else None + content = box.content.boxes_to_tex(evaluation=box.graphics.evaluation) # FIXME: don't hard code text_style_opts, but allow these to be adjustable. font_size = 3 pen = asy_create_pens( - edge_color=self.color, edge_opacity=opacity_value, fontsize=font_size + edge_color=box.color, edge_opacity=opacity_value, fontsize=font_size ) asy = f"""// InsetBox label("${content}$", ({x},{y}), align={alignment}, {pen});\n""" @@ -448,11 +445,11 @@ def inset_box(self, **options) -> str: add_conversion_fn(InsetBox, inset_box) -def line3dbox(self, **options) -> str: - # l = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def line3dbox(box: Line3DBox, **options) -> str: + # l = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, stroke_width=1, edge_opacity=edge_opacity_value + edge_color=box.edge_color, stroke_width=1, edge_opacity=edge_opacity_value ) return "".join( @@ -460,23 +457,23 @@ def line3dbox(self, **options) -> str: "--".join("({0},{1},{2})".format(*coords.pos()[0]) for coords in line), pen, ) - for line in self.lines + for line in box.lines ) add_conversion_fn(Line3DBox) -def line_box(self: LineBox) -> str: - line_width = self.style.get_line_width(face_element=False) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None +def line_box(box: LineBox) -> str: + line_width = box.style.get_line_width(face_element=False) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, edge_opacity=edge_opacity_value, ) asy = "// LineBox\n" - for line in self.lines: + for line in box.lines: path = "--".join(["(%.5g,%5g)" % coords.pos() for coords in line]) asy += "draw(%s, %s);" % (path, pen) # print("### linebox", asy) @@ -486,15 +483,15 @@ def line_box(self: LineBox) -> str: add_conversion_fn(LineBox, line_box) -def point3dbox(self: Point3DBox, **options) -> str: +def point3dbox(box: Point3DBox, **options) -> str: """ Asymptote 3D formatter for Point3DBox """ - face_color = self.face_color + face_color = box.face_color face_opacity_value = face_color.to_rgba()[3] if face_opacity_value is None: - face_opacity_value = self.face_opacity.opacity + face_opacity_value = box.face_opacity.opacity # Tempoary bug fix: default Point color should be black not white if list(face_color.to_rgba()[:3]) == [1, 1, 1]: @@ -504,7 +501,7 @@ def point3dbox(self: Point3DBox, **options) -> str: face_color=face_color, is_face_element=False, face_opacity=face_opacity_value ) points = [] - for line in self.lines: + for line in box.lines: point_coords = "--".join( "(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line ) @@ -519,24 +516,24 @@ def point3dbox(self: Point3DBox, **options) -> str: add_conversion_fn(Point3DBox) -def pointbox(self: PointBox, **options) -> str: - point_size, _ = self.style.get_style(PointSize, face_element=False) +def pointbox(box: PointBox, **options) -> str: + point_size, _ = box.style.get_style(PointSize, face_element=False) if point_size is None: - point_size = PointSize(self.graphics, value=DEFAULT_POINT_FACTOR) + point_size = PointSize(box.graphics, value=DEFAULT_POINT_FACTOR) # We'll use the heuristic that the default line width is 1 should correspond # to the DEFAULT_POINT_FACTOR dotfactor = INVERSE_POINT_FACTOR * point_size.value - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pen = asy_create_pens( - face_color=self.face_color, + face_color=box.face_color, is_face_element=False, dotfactor=dotfactor, face_opacity=face_opacity_value, ) asy = "// PointBox\n" - for line in self.lines: + for line in box.lines: for coords in line: asy += "dot(%s, %s);" % (coords.pos(), pen) @@ -547,21 +544,21 @@ def pointbox(self: PointBox, **options) -> str: add_conversion_fn(PointBox) -def polygon_3d_box(self: Polygon3DBox, **options) -> str: +def polygon_3d_box(box: Polygon3DBox, **options) -> str: """ Asymptote formatting of a Polygon3DBox. """ - stroke_width = self.style.get_line_width(face_element=True) - if self.vertex_colors is None: - face_color = self.face_color - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + stroke_width = box.style.get_line_width(face_element=True) + if box.vertex_colors is None: + face_color = box.face_color + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None else: face_color = None face_opacity_value = None - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, face_color=face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, @@ -570,7 +567,7 @@ def polygon_3d_box(self: Polygon3DBox, **options) -> str: ) asy = "// Polygon3DBox\n" - for line in self.lines: + for line in box.lines: asy += ( "path3 g=" + "--".join(["(%.5g,%.5g,%.5g)" % coords.pos()[0] for coords in line]) @@ -585,18 +582,18 @@ def polygon_3d_box(self: Polygon3DBox, **options) -> str: add_conversion_fn(Polygon3DBox, polygon_3d_box) -def polygonbox(self: PolygonBox, **options) -> str: - line_width = self.style.get_line_width(face_element=True) - if self.vertex_colors is None: - face_color = self.face_color - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None +def polygonbox(box: PolygonBox, **options) -> str: + line_width = box.style.get_line_width(face_element=True) + if box.vertex_colors is None: + face_color = box.face_color + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None else: face_color = None face_opacity_value = None - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None pens = asy_create_pens( - edge_color=self.edge_color, + edge_color=box.edge_color, face_color=face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, @@ -604,21 +601,21 @@ def polygonbox(self: PolygonBox, **options) -> str: is_face_element=True, ) asy = "// PolygonBox\n" - if self.vertex_colors is not None: + if box.vertex_colors is not None: paths = [] colors = [] edges = [] - for index, line in enumerate(self.lines): + for index, line in enumerate(box.lines): paths.append( "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" ) # ignore opacity colors.append( - ",".join([asy_color(color)[0] for color in self.vertex_colors[index]]) + ",".join([asy_color(color)[0] for color in box.vertex_colors[index]]) ) - edges.append(",".join(["0"] + ["1"] * (len(self.vertex_colors[index]) - 1))) + edges.append(",".join(["0"] + ["1"] * (len(box.vertex_colors[index]) - 1))) asy += "gouraudshade(%s, new pen[] {%s}, new int[] {%s});" % ( "^^".join(paths), @@ -626,7 +623,7 @@ def polygonbox(self: PolygonBox, **options) -> str: ",".join(edges), ) if pens and pens != "nullpen": - for line in self.lines: + for line in box.lines: path = ( "--".join(["(%.5g,%.5g)" % coords.pos() for coords in line]) + "--cycle" ) @@ -639,15 +636,15 @@ def polygonbox(self: PolygonBox, **options) -> str: add_conversion_fn(PolygonBox) -def rectanglebox(self: RectangleBox, **options) -> str: - line_width = self.style.get_line_width(face_element=True) - x1, y1 = self.p1.pos() - x2, y2 = self.p2.pos() - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None +def rectanglebox(box: RectangleBox, **options) -> str: + line_width = box.style.get_line_width(face_element=True) + x1, y1 = box.p1.pos() + x2, y2 = box.p2.pos() + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pens = asy_create_pens( - self.edge_color, - self.face_color, + box.edge_color, + box.face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, stroke_width=line_width, @@ -673,23 +670,23 @@ def rectanglebox(self: RectangleBox, **options) -> str: add_conversion_fn(RectangleBox) -def _roundbox(self: _RoundBox): - x, y = self.c.pos() - rx, ry = self.r.pos() +def roundbox(box: RoundBox): + x, y = box.c.pos() + rx, ry = box.r.pos() rx -= x ry -= y - line_width = self.style.get_line_width(face_element=self.face_element) - edge_opacity_value = self.edge_opacity.opacity if self.edge_opacity else None - face_opacity_value = self.face_opacity.opacity if self.face_opacity else None + line_width = box.style.get_line_width(face_element=box.face_element) + edge_opacity_value = box.edge_opacity.opacity if box.edge_opacity else None + face_opacity_value = box.face_opacity.opacity if box.face_opacity else None pen = asy_create_pens( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, edge_opacity=edge_opacity_value, face_opacity=face_opacity_value, stroke_width=line_width, - is_face_element=self.face_element, + is_face_element=box.face_element, ) - cmd = "filldraw" if self.face_element else "draw" + cmd = "filldraw" if box.face_element else "draw" return "%s(ellipse((%s,%s),%s,%s), %s);" % ( cmd, asy_number(x), @@ -700,44 +697,44 @@ def _roundbox(self: _RoundBox): ) -add_conversion_fn(_RoundBox) +add_conversion_fn(RoundBox) -def sphere3dbox(self: Sphere3DBox, **options) -> str: - # l = self.style.get_line_width(face_element=True) +def sphere3dbox(box: Sphere3DBox, **options) -> str: + # l = box.style.get_line_width(face_element=True) - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) return "// Sphere3DBox\n" + "\n".join( "draw(surface(sphere({0}, {1})), {2});".format( - tuple(coord.pos()[0]), self.radius, color_str + tuple(coord.pos()[0]), box.radius, color_str ) - for coord in self.points + for coord in box.points ) add_conversion_fn(Sphere3DBox) -def tube_3d_box(self: Tube3DBox, **options) -> str: - # if not (hasattr(self.graphics, "tube_import_added") and self.tube_import_added): - # self.graphics.tube_import_added = True +def tube_3d_box(box: Tube3DBox, **options) -> str: + # if not (hasattr(box.graphics, "tube_import_added") and box.tube_import_added): + # box.graphics.tube_import_added = True # asy_head = "import tube;\n\n" # else: # asy_head = "" - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) asy = ( # asy_head + "// Tube3DBox\n draw(tube({0}, scale({1})*unitcircle), {2});".format( "--".join( - "({0},{1},{2})".format(*coords.pos()[0]) for coords in self.points + "({0},{1},{2})".format(*coords.pos()[0]) for coords in box.points ), - self.radius, + box.radius, color_str, ) ) @@ -747,16 +744,16 @@ def tube_3d_box(self: Tube3DBox, **options) -> str: add_conversion_fn(Tube3DBox, tube_3d_box) -def uniform_polyhedron_3d_box(self: UniformPolyhedron3DBox, **options) -> str: - # l = self.style.get_line_width(face_element=True) +def uniform_polyhedron_3d_box(box: UniformPolyhedron3DBox, **options) -> str: + # l = box.style.get_line_width(face_element=True) - face_color = self.face_color.to_js() if self.face_color else (1, 1, 1) - opacity = self.face_opacity + face_color = box.face_color.to_js() if box.face_color else (1, 1, 1) + opacity = box.face_opacity color_str = build_3d_pen_color(face_color, opacity) - render_fn = HEDRON_NAME_MAP.get(self.sub_type, unimplimented_polygon) - return f"// {self.sub_type}\n" + "\n".join( - render_fn(tuple(coord.pos()[0]), self.edge_length, color_str) - for coord in self.points + render_fn = HEDRON_NAME_MAP.get(box.sub_type, unimplimented_polygon) + return f"// {box.sub_type}\n" + "\n".join( + render_fn(tuple(coord.pos()[0]), box.edge_length, color_str) + for coord in box.points ) diff --git a/mathics/format/render/json.py b/mathics/format/render/json.py index 28632e8b0..4d1b81b8e 100644 --- a/mathics/format/render/json.py +++ b/mathics/format/render/json.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter of Mathics objects as JSON data. +Mathics3 Graphics box rendering to JSON data. -Right now this happens mostly for graphics primitives. +Right now, this happens in graphics primitives. """ +import json from mathics.builtin.box.graphics3d import ( Arrow3DBox, Cone3DBox, Cuboid3DBox, Cylinder3DBox, + Graphics3DBox, Line3DBox, Point3DBox, Polygon3DBox, @@ -20,6 +22,7 @@ from mathics.builtin.drawing.graphics3d import Graphics3DElements from mathics.builtin.graphics import PointSize from mathics.core.formatter import add_conversion_fn, lookup_method +from mathics.format.box.graphics3d import prepare_elements as prepare_elements3d # FIXME # Add 2D elements like DensityPlot @@ -47,12 +50,12 @@ def convert_coord_collection( return data -def graphics_3D_elements(self, **options) -> list: - """Iterates over self.elements to convert each item. +def graphics_3D_elements(box: Graphics3DElements, **options) -> list: + """Iterates over box.elements to converting each item. The list of converted items is returned. """ result = [] - for element in self.elements: + for element in box.elements: format_fn = lookup_method(element, "json") result += format_fn(element) @@ -63,13 +66,13 @@ def graphics_3D_elements(self, **options) -> list: add_conversion_fn(Graphics3DElements, graphics_3D_elements) -def arrow_3d_box(self): +def arrow_3d_box(box: Arrow3DBox): """ Compact (lower-level) JSON formatting of a Arrow3DBox. """ # TODO: account for arrow widths and style - color = self.edge_color.to_rgba() - data = convert_coord_collection(self.lines, "arrow", color) + color = box.edge_color.to_rgba() + data = convert_coord_collection(box.lines, "arrow", color) # print("### json Arrow3DBox", data) return data @@ -77,18 +80,18 @@ def arrow_3d_box(self): add_conversion_fn(Arrow3DBox, arrow_3d_box) -def cone_3d_box(self): +def cone_3d_box(box: Cone3DBox): """ Compact (lower-level) JSON formatting of a Cone3DBox. """ - face_color = self.face_color + face_color = box.face_color if face_color is not None: face_color = face_color.to_js() data = convert_coord_collection( - [self.points], + [box.points], "cone", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Cone3DBox", data) return data @@ -97,15 +100,15 @@ def cone_3d_box(self): add_conversion_fn(Cone3DBox, cone_3d_box) -def cuboid_3d_box(self): +def cuboid_3d_box(box: Cuboid3DBox): """ Compact (lower-level) JSON formatting of a Cuboid3DBox. """ - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "cuboid", face_color, ) @@ -116,18 +119,18 @@ def cuboid_3d_box(self): add_conversion_fn(Cuboid3DBox, cuboid_3d_box) -def cylinder_3d_box(self): +def cylinder_3d_box(box: Cylinder3DBox): """ Compact (lower-level) JSON formatting of a Cylinder3DBox. """ - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "cylinder", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Cylinder3DBox", data) return data @@ -136,15 +139,79 @@ def cylinder_3d_box(self): add_conversion_fn(Cylinder3DBox, cylinder_3d_box) -def line_3d_box(self): +def graphics3d_boxes_to_json(box: Graphics3DBox, content=None, **options): + """Turn the Graphics3DBox to into a something JSON like. + This can be used to embed in something else like MathML or Javascript. + + In contrast to to javascript or MathML, no enclosing tags are included. + the caller will do that if it is needed. + """ + assert content is None + ( + elements, + axes, + ticks, + ticks_style, + calc_dimensions, + boxscale, + ) = prepare_elements3d(box, box.content, options) + background = "rgba(100.0%, 100.0%, 100.0%, 100.0%)" + if box.background_color: + components = box.background_color.to_rgba() + if len(components) == 3: + background = "rgb(" + ", ".join(f"{100*c}%" for c in components) + ")" + else: + background = "rgba(" + ", ".join(f"{100*c}%" for c in components) + ")" + + tooltip_text = elements.tooltip_text if hasattr(elements, "tooltip_text") else "" + + js_ticks_style = [s.to_js() for s in ticks_style] + elements._apply_boxscaling(boxscale) + + xmin, xmax, ymin, ymax, zmin, zmax, boxscale, w, h = calc_dimensions() + elements.view_width = w + # FIXME: json is the only thing we can convert MathML into. + # Handle other graphics formats. + format_fn = lookup_method(elements, "json") + + json_repr = json.dumps( + { + "elements": format_fn(elements, **options), + "background_color": background, + "tooltip_text": tooltip_text, + "axes": { + "hasaxes": axes, + "ticks": ticks, + "ticks_style": js_ticks_style, + }, + "extent": { + "xmin": xmin, + "xmax": xmax, + "ymin": ymin, + "ymax": ymax, + "zmin": zmin, + "zmax": zmax, + }, + "lighting": box.lighting, + "viewpoint": box.viewpoint, + "protocol": "1.1", + } + ) + return json_repr + + +add_conversion_fn(Graphics3DBox, graphics3d_boxes_to_json) + + +def line_3d_box(box: Line3DBox): """ Compact (lower-level) JSON formatting of a Line3DBox. """ # TODO: account for line widths and style - color = self.edge_color.to_rgba() - if len(color) < 4 and self.edge_opacity: - color = color + [self.edge_opacity.opacity] - data = convert_coord_collection(self.lines, "line", color) + color = box.edge_color.to_rgba() + if len(color) < 4 and box.edge_opacity: + color = color + [box.edge_opacity.opacity] + data = convert_coord_collection(box.lines, "line", color) # print("### json Line3DBox", data) return data @@ -152,21 +219,21 @@ def line_3d_box(self): add_conversion_fn(Line3DBox, line_3d_box) -def point_3d_box(self) -> list: +def point_3d_box(box: Point3DBox) -> list: """ Compact (lower-level) JSON formatting of a Point3DBox. """ # TODO: account for point size - face_color = self.face_color.to_rgba() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + face_color = box.face_color.to_rgba() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] - point_size, _ = self.style.get_style(PointSize, face_element=False) + point_size, _ = box.style.get_style(PointSize, face_element=False) relative_point_size = 0.01 if point_size is None else point_size.value data = convert_coord_collection( - self.lines, + box.lines, "point", face_color, {"pointSize": relative_point_size * 0.5}, @@ -179,21 +246,21 @@ def point_3d_box(self) -> list: add_conversion_fn(Point3DBox, point_3d_box) -def polygon_3d_box(self) -> list: +def polygon_3d_box(box: Polygon3DBox) -> list: """ Compact (lower-level) JSON formatting of a Polygon3DBox. This format follows an API understood by mathics_threejs_backend. """ # TODO: account for line widths and style - if self.vertex_colors is None: - face_color = self.face_color.to_js() + if box.vertex_colors is None: + face_color = box.face_color.to_js() else: face_color = None - if face_color and len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] + if face_color and len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - self.lines, + box.lines, "polygon", face_color, ) @@ -204,15 +271,15 @@ def polygon_3d_box(self) -> list: add_conversion_fn(Polygon3DBox, polygon_3d_box) -def sphere_3d_box(self) -> list: - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.face_opacity: - face_color = face_color + [self.face_opacity.opacity] +def sphere_3d_box(box: Sphere3DBox) -> list: + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.face_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "sphere", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Sphere3DBox", data) return data @@ -221,15 +288,15 @@ def sphere_3d_box(self) -> list: add_conversion_fn(Sphere3DBox, sphere_3d_box) -def uniform_polyhedron_3d_box(self) -> list: - face_color = self.face_color.to_js() - if len(face_color) < 4 and self.edge_opacity: - face_color = face_color + [self.face_opacity.opacity] +def uniform_polyhedron_3d_box(box: UniformPolyhedron3DBox) -> list: + face_color = box.face_color.to_js() + if len(face_color) < 4 and box.edge_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "uniformPolyhedron", face_color, - {"subType": self.sub_type}, + {"subType": box.sub_type}, ) # print("### json UniformPolyhedron3DBox", data) return data @@ -238,17 +305,17 @@ def uniform_polyhedron_3d_box(self) -> list: add_conversion_fn(UniformPolyhedron3DBox, uniform_polyhedron_3d_box) -def tube_3d_box(self) -> list: - face_color = self.face_color +def tube_3d_box(box: Tube3DBox) -> list: + face_color = box.face_color if face_color is not None: face_color = face_color.to_js() - if len(face_color) < 4 and self.edge_opacity: - face_color = face_color + [self.face_opacity.opacity] + if len(face_color) < 4 and box.edge_opacity: + face_color = face_color + [box.face_opacity.opacity] data = convert_coord_collection( - [self.points], + [box.points], "tube", face_color, - {"radius": self.radius}, + {"radius": box.radius}, ) # print("### json Tube3DBox", data) return data diff --git a/mathics/format/render/latex.py b/mathics/format/render/latex.py index ce589c8a3..329f987fa 100644 --- a/mathics/format/render/latex.py +++ b/mathics/format/render/latex.py @@ -1,22 +1,25 @@ # -*- coding: utf-8 -*- -"""Lower-level formatter of Mathics objects as (AMS)LaTeX strings. +""" +Mathics3 box rendering to (AMS)LaTeX strings. + +Formatting is usually initiated in Mathics via TeXForm[]. AMS LaTeX is LaTeX with addition mathematical symbols, which we may make use of via the mathics-scanner tables. -LaTeX formatting is usually initiated in Mathics via TeXForm[]. - TeXForm in WMA is slightly vague or misleading since the output is -typically LaTeX rather than Plain TeX. In Mathics, we also assume AMS +typically LaTeX rather than Plain TeX. In Mathics3, we also assume AMS LaTeX or more specifically that we the additional AMS Mathematical Symbols exist. """ import re +from mathics.builtin.box.expression import BoxExpression from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox from mathics.builtin.box.layout import ( + FormBox, FractionBox, GridBox, InterpretationBox, @@ -31,7 +34,13 @@ ) from mathics.builtin.colors.color_directives import RGBColor from mathics.core.atoms import String -from mathics.core.convert.op import amstex_operators, get_latex_operator +from mathics.core.convert.op import ( + AMSTEX_OPERATORS, + UNICODE_TO_AMSLATEX, + UNICODE_TO_LATEX, + get_latex_operator, + named_characters, +) from mathics.core.exceptions import BoxConstructError from mathics.core.formatter import ( add_conversion_fn, @@ -39,11 +48,81 @@ ) from mathics.core.symbols import SymbolTrue from mathics.core.systemsymbols import SymbolAutomatic +from mathics.format.box.graphics import prepare_elements as prepare_elements2d +from mathics.format.box.graphics3d import ( + get_boundbox_lines as get_boundbox_lines3D, + prepare_elements as prepare_elements3d, +) from mathics.format.render.asy_fns import asy_color, asy_create_pens, asy_number # mathics_scanner does not generates this table in a way that we can load it here. # When it get fixed, we can use that table instead of this one: +BRACKET_INFO = { + ( + String("("), + String(")"), + ): { + "latex_open": "(", + "latex_closing": ")", + "latex_open_large": r"\left(", + "latex_closing_large": r"\right)", + }, + ( + String("{"), + String("}"), + ): { + "latex_open": r"\{", + "latex_closing": r"\}", + "latex_open_large": r"\left\{", + "latex_closing_large": r"\right\}", + }, + ( + String("["), + String("]"), + ): { + "latex_open": "[", + "latex_closing": "]", + "latex_open_large": r"\left[", + "latex_closing_large": r"\right]", + }, + ( + String(named_characters["LeftDoubleBracket"]), + String(named_characters["RightDoubleBracket"]), + ): { + "latex_open": r"[[", + "latex_closing": "]]", + "latex_open_large": r"\left[\left[", + "latex_closing_large": r"\right]\right]", + }, + ( + String(named_characters["LeftAngleBracket"]), + String(named_characters["RightAngleBracket"]), + ): { + "latex_open": "\\langle", + "latex_closing": "\\rangle", + "latex_open_large": r"\left\langle ", + "latex_closing_large": r"\right\rangle ", + }, + ( + String(named_characters["LeftDoubleBracketingBar"]), + String(named_characters["RightDoubleBracketingBar"]), + ): { + "latex_open": r"\|", + "latex_closing": r"\|", + "latex_open_large": r"\left\|", + "latex_closing_large": r"\right\| ", + }, + ( + String("<|"), + String("|>"), + ): { + "latex_open": r"\langle\vert ", + "latex_closing": r"\vert\rangle ", + "latex_open_large": r"\left\langle\left\vert ", + "latex_closing_large": r"\right\vert\right\rangle ", + }, +} TEX_REPLACE = { "{": r"\{", @@ -57,24 +136,46 @@ "^": r"{}^{\wedge}", "~": r"\sim{}", "|": r"\vert{}", - "\u222b": r"\int ", - "\u2146": r"\, d", - "\uF74C": r"\, d", - "\U0001D451": r"\, d", + # These two are trivial replaces, + # but are needed to define the regular expression + "<": "<", + ">": ">", +} +TEX_TEXT_REPLACE = { + "$": r"\$", + "&": r"$\&$", + "#": r"$\#$", + "%": r"$\%$", + r"{": r"\{", + r"}": r"\}", + r"_": r"\_", + "<": r"$<$", + ">": r"$>$", + "~": r"$\sim$", + "|": r"$\vert$", + "\\": r"$\backslash$", + "^": r"${}^{\wedge}$", } -TEX_TEXT_REPLACE = TEX_REPLACE.copy() + +TEX_REPLACE.update(UNICODE_TO_AMSLATEX) +TEX_REPLACE.update( + { + key: r"\text{" + val + "}" + for key, val in UNICODE_TO_LATEX.items() + if key not in TEX_REPLACE + } +) + +TEX_TEXT_REPLACE.update(UNICODE_TO_LATEX) TEX_TEXT_REPLACE.update( { - "<": r"$<$", - ">": r"$>$", - "~": r"$\sim$", - "|": r"$\vert$", - "\\": r"$\backslash$", - "^": r"${}^{\wedge}$", - "\u222b": r"$\int$ ", - "\uF74C": r"\, d", + key: f"${val}$" + for key, val in UNICODE_TO_AMSLATEX.items() + if key not in TEX_TEXT_REPLACE } ) + + TEX_REPLACE_RE = re.compile("([" + "".join([re.escape(c) for c in TEX_REPLACE]) + "])") @@ -90,12 +191,12 @@ def replace(match): return text -def string(self, **options) -> str: +def string(s: String, **options) -> str: """String to LaTeX form""" - text = self.value + text = s.value - def render(format, string, in_text=False): - return format % encode_tex(string, in_text) + def render(format, string_, in_text=False): + return format % encode_tex(string_, in_text) if text.startswith('"') and text.endswith('"'): show_string_characters = ( @@ -108,51 +209,50 @@ def render(format, string, in_text=False): # show_string_characters = False if show_string_characters: return render(r"\text{``%s''}", text[1:-1], in_text=True) - else: - return render(r"\text{%s}", text[1:-1], in_text=True) - elif text and text[0] in "0123456789-.": + return render(r"\text{%s}", text[1:-1], in_text=True) + if text and text[0] in "0123456789-.": text = text.split("`")[0] return render("%s", text) - else: - # First consider the special cases - op_string = amstex_operators.get(text, None) - if op_string: - return op_string - # Regular text: - if len(text) > 1: - return render(r"\text{%s}", text, in_text=True) + # First consider the special cases + op_string = AMSTEX_OPERATORS.get(text, None) + if op_string: + return op_string - # Unicode operator or variable? - op_string = get_latex_operator(text) - if len(op_string) > 7 and op_string[:7] == r"\symbol": - op_string = r"\text{" + op_string + "}" + # Regular text: + if len(text) > 1: + return render(r"\text{%s}", text, in_text=True) - if op_string != text: - return f" {op_string} " + # Unicode operator or variable? + op_string = get_latex_operator(text) + if len(op_string) > 7 and op_string[:7] == r"\symbol": + op_string = r"\text{" + op_string + "}" - # must be a variable... - return render("%s", text) + if op_string != text: + return f" {op_string} " + + # must be a variable... + return render("%s", text) add_conversion_fn(String, string) -def interpretation_box(self, **options): - return lookup_conversion_method(self.boxed, "latex")(self.boxed, **options) +def interpretation_box(box: InterpretationBox, **options): + return lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) add_conversion_fn(InterpretationBox, interpretation_box) -def pane_box(self, **options): - content = lookup_conversion_method(self.boxed, "latex")(self.boxed, **options) - options = self.box_options +def pane_box(box: PaneBox, **options): + content = lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) + options = box.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() if size == "System`Automatic": return content - elif isinstance(size, int): + if isinstance(size, int): width = f"{size}pt" height = "" elif isinstance(size, tuple) and len(size) == 2: @@ -183,27 +283,27 @@ def pane_box(self, **options): add_conversion_fn(PaneBox, pane_box) -def fractionbox(self, **options) -> str: - _options = self.box_options.copy() +def fractionbox(box: FractionBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options return "\\frac{%s}{%s}" % ( - lookup_conversion_method(self.num, "latex")(self.num, **options), - lookup_conversion_method(self.den, "latex")(self.den, **options), + lookup_conversion_method(box.num, "latex")(box.num, **options), + lookup_conversion_method(box.den, "latex")(box.den, **options), ) add_conversion_fn(FractionBox, fractionbox) -def gridbox(self, elements=None, **box_options) -> str: +def gridbox(box: GridBox, elements=None, **box_options) -> str: def boxes_to_tex(box, **options): return lookup_conversion_method(box, "latex")(box, **options) if not elements: - elements = self._elements + elements = box._elements evaluation = box_options.get("evaluation") - items, options = self.get_array(elements, evaluation) + items, options = box.get_array(elements, evaluation) box_options.update(options) box_options["inside_list"] = True column_alignments = box_options["System`ColumnAlignments"].get_name() @@ -213,9 +313,9 @@ def boxes_to_tex(box, **options): "System`Left": "l", "System`Right": "r", }[column_alignments] - except KeyError: + except KeyError as exc: # invalid column alignment - raise BoxConstructError + raise BoxConstructError from exc column_count = 1 for row in items: if isinstance(row, tuple): @@ -240,96 +340,93 @@ def boxes_to_tex(box, **options): add_conversion_fn(GridBox, gridbox) -def sqrtbox(self, **options): - _options = self.box_options.copy() +def sqrtbox(box: SqrtBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - if self.index: + if box.index: return "\\sqrt[%s]{%s}" % ( - lookup_conversion_method(self.radicand, "latex")(self.radicand, **options), - lookup_conversion_method(self.index, "latex")(self.index, **options), + lookup_conversion_method(box.radicand, "latex")(box.radicand, **options), + lookup_conversion_method(box.index, "latex")(box.index, **options), ) - return "\\sqrt{%s}" % lookup_conversion_method(self.radicand, "latex")( - self.radicand, **options + return "\\sqrt{%s}" % lookup_conversion_method(box.radicand, "latex")( + box.radicand, **options ) add_conversion_fn(SqrtBox, sqrtbox) -def superscriptbox(self, **options): - _options = self.box_options.copy() +def superscriptbox(box: SuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - base_to_tex = lookup_conversion_method(self.base, "latex") - tex1 = base_to_tex(self.base, **options) + base_to_tex = lookup_conversion_method(box.base, "latex") + tex1 = base_to_tex(box.base, **options) - sup_string = self.superindex.get_string_value() + sup_string = box.superindex.get_string_value() # Handle derivatives - if sup_string == "\u2032": + if sup_string == named_characters["Prime"]: return "%s'" % tex1 - elif sup_string == "\u2032\u2032": + if sup_string == named_characters["Prime"] * 2: return "%s''" % tex1 - else: - base = self.tex_block(tex1, True) - superidx_to_tex = lookup_conversion_method(self.superindex, "latex") - superindx = self.tex_block(superidx_to_tex(self.superindex, **options), True) - if len(superindx) == 1 and isinstance(self.superindex, (String, StyleBox)): - return "%s^%s" % ( - base, - superindx, - ) - else: - return "%s^{%s}" % ( - base, - superindx, - ) + base = box.tex_block(tex1, True) + superidx_to_tex = lookup_conversion_method(box.superindex, "latex") + superindx = box.tex_block(superidx_to_tex(box.superindex, **options), True) + if len(superindx) == 1 and isinstance(box.superindex, (String, StyleBox)): + return "%s^%s" % ( + base, + superindx, + ) + return "%s^{%s}" % ( + base, + superindx, + ) add_conversion_fn(SuperscriptBox, superscriptbox) -def subscriptbox(self, **options): - _options = self.box_options.copy() +def subscriptbox(box: SubscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - base_to_tex = lookup_conversion_method(self.base, "latex") - subidx_to_tex = lookup_conversion_method(self.subindex, "latex") + base_to_tex = lookup_conversion_method(box.base, "latex") + subidx_to_tex = lookup_conversion_method(box.subindex, "latex") return "%s_%s" % ( - self.tex_block(base_to_tex(self.base, **options), True), - self.tex_block(subidx_to_tex(self.subindex, **options)), + box.tex_block(base_to_tex(box.base, **options), True), + box.tex_block(subidx_to_tex(box.subindex, **options)), ) add_conversion_fn(SubscriptBox, subscriptbox) -def subsuperscriptbox(self, **options): - _options = self.box_options.copy() +def subsuperscriptbox(box: SubsuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - base_to_tex = lookup_conversion_method(self.base, "latex") - subidx_to_tex = lookup_conversion_method(self.subindex, "latex") - superidx_to_tex = lookup_conversion_method(self.superindex, "latex") + base_to_tex = lookup_conversion_method(box.base, "latex") + subidx_to_tex = lookup_conversion_method(box.subindex, "latex") + superidx_to_tex = lookup_conversion_method(box.superindex, "latex") return "%s_%s^%s" % ( - self.tex_block(base_to_tex(self.base, **options), True), - self.tex_block(subidx_to_tex(self.subindex, **options)), - self.tex_block(superidx_to_tex(self.superindex, **options)), + box.tex_block(base_to_tex(box.base, **options), True), + box.tex_block(subidx_to_tex(box.subindex, **options)), + box.tex_block(superidx_to_tex(box.superindex, **options)), ) add_conversion_fn(SubsuperscriptBox, subsuperscriptbox) -def rowbox(self, **options) -> str: - _options = self.box_options.copy() - _options.update(options) - options = _options +def rowbox_sequence(items, **options): parts_str = [ lookup_conversion_method(element, "latex")(element, **options) - for element in self.items + for element in items ] + if len(parts_str) == 0: + return "" if len(parts_str) == 1: return parts_str[0] # This loop integrate all the row adding spaces after a ",", followed @@ -351,30 +448,71 @@ def rowbox(self, **options) -> str: return result +def rowbox_parenthesized(items, **options): + if len(items) < 2: + return None + key = ( + items[0], + items[-1], + ) + items = items[1:-1] + try: + bracket_data = BRACKET_INFO[key] + except KeyError: + return None + + contain = rowbox_sequence(items, **options) if len(items) > 0 else "" + + if any(item.is_multiline for item in items): + return f'{bracket_data["latex_open_large"]}{contain}{bracket_data["latex_closing_large"]}' + return f'{bracket_data["latex_open"]}{contain}{bracket_data["latex_closing"]}' + + +def rowbox(box: RowBox, **options) -> str: + _options = box.box_options.copy() + _options.update(options) + options = _options + items = box.items + # Handle special cases + if len(items) >= 3: + head, *rest = items + rest_latex = rowbox_parenthesized(rest, **options) + if rest_latex is not None: + # Must be a function-like expression f[] + head_latex = lookup_conversion_method(head, "latex")(head, **options) + return head_latex + rest_latex + if len(items) >= 2: + parenthesized_latex = rowbox_parenthesized(items, **options) + if parenthesized_latex is not None: + return parenthesized_latex + return rowbox_sequence(items, **options) + + add_conversion_fn(RowBox, rowbox) -def stylebox(self, **options) -> str: - _options = self.box_options.copy() +def stylebox(box: StyleBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options - return lookup_conversion_method(self.boxes, "latex")(self.boxes, **options) + return lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) add_conversion_fn(StyleBox, stylebox) -def graphicsbox(self, elements=None, **options) -> str: +def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: """This is the top-level function that converts a Mathics Expression in to something suitable for AMSLaTeX. However right now the only LaTeX support for graphics is via Asymptote and that seems to be the package of choice in general for LaTeX. """ + assert elements is None if not elements: - elements = self._elements - fields = self._prepare_elements(elements, options, max_width=450) + content = box.content + fields = prepare_elements2d(box, content, options, max_width=450) if len(fields) == 2: elements, calc_dimensions = fields else: @@ -409,8 +547,8 @@ def graphicsbox(self, elements=None, **options) -> str: asy_number(ymax), ) - if self.background_color is not None: - color, opacity = asy_color(self.background_color) + if box.background_color is not None: + color, opacity = asy_color(box.background_color) if opacity is not None: color = color + f"+opacity({opacity})" asy_background = "filldraw(%s, %s);" % (asy_box, color) @@ -440,9 +578,9 @@ def graphicsbox(self, elements=None, **options) -> str: add_conversion_fn(GraphicsBox, graphicsbox) -def graphics3dbox(self, elements=None, **options) -> str: - if not elements: - elements = self._elements +def graphics3dbox(box: Graphics3DBox, elements=None, **options) -> str: + assert elements is None + elements = box.content ( elements, @@ -451,7 +589,7 @@ def graphics3dbox(self, elements=None, **options) -> str: ticks_style, calc_dimensions, boxscale, - ) = self._prepare_elements(elements, options, max_width=450) + ) = prepare_elements3d(box, elements, options, max_width=450) elements._apply_boxscaling(boxscale) @@ -478,7 +616,7 @@ def graphics3dbox(self, elements=None, **options) -> str: # Draw boundbox and axes boundbox_asy = "" - boundbox_lines = self.get_boundbox_lines(xmin, xmax, ymin, ymax, zmin, zmax) + boundbox_lines = get_boundbox_lines3D(box, xmin, xmax, ymin, ymax, zmin, zmax) for i, line in enumerate(boundbox_lines): if i in axes_indices: @@ -612,8 +750,8 @@ def graphics3dbox(self, elements=None, **options) -> str: height, width = (400, 400) # TODO: Proper size # Background color - if self.background_color: - bg_color, opacity = asy_color(self.background_color) + if box.background_color: + bg_color, opacity = asy_color(box.background_color) background_directive = "background=" + bg_color + ", " else: background_directive = "" @@ -633,7 +771,7 @@ def graphics3dbox(self, elements=None, **options) -> str: asy_number(width / 60), asy_number(height / 60), # Rescale viewpoint - [vp * max([xmax - xmin, ymax - ymin, zmax - zmin]) for vp in self.viewpoint], + [vp * max([xmax - xmin, ymax - ymin, zmax - zmin]) for vp in box.viewpoint], asy, boundbox_asy, background_directive, @@ -644,8 +782,9 @@ def graphics3dbox(self, elements=None, **options) -> str: add_conversion_fn(Graphics3DBox, graphics3dbox) -def tag_box(self, **options): - return lookup_conversion_method(self.boxed, "latex")(self.boxed, **options) +def tag_and_form_box(box: BoxExpression, **options): + return lookup_conversion_method(box.boxes, "latex")(box.boxes, **options) -add_conversion_fn(TagBox, tag_box) +add_conversion_fn(FormBox, tag_and_form_box) +add_conversion_fn(TagBox, tag_and_form_box) diff --git a/mathics/format/render/mathml.py b/mathics/format/render/mathml.py index d48a2eb6f..70c641e4c 100644 --- a/mathics/format/render/mathml.py +++ b/mathics/format/render/mathml.py @@ -1,18 +1,19 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter of Mathics objects as MathML strings. +Mathics3 Graphics3D box rendering to MathML strings. MathML formatting is usually initiated in Mathics via MathMLForm[]. """ import base64 -import html from mathics_scanner.tokeniser import is_symbol_name +from mathics.builtin.box.expression import BoxExpression from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox from mathics.builtin.box.layout import ( + FormBox, FractionBox, GridBox, InterpretationBox, @@ -64,14 +65,14 @@ def encode_mathml(text: str) -> str: } -def string(self, **options) -> str: - text = self.value +def string(s: String, **options) -> str: + text = s.value number_as_text = options.get("number_as_text", None) show_string_characters = ( options.get("System`ShowStringCharacters", None) is SymbolTrue ) - if isinstance(self, BoxElementMixin): + if isinstance(s, BoxElementMixin): if number_as_text is None: number_as_text = SymbolFalse @@ -122,16 +123,16 @@ def render(format, string): add_conversion_fn(String, string) -def interpretation_box(self, **options): - return lookup_conversion_method(self.boxed, "mathml")(self.boxed, **options) +def interpretation_box(box: InterpretationBox, **options): + return lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) add_conversion_fn(InterpretationBox, interpretation_box) -def pane_box(self, **options): - content = lookup_conversion_method(self.boxed, "mathml")(self.boxed, **options) - options = self.box_options +def pane_box(box: PaneBox, **options): + content = lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) + options = box.box_options size = options.get("System`ImageSize", SymbolAutomatic).to_python() if size is SymbolAutomatic: width = "" @@ -167,27 +168,27 @@ def pane_box(self, **options): add_conversion_fn(PaneBox, pane_box) -def fractionbox(self, **options) -> str: - _options = self.box_options.copy() +def fractionbox(box: FractionBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options return "%s %s" % ( - lookup_conversion_method(self.num, "mathml")(self.num, **options), - lookup_conversion_method(self.den, "mathml")(self.den, **options), + lookup_conversion_method(box.num, "mathml")(box.num, **options), + lookup_conversion_method(box.den, "mathml")(box.den, **options), ) add_conversion_fn(FractionBox, fractionbox) -def gridbox(self, elements=None, **box_options) -> str: +def gridbox(box: GridBox, elements=None, **box_options) -> str: def boxes_to_mathml(box, **options): return lookup_conversion_method(box, "mathml")(box, **options) if not elements: - elements = self._elements + elements = box._elements evaluation = box_options.get("evaluation") - items, options = self.get_array(elements, evaluation) + items, options = box.get_array(elements, evaluation) num_fields = max(len(item) if isinstance(item, tuple) else 1 for item in items) attrs = {} @@ -221,67 +222,67 @@ def boxes_to_mathml(box, **options): add_conversion_fn(GridBox, gridbox) -def sqrtbox(self, **options): - _options = self.box_options.copy() +def sqrtbox(box: SqrtBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options - if self.index: + if box.index: return " %s %s " % ( - lookup_conversion_method(self.radicand, "mathml")(self.radicand, **options), - lookup_conversion_method(self.index, "mathml")(self.index, **options), + lookup_conversion_method(box.radicand, "mathml")(box.radicand, **options), + lookup_conversion_method(box.index, "mathml")(box.index, **options), ) - return " %s " % lookup_conversion_method(self.radicand, "mathml")( - self.radicand, **options + return " %s " % lookup_conversion_method(box.radicand, "mathml")( + box.radicand, **options ) add_conversion_fn(SqrtBox, sqrtbox) -def subscriptbox(self, **options): - _options = self.box_options.copy() +def subscriptbox(box: SubscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options return "%s %s" % ( - lookup_conversion_method(self.base, "mathml")(self.base, **options), - lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), + lookup_conversion_method(box.base, "mathml")(box.base, **options), + lookup_conversion_method(box.subindex, "mathml")(box.subindex, **options), ) add_conversion_fn(SubscriptBox, subscriptbox) -def superscriptbox(self, **options): - _options = self.box_options.copy() +def superscriptbox(box: SuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options return "%s %s" % ( - lookup_conversion_method(self.base, "mathml")(self.base, **options), - lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), + lookup_conversion_method(box.base, "mathml")(box.base, **options), + lookup_conversion_method(box.superindex, "mathml")(box.superindex, **options), ) add_conversion_fn(SuperscriptBox, superscriptbox) -def subsuperscriptbox(self, **options): - _options = self.box_options.copy() +def subsuperscriptbox(box: SubsuperscriptBox, **options): + _options = box.box_options.copy() _options.update(options) options = _options options["inside_row"] = True return "%s %s %s" % ( - lookup_conversion_method(self.base, "mathml")(self.base, **options), - lookup_conversion_method(self.subindex, "mathml")(self.subindex, **options), - lookup_conversion_method(self.superindex, "mathml")(self.superindex, **options), + lookup_conversion_method(box.base, "mathml")(box.base, **options), + lookup_conversion_method(box.subindex, "mathml")(box.subindex, **options), + lookup_conversion_method(box.superindex, "mathml")(box.superindex, **options), ) add_conversion_fn(SubsuperscriptBox, subsuperscriptbox) -def rowbox(self, **options) -> str: - _options = self.box_options.copy() +def rowbox(box: RowBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options result = [] @@ -296,16 +297,16 @@ def is_list_interior(content): is_list_row = False if ( - len(self.items) >= 3 - and self.items[0].get_string_value() == "{" - and self.items[2].get_string_value() == "}" - and self.items[1].has_form("RowBox", 1, None) + len(box.items) >= 3 + and box.items[0].get_string_value() == "{" + and box.items[2].get_string_value() == "}" + and box.items[1].has_form("RowBox", 1, None) ): - content = self.items[1].items + content = box.items[1].items if is_list_interior(content): is_list_row = True - if not inside_row and is_list_interior(self.items): + if not inside_row and is_list_interior(box.items): is_list_row = True if is_list_row: @@ -313,7 +314,7 @@ def is_list_interior(content): else: options["inside_row"] = True - for element in self.items: + for element in box.items: result.append(lookup_conversion_method(element, "mathml")(element, **options)) # print(f"mrow: {result}") @@ -324,20 +325,20 @@ def is_list_interior(content): add_conversion_fn(RowBox, rowbox) -def stylebox(self, **options) -> str: - _options = self.box_options.copy() +def stylebox(box: StyleBox, **options) -> str: + _options = box.box_options.copy() _options.update(options) options = _options - return lookup_conversion_method(self.boxes, "mathml")(self.boxes, **options) + return lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) add_conversion_fn(StyleBox, stylebox) -def graphicsbox(self, elements=None, **options) -> str: +def graphicsbox(box: GraphicsBox, elements=None, **options) -> str: # FIXME: SVG is the only thing we can convert MathML into. # Handle other graphics formats. - svg_body = self.boxes_to_svg(elements, **options) + svg_body = box.boxes_to_format("svg", **options) # mglyph, which is what we have been using, is bad because MathML standard changed. # metext does not work because the way in which we produce the svg images is also based on this outdated mglyph @@ -349,8 +350,8 @@ def graphicsbox(self, elements=None, **options) -> str: ) # print(svg_body) mathml = template % ( - int(self.width), - int(self.height), + int(box.boxwidth), + int(box.boxheight), base64.b64encode(svg_body.encode("utf8")).decode("utf8"), ) # print("boxes_to_mathml", mathml) @@ -360,19 +361,19 @@ def graphicsbox(self, elements=None, **options) -> str: add_conversion_fn(GraphicsBox, graphicsbox) -def graphics3dbox(self, elements=None, **options) -> str: +def graphics3dbox(box, elements=None, **options) -> str: """Turn the Graphics3DBox into a MathML string""" - json_repr = self.boxes_to_json(elements, **options) - mathml = f'' - mathml = f"{mathml}" - return mathml + result = box.boxes_to_js(**options) + result = f"{result}" + return result add_conversion_fn(Graphics3DBox, graphics3dbox) -def tag_box(self, **options): - return lookup_conversion_method(self.boxed, "mathml")(self.boxed, **options) +def tag_and_form_box(box: BoxExpression, **options): + return lookup_conversion_method(box.boxes, "mathml")(box.boxes, **options) -add_conversion_fn(TagBox, tag_box) +add_conversion_fn(FormBox, tag_and_form_box) +add_conversion_fn(TagBox, tag_and_form_box) diff --git a/mathics/format/render/svg.py b/mathics/format/render/svg.py index 13a0992af..bd48f4df4 100644 --- a/mathics/format/render/svg.py +++ b/mathics/format/render/svg.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter of Mathics objects as SVG strings. +Mathics3 Graphics box rendering to SVG strings. """ from mathics.builtin.box.graphics import ( + ArcBox, ArrowBox, BezierCurveBox, FilledCurveBox, @@ -13,17 +14,15 @@ PointBox, PolygonBox, RectangleBox, - _ArcBox, - _RoundBox, + RoundBox, ) from mathics.builtin.drawing.graphics3d import Graphics3DElements -from mathics.builtin.graphics import ( - DEFAULT_POINT_FACTOR, +from mathics.builtin.graphics import DEFAULT_POINT_FACTOR, PointSize, _svg_bezier +from mathics.core.formatter import add_conversion_fn, lookup_method +from mathics.format.box.graphics import ( GraphicsElements, - PointSize, - _svg_bezier, + prepare_elements as prepare_elements2d, ) -from mathics.core.formatter import add_conversion_fn, lookup_method class _SVGTransform: @@ -93,16 +92,16 @@ def create_css( return "; ".join(css) -def arcbox(self, **options) -> str: +def arcbox(box: ArcBox, **options) -> str: """ SVG formatting for arc of a circle. """ - if self.arc is None: + if box.arc is None: # We have a doughnut graph and this is the inner blank hole of that. # It is an empty circle - return _roundbox(self) + return roundbox(box) - x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() + x, y, rx, ry, sx, sy, ex, ey, large_arc = box._arc_params() def path(closed): if closed: @@ -116,40 +115,40 @@ def path(closed): if closed: yield "Z" - line_width = self.style.get_line_width(face_element=self.face_element) + line_width = box.style.get_line_width(face_element=box.face_element) style = create_css( - self.edge_color, - self.face_color, + box.edge_color, + box.face_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, - face_opacity=self.face_opacity, + edge_opacity=box.edge_opacity, + face_opacity=box.face_opacity, ) - svg = f"" + svg = f"" # print("_Arcbox: ", svg) return svg -add_conversion_fn(_ArcBox, arcbox) +add_conversion_fn(ArcBox, arcbox) -def arrow_box(self, **options) -> str: - width = self.style.get_line_width(face_element=False) +def arrow_box(box: ArrowBox, **options) -> str: + width = box.style.get_line_width(face_element=False) style = create_css( - self.edge_color, stroke_width=width, edge_opacity=self.edge_opacity + box.edge_color, stroke_width=width, edge_opacity=box.edge_opacity ) - polyline = self.curve.make_draw_svg(style) + polyline = box.curve.make_draw_svg(style) - arrow_style = create_css(face_color=self.edge_color, stroke_width=width) + arrow_style = create_css(face_color=box.edge_color, stroke_width=width) def polygon(points): yield '' - extent = self.graphics.view_width or 0 - default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("svg", _SVGTransform) - svg = "\n".join(self._draw(polyline, default_arrow, custom_arrow, extent)) + extent = box.graphics.view_width or 0 + default_arrow = box._default_arrow(polygon) + custom_arrow = box._custom_arrow("svg", _SVGTransform) + svg = "\n".join(box._draw(polyline, default_arrow, custom_arrow, extent)) # print("ArrowBox: ", svg) return svg @@ -157,19 +156,19 @@ def polygon(points): add_conversion_fn(ArrowBox, arrow_box) -def bezier_curve_box(self, **options) -> str: +def bezier_curve_box(box: BezierCurveBox, **options) -> str: """ SVG formatter for BezierCurveBox. """ - line_width = self.style.get_line_width(face_element=False) + line_width = box.style.get_line_width(face_element=False) style = create_css( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, + edge_opacity=box.edge_opacity, ) svg = "\n" - for line in self.lines: - s = "\n".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line]))) + for line in box.lines: + s = "\n".join(_svg_bezier((box.spline_degree, [xy.pos() for xy in line]))) svg += f'' # print("BezierCurveBox: ", svg) return svg @@ -178,7 +177,7 @@ def bezier_curve_box(self, **options) -> str: add_conversion_fn(BezierCurveBox, bezier_curve_box) -def density_plot_box(self, **options): +def density_plot_box(box, **options): """ SVG formatter for DensityPlotBox. """ @@ -203,9 +202,9 @@ def density_plot_box(self, **options): # to go from the center to each of the (square) sides. svg_data = [""] - for index, triangle_coords in enumerate(self.lines): + for index, triangle_coords in enumerate(box.lines): triangle = [coords.pos() for coords in triangle_coords] - colors = [rgb.to_js() for rgb in self.vertex_colors[index]] + colors = [rgb.to_js() for rgb in box.vertex_colors[index]] r = (colors[0][0] + colors[1][0] + colors[2][0]) / 3 g = (colors[0][1] + colors[1][1] + colors[2][1]) / 3 b = (colors[0][2] + colors[1][2] + colors[2][1]) / 3 @@ -222,21 +221,21 @@ def density_plot_box(self, **options): # No add_conversion_fn since this is a hacken-on polygonbox -def filled_curve_box(self, **options): - line_width = self.style.get_line_width(face_element=False) +def filled_curve_box(box: FilledCurveBox, **options): + line_width = box.style.get_line_width(face_element=False) style = create_css( - edge_color=self.edge_color, face_color=self.face_color, stroke_width=line_width + edge_color=box.edge_color, face_color=box.face_color, stroke_width=line_width ) style = create_css( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, - face_opacity=self.edge_opacity, + edge_opacity=box.edge_opacity, + face_opacity=box.edge_opacity, ) def components(): - for component in self.components: + for component in box.components: transformed = [(k, [xy.pos() for xy in p]) for k, p in component] yield " ".join(_svg_bezier(*transformed)) + " Z" @@ -250,7 +249,7 @@ def components(): add_conversion_fn(FilledCurveBox, filled_curve_box) -def graphics_box(self, elements=None, **options: dict) -> str: +def graphics_box(box: GraphicsBox, elements=None, **options: dict) -> str: """ Top-level SVG routine takes ``elements`` and ``options`` and turns this into a SVG string, including the .. tag. @@ -266,10 +265,11 @@ def graphics_box(self, elements=None, **options: dict) -> str: ``evaluation``: an ``Evaluation`` object that can be used when further evaluation is needed. """ - if not elements: - elements = self._elements + assert elements is None + elements = box.content data = options.get("data", None) + assert data is None if data: ( elements, @@ -277,27 +277,27 @@ def graphics_box(self, elements=None, **options: dict) -> str: xmax, ymin, ymax, - self.boxwidth, - self.boxheight, + box.boxwidth, + box.boxheight, width, height, ) = data else: - elements, calc_dimensions = self._prepare_elements( - elements, options, neg_y=True + elements, calc_dimensions = prepare_elements2d( + box, elements, options, neg_y=True ) ( xmin, xmax, ymin, ymax, - self.boxwidth, - self.boxheight, + box.boxwidth, + box.boxheight, width, height, ) = calc_dimensions() - elements.view_width = self.boxwidth + elements.view_width = box.boxwidth format_fn = lookup_method(elements, "svg") if format_fn is not None: @@ -305,17 +305,26 @@ def graphics_box(self, elements=None, **options: dict) -> str: else: svg_body = elements.to_svg(**options) - self.boxwidth = options.get("width", self.boxwidth) - self.boxheight = options.get("height", self.boxheight) + boxwidth = options.get("width", box.boxwidth) + boxheight = options.get("height", box.boxheight) + + assert isinstance( + boxwidth, (int, float) + ), f"boxwidth {boxwidth} should be 'int' or 'float'. is {type(boxwidth)}" + assert isinstance( + boxheight, (int, float) + ), f"boxwidth {boxheight} should be 'int' or 'float'. is {type(boxheight)}" + box.boxwidth = boxwidth + box.boxheight = boxheight - tooltip_text = self.tooltip_text if hasattr(self, "tooltip_text") else "" - if self.background_color is not None: + tooltip_text = box.tooltip_text or "" + if box.background_color is not None: # FIXME: tests don't seem to cover this section of code. # Wrap svg_elements in a rectangle background = "rgba(100%,100%,100%,100%)" - if self.background_color: - components = self.background_color.to_rgba() + if box.background_color: + components = box.background_color.to_rgba() if len(components) == 3: background = "rgb(" + ", ".join(f"{100*c}%" for c in components) + ")" else: @@ -324,8 +333,8 @@ def graphics_box(self, elements=None, **options: dict) -> str: svg_body = f""" {tooltip_text} {svg_body} """ @@ -333,7 +342,7 @@ def graphics_box(self, elements=None, **options: dict) -> str: if options.get("noheader", False): return svg_body - svg_main = wrap_svg_body(self.boxwidth, self.boxheight, xmin, ymin, svg_body) + svg_main = wrap_svg_body(box.boxwidth, box.boxheight, xmin, ymin, svg_body) # print("svg_main", svg_main) return svg_main # , width, height @@ -341,12 +350,12 @@ def graphics_box(self, elements=None, **options: dict) -> str: add_conversion_fn(GraphicsBox, graphics_box) -def graphics_elements(self, **options) -> str: +def graphics_elements(box: GraphicsElements, **options) -> str: """ - SVG formatting on a list of graphics elements. + SVG formatting on a GraphicsElementBox which may contain other GraphicsElementBox's. """ result = [""] - for element in self.elements: + for element in box.elements: try: format_fn = lookup_method(element, "svg") except Exception: @@ -370,45 +379,45 @@ def graphics_elements(self, **options) -> str: add_conversion_fn(Graphics3DElements) -def inset_box(self, **options) -> str: +def inset_box(box: InsetBox, **options) -> str: """ SVG formatting for boxing an Inset in a graphic. """ - x, y = self.pos.pos() + x, y = box.pos.pos() offset = options.get("offset", None) if offset is not None: x = x + offset[0] y = y + offset[1] - if hasattr(self.content, "to_svg"): - content = self.content.to_svg(noheader=True, offset=(x, y)) + if hasattr(box.content, "to_svg"): + content = box.content.to_svg(noheader=True, offset=(x, y)) svg = "\n" + content + "\n" else: css_style = create_css( - font_color=self.color, - edge_color=self.color, - face_color=self.color, + font_color=box.color, + edge_color=box.color, + face_color=box.color, stroke_width=0.2, - opacity=self.opacity.opacity, + opacity=box.opacity.opacity, ) - text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"' + text_pos_opts = f'x="{x}" y="{y}" ox="{box.opos[0]}" oy="{box.opos[1]}"' alignment = " dominant-baseline:hanging;" - if hasattr(self, "alignment"): - if self.alignment == "bottom": + if hasattr(box, "alignment"): + if box.alignment == "bottom": # This is typically done for labels under the x axis. alignment = " dominant-baseline:hanging; text-anchor:middle;" - elif self.alignment == "left": + elif box.alignment == "left": # This is typically done for labels to the left of the y axis. alignment = " dominant-baseline:middle; text-anchor:end;" # FIXME: don't hard code text_style_opts, but allow these to be adjustable. text_style_opts = alignment - content = self.content.boxes_to_text(evaluation=self.graphics.evaluation) + content = box.content.boxes_to_text(evaluation=box.graphics.evaluation) font_size = f'''font-size="{options.get("point_size", "10px")}"''' svg = f'{content}' - # content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation) - # style = create_css(font_color=self.color) + # content = box.content.boxes_to_mathml(evaluation=box.graphics.evaluation) + # style = create_css(font_color=box.color) # svg = ( # '' # "%s") @@ -420,12 +429,12 @@ def inset_box(self, **options) -> str: add_conversion_fn(InsetBox, inset_box) -def line_box(self, **options) -> str: - line_width = self.style.get_line_width(face_element=False) +def line_box(box: LineBox, **options) -> str: + line_width = box.style.get_line_width(face_element=False) style = create_css( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, + edge_opacity=box.edge_opacity, ) # The following line options come from looking at SVG produced from WMA. @@ -434,7 +443,7 @@ def line_box(self, **options) -> str: style += "; stroke-linecap:square; stroke-linejoin:miter; stroke-miterlimit:3.25" svg = "\n" - for line in self.lines: + for line in box.lines: svg += '' % ( " ".join(["%f,%f" % coords.pos() for coords in line]), style, @@ -446,18 +455,18 @@ def line_box(self, **options) -> str: add_conversion_fn(LineBox, line_box) -def pointbox(self, **options) -> str: - point_size, _ = self.style.get_style(PointSize, face_element=False) +def pointbox(box: PointBox, **options) -> str: + point_size, _ = box.style.get_style(PointSize, face_element=False) if point_size is None: - point_size = PointSize(self.graphics, value=DEFAULT_POINT_FACTOR) + point_size = PointSize(box.graphics, value=DEFAULT_POINT_FACTOR) size = point_size.get_absolute_size() style = create_css( - edge_color=self.edge_color, + edge_color=box.edge_color, stroke_width=0, - face_color=self.face_color, - edge_opacity=self.edge_opacity, - face_opacity=self.face_opacity, + face_color=box.face_color, + edge_opacity=box.edge_opacity, + face_opacity=box.face_opacity, ) # The following line options come from looking at SVG produced from WMA. @@ -465,7 +474,7 @@ def pointbox(self, **options) -> str: style += "; fill-rule:even-odd" svg = "" - for line in self.lines: + for line in box.lines: for coords in line: svg += f""" str: add_conversion_fn(PointBox) -def polygonbox(self, **options): +def polygonbox(box: PolygonBox, **options): """ SVG formatter for PolygonBox """ - line_width = self.style.get_line_width(face_element=True) + line_width = box.style.get_line_width(face_element=True) # Hack alert. Currently we encode density plots as a polygon box where # each polygon is a triangle with a color. We know we have this case because - # self.vertex_colors is not empty here. - if self.vertex_colors: - return density_plot_box(self, **options) + # box.vertex_colors is not empty here. + if box.vertex_colors: + return density_plot_box(box, **options) style = create_css( - edge_color=self.edge_color, - face_color=self.face_color, + edge_color=box.edge_color, + face_color=box.face_color, stroke_width=line_width, - edge_opacity=self.edge_opacity, - face_opacity=self.face_opacity, + edge_opacity=box.edge_opacity, + face_opacity=box.face_opacity, ) svg = "\n" @@ -505,7 +514,7 @@ def polygonbox(self, **options): # Perhaps one day we will find it useful to have other fill_rules specified as an option. fill_rule = "evenodd" - for line in self.lines: + for line in box.lines: svg += f""" ' - # print("_RoundBox: ", svg) + # print("RoundBox: ", svg) return svg -add_conversion_fn(_RoundBox) +add_conversion_fn(RoundBox) def wrap_svg_body( diff --git a/mathics/format/render/text.py b/mathics/format/render/text.py index 59e9236c2..a62f201cb 100644 --- a/mathics/format/render/text.py +++ b/mathics/format/render/text.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- """ -Lower-level formatter Mathics objects as plain text. +Mathics3 box rendering to plain text. """ from mathics.builtin.box.graphics import GraphicsBox from mathics.builtin.box.graphics3d import Graphics3DBox from mathics.builtin.box.layout import ( + FormBox, FractionBox, GridBox, InterpretationBox, @@ -23,6 +24,8 @@ from mathics.core.exceptions import BoxConstructError from mathics.core.formatter import add_conversion_fn, lookup_method from mathics.core.symbols import Atom, SymbolTrue +from mathics.format.box.graphics import prepare_elements as prepare_elements2d +from mathics.format.box.graphics3d import prepare_elements as prepare_elements3d from mathics.format.form.util import _WrongFormattedExpression, text_cells_to_grid @@ -30,8 +33,8 @@ def boxes_to_text(boxes, **options) -> str: return lookup_method(boxes, "text")(boxes, **options) -def string(self, **options) -> str: - value = self.value +def string(s: String, **options) -> str: + value = s.value show_string_characters = ( options.get("System`ShowStringCharacters", None) is SymbolTrue ) @@ -45,14 +48,14 @@ def string(self, **options) -> str: def interpretation_box(self, **options): - return boxes_to_text(self.boxed, **options) + return boxes_to_text(self.boxes, **options) add_conversion_fn(InterpretationBox, interpretation_box) def pane_box(self, **options): - result = boxes_to_text(self.boxed, **options) + result = boxes_to_text(self.boxes, **options) return result @@ -180,6 +183,8 @@ def rowbox(self, elements=None, **options) -> str: _options.update(options) options = _options parts_str = [boxes_to_text(element, **options) for element in self.items] + if len(parts_str) == 0: + return "" if len(parts_str) == 1: return parts_str[0] # This loop integrate all the row adding spaces after a ",", followed @@ -216,10 +221,9 @@ def stylebox(self, **options) -> str: def graphicsbox(self, elements=None, **options) -> str: - if not elements: - elements = self._elements + assert elements is None - self._prepare_elements(elements, options) # to test for Box errors + prepare_elements2d(self, self.content, options) # to test for Box errors return "-Graphics-" @@ -227,16 +231,18 @@ def graphicsbox(self, elements=None, **options) -> str: def graphics3dbox(self, elements=None, **options) -> str: - if not elements: - elements = self._elements + assert elements is None + + prepare_elements3d(self, self.content, options) # to test for Box errors return "-Graphics3D-" add_conversion_fn(Graphics3DBox, graphics3dbox) -def tag_box(self, **options): - return boxes_to_text(self.boxed, **options) +def tag_and_form_box(self, **options): + return boxes_to_text(self.boxes, **options) -add_conversion_fn(TagBox, tag_box) +add_conversion_fn(FormBox, tag_and_form_box) +add_conversion_fn(TagBox, tag_and_form_box) diff --git a/mathics/interrupt.py b/mathics/interrupt.py index df70e8039..c47470565 100644 --- a/mathics/interrupt.py +++ b/mathics/interrupt.py @@ -41,7 +41,10 @@ def inspect_eval_loop(evaluation: Evaluation): query, source_code = evaluation.parse_feeder_returning_code(shell) # show_echo(source_code, evaluation) if len(source_code) and source_code[0] == "!" and shell is not None: - subprocess.run(source_code[1:], shell=True) + if not settings.ENABLE_SYSTEM_COMMANDS: + evaluation.message("Run", "dis") + else: + subprocess.run(source_code[1:], shell=True) if shell.definitions is not None: shell.definitions.increment_line_no(1) continue @@ -90,7 +93,10 @@ def Mathics3_interrupt_handler( print_fn("continuing") break elif user_input in ("debugger", "d"): - breakpoint() + if not settings.ENABLE_SYSTEM_COMMANDS: + print_fn("Execution of external commands is disabled.") + else: + breakpoint() elif user_input in ("exit", "quit"): print_fn("Mathics3 exited because of an interrupt.") sys.exit(3) diff --git a/mathics/session.py b/mathics/session.py index af4ef54ee..607319e6a 100644 --- a/mathics/session.py +++ b/mathics/session.py @@ -115,7 +115,7 @@ def __init__( # the formats must be already loaded. # The need of importing this module here seems # to be related to an issue in the modularity design. - import mathics.format + import mathics.format.render if character_encoding is not None: mathics.settings.SYSTEM_CHARACTER_ENCODING = character_encoding diff --git a/mathics/settings.py b/mathics/settings.py index 288f1cf09..65190cebc 100644 --- a/mathics/settings.py +++ b/mathics/settings.py @@ -4,6 +4,7 @@ Some of the values can be adjusted via Environment Variables. """ + import os import os.path as osp import sys @@ -94,6 +95,23 @@ def get_srcdir(): # users to access local files. ENABLE_FILES_MODULE = True +# Leave this True unless you have specific reason for not permitting +# users to execute system commands. +# If MATHICS3_SANDBOX environment variable is set, this defaults to False. +ENABLE_SYSTEM_COMMANDS = ( + os.environ.get( + "MATHICS3_ENABLE_SYSTEM_COMMANDS", + str( + not ( + os.environ.get("MATHICS3_SANDBOX") + or sys.platform in ("emscripten", "wasi") + ) + ), + ).lower() + == "true" +) + + # Rocky: this is probably a hack. LoadModule[] needs to handle # whatever it is that setting this thing did. default_pymathics_modules: List[str] = [] diff --git a/setup.py b/setup.py index 0998ddeeb..1bba72e84 100644 --- a/setup.py +++ b/setup.py @@ -109,21 +109,15 @@ def get_srcdir(): class build_py(setuptools_build_py): def run(self): - if not os.path.exists("mathics/data/op-tables.json"): + if not os.path.exists("mathics/data/character-tables.json"): os.system( - "mathics3-generate-json-table" - " --field=ascii-operator-to-symbol" - " --field=ascii-operator-to-unicode" - " --field=ascii-operator-to-wl-unicode" - " --field=operator-to-ascii" - " --field=operator-to-unicode" - " -o mathics/data/op-tables.json" + "mathics3-generate-json-table" " -o mathics/data/character-tables.json" ) if not os.path.exists("mathics/data/operator-tables.json"): os.system( "mathics3-generate-operator-json-table" " -o operator-tables.json" ) - self.distribution.package_data["mathics"].append("data/op-tables.json") + self.distribution.package_data["mathics"].append("data/character-tables.json") setuptools_build_py.run(self) diff --git a/test/builtin/atomic/test_symbols.py b/test/builtin/atomic/test_symbols.py index ec44b7974..2a86f4f7b 100644 --- a/test/builtin/atomic/test_symbols.py +++ b/test/builtin/atomic/test_symbols.py @@ -47,7 +47,7 @@ def test_downvalues(): ( "Information[f]", tuple(), - "f[x] returns the square of x\n\nf[x_] = x ^ 2\n\ng[f] ^= 2\n", + "f[x] returns the square of x\n\nf[x_] = x^2\n\ng[f] ^= 2\n", None, ), ('Length[Names["System`*"]] > 350', None, "True", None), diff --git a/test/builtin/box/test_custom_boxexpression.py b/test/builtin/box/test_custom_boxexpression.py index d3b36fdcb..aaac0e8d2 100644 --- a/test/builtin/box/test_custom_boxexpression.py +++ b/test/builtin/box/test_custom_boxexpression.py @@ -6,6 +6,7 @@ from mathics.core.builtin import Predefined from mathics.core.evaluation import Evaluation from mathics.core.expression import Expression +from mathics.core.rules import BaseRule, FunctionApplyRule, Rule from mathics.core.symbols import Symbol SymbolCustomGraphicsBox = Symbol("CustomGraphicsBox") @@ -42,12 +43,28 @@ class CustomAtom(Predefined): "N[System`CustomAtom]": "37", } - def eval_to_boxes(self, evaluation): - "System`MakeBoxes[System`CustomAtom, StandardForm|TraditionalForm|OutputForm]" + # Since this is a Mathics3 Module which is loaded after + # the core symbols are loaded, it is safe to assume that `MakeBoxes` + # definition was already loaded. We can add then rules to it. + # This modified `contribute` method do that, adding specific + # makeboxes rules for this kind of atoms. + def contribute(self, definitions, is_pymodule=True): + super().contribute(definitions, is_pymodule) + # Add specific MakeBoxes rules + name = self.get_name() + + for pattern, function in self.get_functions("makeboxes_"): + mb_rule = FunctionApplyRule( + name, pattern, function, None, attributes=None, system=True + ) + definitions.add_format("System`MakeBoxes", mb_rule, "_MakeBoxes") + + def makeboxes_general(self, evaluation): + "System`MakeBoxes[System`CustomAtom, StandardForm|TraditionalForm]" return CustomBoxExpression(evaluation=evaluation) - def eval_to_boxes_inputform(self, evaluation): - "System`MakeBoxes[InputForm[System`CustomAtom], StandardForm|TraditionalForm|OutputForm]" + def makeboxes_inputform(self, evaluation): + "System`MakeBoxes[InputForm[System`CustomAtom], StandardForm|TraditionalForm]" return CustomBoxExpression(evaluation=evaluation) @@ -57,6 +74,22 @@ class CustomGraphicsBox(BoxExpression): options = GRAPHICS_OPTIONS attributes = A_HOLD_ALL | A_PROTECTED | A_READ_PROTECTED + # Since this is a Mathics3 Module which is loaded after + # the core symbols are loaded, it is safe to assume that `MakeBoxes` + # definition was already loaded. We can add then rules to it. + # This modified `contribute` method do that, adding specific + # makeboxes rules for this kind of BoxExpression. + def contribute(self, definitions, is_pymodule=True): + super().contribute(definitions, is_pymodule) + # Add specific MakeBoxes rules + name = self.get_name() + + for pattern, function in self.get_functions("makeboxes_"): + mb_rule = FunctionApplyRule( + name, pattern, function, None, attributes=None, system=True + ) + definitions.add_format("System`MakeBoxes", mb_rule, "_MakeBoxes") + def init(self, *elems, **options): self._elements = elems self.evaluation = options.pop("evaluation", None) @@ -65,16 +98,15 @@ def init(self, *elems, **options): def to_expression(self): return Expression(SymbolCustomGraphicsBox, *self.elements) - def eval_box(self, expr, evaluation: Evaluation, options: dict): + def makeboxes_graphics(self, expr, evaluation: Evaluation, options: dict): """System`MakeBoxes[System`Graphics[System`expr_, System`OptionsPattern[System`Graphics]], - System`StandardForm|System`TraditionalForm|System`OutputForm]""" + System`StandardForm|System`TraditionalForm]""" instance = CustomGraphicsBox(*(expr.elements), evaluation=evaluation) return instance - def eval_box_outputForm(self, expr, evaluation: Evaluation, options: dict): + def makeboxes_outputForm(self, expr, evaluation: Evaluation, options: dict): """System`MakeBoxes[System`OutputForm[System`Graphics[System`expr_, System`OptionsPattern[System`Graphics]]], System`StandardForm|System`TraditionalForm]""" - print("MakeBoxes OutputForm") instance = CustomGraphicsBox(*(expr.elements), evaluation=evaluation) return instance diff --git a/test/builtin/drawing/fonts.py b/test/builtin/drawing/fonts.py index 078db2ec5..da2d5a9c7 100644 --- a/test/builtin/drawing/fonts.py +++ b/test/builtin/drawing/fonts.py @@ -8,13 +8,13 @@ SVG_NS = "http://www.w3.org/2000/svg" ET.register_namespace("", SVG_NS) -css = f""" -text, tspan, * {{ +css = """ +text, tspan, * { font-family: "Noto Sans" !important; font-size: 10px !important; font-style: normal !important; font-weight: regular !important; -}} +} """.strip() diff --git a/test/builtin/drawing/test_image.py b/test/builtin/drawing/test_image.py index c7e52b74e..b6c94ca91 100644 --- a/test/builtin/drawing/test_image.py +++ b/test/builtin/drawing/test_image.py @@ -56,8 +56,8 @@ reason="Test doesn't work in a when scikit-image is not installed", ) @pytest.mark.skipif( - os.getenv("SANDBOX", False), - reason="Test doesn't work in a sandboxed environment with access to local files", + os.getenv("MATHICS3_SANDBOX"), + reason="Files module is disabled in sandbox mode", ) @pytest.mark.parametrize(("str_expr, str_expected, msg"), image_tests) def test_image(str_expr: str, str_expected: str, msg: str, message=""): diff --git a/test/builtin/drawing/test_plot_detail.py b/test/builtin/drawing/test_plot_detail.py index 416f60fc8..7b98717d1 100644 --- a/test/builtin/drawing/test_plot_detail.py +++ b/test/builtin/drawing/test_plot_detail.py @@ -253,7 +253,7 @@ def one_test(name: str, str_expr: str, vec: bool, svg: bool, opts: str): boxed_expr = Expression(Symbol("System`ToBoxes"), act_expr).evaluate( session.evaluation ) - act_svg = boxed_expr.boxes_to_svg() + act_svg = boxed_expr.boxes_to_format("svg") act_svg = outline_svg( act_svg, precision=2, include_text=True, include_tail=True ) @@ -271,7 +271,7 @@ def one_test(name: str, str_expr: str, vec: bool, svg: bool, opts: str): boxed_expr = Expression(Symbol("System`ToBoxes"), act_expr).evaluate( session.evaluation ) - act_svg = boxed_expr.boxes_to_svg() + act_svg = boxed_expr.boxes_to_format("svg") act_svg = inject_font_style(act_svg) cairosvg.svg2png( bytestring=act_svg.encode("utf-8"), @@ -370,7 +370,7 @@ def test_yaml(parms): one_test(**parms) -def do_test_all(fns): +def do_test_all(fns, names=None): # several of these tests failed on pyodide due to apparent differences # in numpy (and/or the blas library backing it) between pyodide and other platforms # including numerical instability, different data types (integer vs real) @@ -378,7 +378,8 @@ def do_test_all(fns): # simpler than these doc_tests if not pyodide: for parms in all_yaml_tests_generator(fns): - one_test(**parms) + if not names or parms["name"] in names: + one_test(**parms) if __name__ == "__main__": @@ -391,7 +392,14 @@ def do_test_all(fns): UPDATE_MODE = args.update try: - do_test_all(args.files) + if args.files: + for fn in args.files: + split = fn.split(":") + fn = split[0] + names = split[1].split(",") if len(split) > 1 else None + do_test_all([fn], names) + else: + do_test_all(YAML_TESTS) except AssertionError as oops: print(oops) print("FAIL") diff --git a/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-hyperboloids-vec.txt b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-hyperboloids-vec.txt new file mode 100644 index 000000000..29130b64c --- /dev/null +++ b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-hyperboloids-vec.txt @@ -0,0 +1,348 @@ +System`Graphics3D + System`List + System`RGBColor + System`Real 1.0 + System`Real 0.690196 + System`Real 0.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 1216×3] + [[-0.17638475 -0.42857143 -1. ] + [-0.14285714 -0.44076332 -1. ] + [-0.14285714 -0.42857143 -0.99441206] + ... + [ 0.10204082 0.45189511 1. ] + [ 0.14285714 0.44076336 -1. ] + [ 0.14285714 0.44076336 1. ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 2252×3] + [[ 3 2 1] + [ 6 5 4] + [ 2 3 7] + ... + [1194 1214 1192] + [1215 1196 1193] + [1197 1216 1194]] + System`RGBColor + System`Real 0.392157 + System`Real 0.560784 + System`Real 1.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 4544×3] + [[-0.09912538 -0.79591837 -1. ] + [-0.06122449 -0.79970846 -1. ] + [-0.06122449 -0.79591837 -0.99684159] + ... + [ 0.02040816 0.80174925 1. ] + [ 0.06122449 0.79970846 -1. ] + [ 0.06122449 0.79970846 1. ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 8764×3] + [[ 3 2 1] + [ 6 5 4] + [ 2 3 7] + ... + [4524 4542 4522] + [4543 4526 4523] + [4527 4544 4524]] + System`RGBColor + System`Real 0.862745 + System`Real 0.14902 + System`Real 0.498039 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 9528×3] + [[-0.38950437 -0.95918367 -1. ] + [-0.3877551 -0.95991254 -1. ] + [-0.3877551 -0.95918367 -0.99927114] + ... + [ 0.34693878 0.97521864 1. ] + [ 0.3877551 0.95991251 -1. ] + [ 0.3877551 0.95991251 1. ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 18656×3] + [[ 3 2 1] + [ 6 5 4] + [ 2 3 7] + ... + [9450 9526 9448] + [9527 9452 9449] + [9453 9528 9450]] + System`RGBColor + System`Real 0.196078 + System`Real 0.588235 + System`Real 0.54902 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 10040×3] + [[-0.76154672 -0.95918367 -1. ] + [-0.75510204 -0.96428571 -1. ] + [-0.75510204 -0.95918367 -0.99489796] + ... + [ 0.71428571 0.99489796 1. ] + [ 0.75510204 0.96428571 -1. ] + [ 0.75510204 0.96428571 1. ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 19568×3] + [[ 3 2 1] + [ 6 5 4] + [ 2 3 7] + ... + [ 9848 10038 9844] + [10039 9850 9846] + [ 9851 10040 9848]] + System`RGBColor + System`Real 0.470588 + System`Real 0.368627 + System`Real 0.941176 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 7256×3] + [[-0.95918367 -1. -0.99562683] + [-0.95918367 -0.96355684 -0.95918367] + [-0.96355684 -0.95918367 -0.95918367] + ... + [ 0.96355687 1. -1. ] + [ 0.96355687 1. 1. ] + [ 1. 0.96355687 1. ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 13840×3] + [[ 3 2 1] + [ 3 1 4] + [ 5 3 4] + ... + [6808 7255 6806] + [7255 7252 6806] + [7252 7251 6806]] + System`RGBColor + System`Real 0.996078 + System`Real 0.380392 + System`Real 0.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 2504×3] + [[-1. -1. -0.80174927] + [-0.9951409 -1. -0.79591837] + [-1. -0.9951409 -0.79591837] + ... + [ 0.99514085 1. 0.79591837] + [ 1. 0.99514085 0.79591837] + [ 1. 1. 0.80174925]] + System`Polygon + System`NumericArray NumericArray[Integer*, 4528×3] + [[ 3 2 1] + [ 2 3 4] + [ 4 3 5] + ... + [2503 2502 2501] + [2501 2502 2500] + [2503 2504 2502]] + System`RGBColor + System`Real 0.0 + System`Real 0.447059 + System`Real 0.698039 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 464×3] + [[-1. -1. -0.46302675] + [-0.98420798 -1. -0.42857143] + [-1. -0.98420798 -0.42857143] + ... + [ 0.98420793 1. 0.42857143] + [ 1. 0.98420793 0.42857143] + [ 1. 1. 0.46302671]] + System`Polygon + System`NumericArray NumericArray[Integer*, 704×3] + [[ 3 2 1] + [ 2 3 4] + [ 4 3 5] + ... + [463 462 461] + [461 462 460] + [463 464 462]] + System`Rule + System`AlignmentPoint + System`Center + System`Rule + System`AspectRatio + System`Integer 1 + System`Rule + System`Axes + System`True + System`Rule + System`AxesEdge + System`Automatic + System`Rule + System`AxesLabel + System`None + System`Rule + System`AxesOrigin + System`Automatic + System`Rule + System`AxesStyle + System`List + System`Rule + System`Background + System`Automatic + System`Rule + System`BaseStyle + System`List + System`Rule + System`BaselinePosition + System`Automatic + System`Rule + System`BoxRatios + System`List + System`Integer 1 + System`Integer 1 + System`Integer 1 + System`Rule + System`BoxStyle + System`List + System`Rule + System`Boxed + System`True + System`Rule + System`ClipPlanes + System`None + System`Rule + System`ClipPlanesStyle + System`Automatic + System`Rule + System`ContentSelectable + System`Automatic + System`Rule + System`ControllerLinking + System`False + System`Rule + System`ControllerPath + System`Automatic + System`Rule + System`CoordinatesToolOptions + System`Automatic + System`Rule + System`Epilog + System`List + System`Rule + System`FaceGrids + System`None + System`Rule + System`FaceGridsStyle + System`List + System`Rule + System`FormatType + System`TraditionalForm + System`Rule + System`Frame + System`False + System`Rule + System`FrameLabel + System`None + System`Rule + System`FrameStyle + System`List + System`Rule + System`FrameTicks + System`Automatic + System`Rule + System`FrameTicksStyle + System`List + System`Rule + System`GridLines + System`None + System`Rule + System`GridLinesStyle + System`List + System`Rule + System`ImageMargins + System`Real 0.0 + System`Rule + System`ImagePadding + System`All + System`Rule + System`ImageSize + System`Automatic + System`Rule + System`LabelStyle + System`List + System`Rule + System`Lighting + System`Automatic + System`Rule + System`LogPlot + System`False + System`Rule + System`Method + System`Automatic + System`Rule + System`PlotLabel + System`None + System`Rule + System`PlotRange + System`List + System`List + System`Real -1.0 + System`Real 1.0 + System`List + System`Real -1.0 + System`Real 1.0 + System`List + System`Real -1.0 + System`Real 1.0 + System`Rule + System`PlotRangeClipping + System`False + System`Rule + System`PlotRangePadding + System`Automatic + System`Rule + System`PlotRegion + System`Automatic + System`Rule + System`PreserveImageOptions + System`Automatic + System`Rule + System`Prolog + System`List + System`Rule + System`RotateLabel + System`True + System`Rule + System`RotationAction + System`Fit + System`Rule + System`SphericalRegion + System`Automatic + System`Rule + System`Ticks + System`Automatic + System`Rule + System`TicksStyle + System`List + System`Rule + System`TouchscreenAutoZoom + System`False + System`Rule + System`ViewAngle + System`Automatic + System`Rule + System`ViewCenter + System`Automatic + System`Rule + System`ViewMatrix + System`Automatic + System`Rule + System`ViewPoint + System`List + System`Real 1.3 + System`Real -2.4 + System`Real 2.0 + System`Rule + System`ViewProjection + System`Automatic + System`Rule + System`ViewRange + System`All + System`Rule + System`ViewVector + System`Automatic + System`Rule + System`ViewVertical + System`List + System`Integer 0 + System`Integer 0 + System`Integer 1 diff --git a/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-mesh-vec.txt b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-mesh-vec.txt new file mode 100644 index 000000000..d08910d55 --- /dev/null +++ b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-mesh-vec.txt @@ -0,0 +1,254 @@ +System`Graphics3D + System`List + System`RGBColor + System`Real 1.0 + System`Real 0.690196 + System`Real 0.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 1656×3] + [[-0.36842105 -0.91520468 -0.15789474] + [-0.41447368 -0.89473684 -0.15789474] + [-0.36842105 -0.89473684 -0.25 ] + ... + [ 0.36842105 0.92690056 -0.05263158] + [ 0.36842105 0.92690056 0.05263158] + [ 0.36842105 0.9152047 0.15789474]] + System`Polygon + System`NumericArray NumericArray[Integer*, 3308×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [1655 1595 1597] + [1655 1597 1656] + [1656 1597 1585]] + System`RGBColor + System`Integer 0 + System`Integer 0 + System`Integer 0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 1656×3] + [[-0.36842105 -0.91520468 -0.15789474] + [-0.41447368 -0.89473684 -0.15789474] + [-0.36842105 -0.89473684 -0.25 ] + ... + [ 0.36842105 0.92690056 -0.05263158] + [ 0.36842105 0.92690056 0.05263158] + [ 0.36842105 0.9152047 0.15789474]] + System`Line + System`NumericArray NumericArray[Integer*, 3×3308×2] + [[[ 3 2] + [ 5 4] + [ 5 1] + ... + [1655 1595] + [1655 1597] + [1656 1597]] + + [[ 2 1] + [ 4 1] + [ 1 2] + ... + [1595 1597] + [1597 1656] + [1597 1585]] + + [[ 1 3] + [ 1 5] + [ 2 5] + ... + [1597 1655] + [1656 1655] + [1585 1656]]] + System`Rule + System`AlignmentPoint + System`Center + System`Rule + System`AspectRatio + System`Integer 1 + System`Rule + System`Axes + System`True + System`Rule + System`AxesEdge + System`Automatic + System`Rule + System`AxesLabel + System`None + System`Rule + System`AxesOrigin + System`Automatic + System`Rule + System`AxesStyle + System`List + System`Rule + System`Background + System`Automatic + System`Rule + System`BaseStyle + System`List + System`Rule + System`BaselinePosition + System`Automatic + System`Rule + System`BoxRatios + System`List + System`Integer 1 + System`Integer 1 + System`Integer 1 + System`Rule + System`BoxStyle + System`List + System`Rule + System`Boxed + System`True + System`Rule + System`ClipPlanes + System`None + System`Rule + System`ClipPlanesStyle + System`Automatic + System`Rule + System`ContentSelectable + System`Automatic + System`Rule + System`ControllerLinking + System`False + System`Rule + System`ControllerPath + System`Automatic + System`Rule + System`CoordinatesToolOptions + System`Automatic + System`Rule + System`Epilog + System`List + System`Rule + System`FaceGrids + System`None + System`Rule + System`FaceGridsStyle + System`List + System`Rule + System`FormatType + System`TraditionalForm + System`Rule + System`Frame + System`False + System`Rule + System`FrameLabel + System`None + System`Rule + System`FrameStyle + System`List + System`Rule + System`FrameTicks + System`Automatic + System`Rule + System`FrameTicksStyle + System`List + System`Rule + System`GridLines + System`None + System`Rule + System`GridLinesStyle + System`List + System`Rule + System`ImageMargins + System`Real 0.0 + System`Rule + System`ImagePadding + System`All + System`Rule + System`ImageSize + System`Automatic + System`Rule + System`LabelStyle + System`List + System`Rule + System`Lighting + System`Automatic + System`Rule + System`LogPlot + System`False + System`Rule + System`Method + System`Automatic + System`Rule + System`PlotLabel + System`None + System`Rule + System`PlotRange + System`List + System`List + System`Real -1.0 + System`Real 1.0 + System`List + System`Real -1.0 + System`Real 1.0 + System`List + System`Real -1.0 + System`Real 1.0 + System`Rule + System`PlotRangeClipping + System`False + System`Rule + System`PlotRangePadding + System`Automatic + System`Rule + System`PlotRegion + System`Automatic + System`Rule + System`PreserveImageOptions + System`Automatic + System`Rule + System`Prolog + System`List + System`Rule + System`RotateLabel + System`True + System`Rule + System`RotationAction + System`Fit + System`Rule + System`SphericalRegion + System`Automatic + System`Rule + System`Ticks + System`Automatic + System`Rule + System`TicksStyle + System`List + System`Rule + System`TouchscreenAutoZoom + System`False + System`Rule + System`ViewAngle + System`Automatic + System`Rule + System`ViewCenter + System`Automatic + System`Rule + System`ViewMatrix + System`Automatic + System`Rule + System`ViewPoint + System`List + System`Real 1.3 + System`Real -2.4 + System`Real 2.0 + System`Rule + System`ViewProjection + System`Automatic + System`Rule + System`ViewRange + System`All + System`Rule + System`ViewVector + System`Automatic + System`Rule + System`ViewVertical + System`List + System`Integer 0 + System`Integer 0 + System`Integer 1 diff --git a/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-sphere-multi-vec.txt b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-sphere-multi-vec.txt new file mode 100644 index 000000000..be2950e9d --- /dev/null +++ b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-sphere-multi-vec.txt @@ -0,0 +1,504 @@ +System`Graphics3D + System`List + System`RGBColor + System`Real 1.0 + System`Real 0.690196 + System`Real 0.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 2156×3] + [[ 0.01020408 -0.58166669 -0.03061224] + [ 0.00816283 -0.58163265 -0.03061224] + [ 0.01020408 -0.58163265 -0.03095241] + ... + [ 0.25510204 0.52271356 0.03061224] + [ 0.01020408 0.58166675 -0.03061224] + [ 0.01020408 0.58166675 0.03061224]] + System`Polygon + System`NumericArray NumericArray[Integer*, 4270×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [2155 2120 2122] + [2155 2122 2156] + [2156 2122 2114]] + System`RGBColor + System`Real 0.392157 + System`Real 0.560784 + System`Real 1.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 5168×3] + [[-0.31632653 -0.95159439 -0.09183673] + [-0.32393319 -0.94897959 -0.09183673] + [-0.31632653 -0.94897959 -0.11275513] + ... + [ 0.29591837 0.96179838 0.03061224] + [ 0.29591837 0.95797185 0.09183673] + [ 0.29591837 0.95031878 0.15306122]] + System`Polygon + System`NumericArray NumericArray[Integer*, 10222×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [5167 4999 5001] + [5167 5001 5168] + [5168 5001 4985]] + System`RGBColor + System`Real 0.862745 + System`Real 0.14902 + System`Real 0.498039 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 7952×3] + [[-0.31632653 -1.25712667 -0.09183673] + [-0.3240569 -1.25510204 -0.09183673] + [-0.31632653 -1.25510204 -0.1130953 ] + ... + [ 0.29591837 1.26490122 0.03061224] + [ 0.29591837 1.26198578 0.09183673] + [ 0.29591837 1.2561549 0.15306122]] + System`Polygon + System`NumericArray NumericArray[Integer*, 15742×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [7951 7770 7772] + [7951 7772 7952] + [7952 7772 7754]] + System`RGBColor + System`Real 0.196078 + System`Real 0.588235 + System`Real 0.54902 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 9972×3] + [[-0.52040816 -1.44369333 -0.09183673] + [-0.53376972 -1.43877551 -0.09183673] + [-0.52040816 -1.43877551 -0.15085006] + ... + [ 0.5 1.45333053 0.03061224] + [ 0.5 1.45077943 0.09183673] + [ 0.5 1.44567747 0.15306122]] + System`Polygon + System`NumericArray NumericArray[Integer*, 19496×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [9965 9685 9686] + [9965 9686 9966] + [9966 9686 9671]] + System`RGBColor + System`Real 0.470588 + System`Real 0.368627 + System`Real 0.941176 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 7796×3] + [[-0.96938776 -1.44060375 -0.15306122] + [-0.97210183 -1.43877551 -0.15306122] + [-0.96938776 -1.43877551 -0.16768712] + ... + [ 0.5 1.48397103 0.76530612] + [ 0.5 1.5 0.73324842] + [ 0.5 1.45080792 0.82653061]] + System`Polygon + System`NumericArray NumericArray[Integer*, 14968×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [7787 7794 7796] + [7796 7524 7518] + [7796 7518 7790]] + System`RGBColor + System`Real 0.996078 + System`Real 0.380392 + System`Real 0.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 5436×3] + [[-1.25510204 -1.44630104 -0.21428571] + [-1.26377145 -1.43877551 -0.21428571] + [-1.25510204 -1.43877551 -0.25943885] + ... + [ 0.5 1.5 -1.10111487] + [ 0.5 1.47634646 1.13265306] + [ 0.5 1.5 1.10111478]] + System`Polygon + System`NumericArray NumericArray[Integer*, 10152×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [5432 5435 5168] + [5429 5435 5432] + [5432 5168 5162]] + System`RGBColor + System`Real 0.0 + System`Real 0.447059 + System`Real 0.698039 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 2952×3] + [[-1.45918367 -1.44916378 -0.39795918] + [-1.46950031 -1.43877551 -0.39795918] + [-1.45918367 -1.43877551 -0.43357635] + ... + [ 0.5 1.49678281 1.37755102] + [ 0.5 1.5 1.37404142] + [ 0.49014313 1.5 1.37755102]] + System`Polygon + System`NumericArray NumericArray[Integer*, 5096×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [2950 2659 2945] + [2659 2660 2945] + [2945 2660 2654]] + System`RGBColor + System`Real 1.0 + System`Real 0.690196 + System`Real 0.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 1112×3] + [[-1.45918367 -1.46449833 -0.8877551 ] + [-1.48472912 -1.43877551 -0.8877551 ] + [-1.45918367 -1.43877551 -0.92891168] + ... + [-0.76530612 1.5 1.49227617] + [-0.75006991 1.5 -1.5 ] + [-0.75006991 1.5 1.5 ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 1868×3] + [[ 3 2 1] + [ 1 2 4] + [ 1 4 5] + ... + [1109 1106 1110] + [1108 1111 1107] + [1110 1112 1109]] + System`RGBColor + System`Real 0.392157 + System`Real 0.560784 + System`Real 1.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 380×3] + [[-1.45918367 -1.47728172 -1.19387755] + [-1.49742435 -1.43877551 -1.19387755] + [-1.45918367 -1.43877551 -1.24008494] + ... + [-1.13265306 1.5 1.48465137] + [-1.11237584 1.5 -1.5 ] + [-1.11237584 1.5 1.5 ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 572×3] + [[ 3 2 1] + [ 1 2 4] + [ 1 4 5] + ... + [377 374 378] + [376 379 375] + [378 380 377]] + System`RGBColor + System`Real 0.862745 + System`Real 0.14902 + System`Real 0.498039 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 52×3] + [[-1.45918367 -1.48751413 -1.43877551] + [-1.5 -1.44641436 -1.43877551] + [-1.5 -1.43877551 -1.44641436] + [-1.45918367 -1.43877551 -1.48751413] + [-1.5 -1.5 -1.38286007] + [-1.45918367 -1.5 -1.42574678] + [-1.5 -1.44641436 1.43877551] + [-1.45918367 -1.5 1.42574676] + [-1.45918367 -1.48751413 1.43877551] + [-1.5 -1.5 1.38286014] + [-1.45918367 -1.43877551 1.48751403] + [-1.5 -1.43877551 1.44641432] + [-1.44643213 -1.43877551 -1.5 ] + [-1.41836735 -1.46625565 -1.5 ] + [-1.41836735 -1.5 -1.46625565] + [-1.44643213 -1.5 -1.43877551] + [-1.44643213 -1.5 1.43877551] + [-1.41836735 -1.5 1.46625558] + [-1.41836735 -1.46625565 1.5 ] + [-1.44643213 -1.43877551 1.5 ] + [-1.38289883 -1.5 -1.5 ] + [-1.38289883 -1.5 1.5 ] + [-1.45918367 -1.42574678 -1.5 ] + [-1.5 -1.38286007 -1.5 ] + [-1.5 -1.38286007 1.5 ] + [-1.45918367 -1.42574678 1.5 ] + [-1.45918367 1.43877551 -1.48751413] + [-1.5 1.38286014 -1.5 ] + [-1.5 1.43877551 -1.44641436] + [-1.45918367 1.42574676 -1.5 ] + [-1.45918367 1.42574676 1.5 ] + [-1.5 1.43877551 1.44641432] + [-1.5 1.38286014 1.5 ] + [-1.45918367 1.43877551 1.48751403] + [-1.44643213 1.43877551 -1.5 ] + [-1.44643213 1.43877551 1.5 ] + [-1.5 1.44641432 -1.43877551] + [-1.45918367 1.48751403 -1.43877551] + [-1.45918367 1.5 -1.42574678] + [-1.5 1.5 -1.38286007] + [-1.45918367 1.48751403 1.43877551] + [-1.5 1.5 1.38286014] + [-1.5 1.44641432 1.43877551] + [-1.45918367 1.5 1.42574676] + [-1.41836735 1.5 -1.46625565] + [-1.41836735 1.46625558 -1.5 ] + [-1.44643213 1.5 -1.43877551] + [-1.44643213 1.5 1.43877551] + [-1.41836735 1.46625558 1.5 ] + [-1.41836735 1.5 1.46625558] + [-1.38289883 1.5 -1.5 ] + [-1.38289883 1.5 1.5 ]] + System`Polygon + System`NumericArray NumericArray[Integer*, 52×3] + [[ 3 2 1] + [ 3 1 4] + [ 6 1 5] + [ 5 1 2] + [ 9 8 7] + [ 7 8 10] + [ 7 12 11] + [ 7 11 9] + [14 13 4] + [14 4 1] + [15 14 1] + [16 15 1] + [16 1 6] + [ 9 17 8] + [20 19 18] + [20 18 17] + [11 20 17] + [ 9 11 17] + [21 14 15] + [19 22 18] + [ 4 23 3] + [ 3 23 24] + [26 11 25] + [25 11 12] + [13 23 4] + [26 20 11] + [29 28 27] + [27 28 30] + [33 32 31] + [31 32 34] + [30 35 27] + [34 36 31] + [27 38 37] + [27 37 29] + [40 37 39] + [39 37 38] + [43 42 41] + [41 42 44] + [41 34 32] + [41 32 43] + [46 45 35] + [35 45 27] + [45 47 27] + [47 38 27] + [38 47 39] + [44 48 41] + [48 50 49] + [48 49 36] + [41 48 36] + [34 41 36] + [46 51 45] + [50 52 49]] + System`Rule + System`AlignmentPoint + System`Center + System`Rule + System`AspectRatio + System`Integer 1 + System`Rule + System`Axes + System`True + System`Rule + System`AxesEdge + System`Automatic + System`Rule + System`AxesLabel + System`None + System`Rule + System`AxesOrigin + System`Automatic + System`Rule + System`AxesStyle + System`List + System`Rule + System`Background + System`Automatic + System`Rule + System`BaseStyle + System`List + System`Rule + System`BaselinePosition + System`Automatic + System`Rule + System`BoxRatios + System`List + System`Integer 1 + System`Integer 1 + System`Integer 1 + System`Rule + System`BoxStyle + System`List + System`Rule + System`Boxed + System`True + System`Rule + System`ClipPlanes + System`None + System`Rule + System`ClipPlanesStyle + System`Automatic + System`Rule + System`ContentSelectable + System`Automatic + System`Rule + System`ControllerLinking + System`False + System`Rule + System`ControllerPath + System`Automatic + System`Rule + System`CoordinatesToolOptions + System`Automatic + System`Rule + System`Epilog + System`List + System`Rule + System`FaceGrids + System`None + System`Rule + System`FaceGridsStyle + System`List + System`Rule + System`FormatType + System`TraditionalForm + System`Rule + System`Frame + System`False + System`Rule + System`FrameLabel + System`None + System`Rule + System`FrameStyle + System`List + System`Rule + System`FrameTicks + System`Automatic + System`Rule + System`FrameTicksStyle + System`List + System`Rule + System`GridLines + System`None + System`Rule + System`GridLinesStyle + System`List + System`Rule + System`ImageMargins + System`Real 0.0 + System`Rule + System`ImagePadding + System`All + System`Rule + System`ImageSize + System`Automatic + System`Rule + System`LabelStyle + System`List + System`Rule + System`Lighting + System`Automatic + System`Rule + System`LogPlot + System`False + System`Rule + System`Method + System`Automatic + System`Rule + System`PlotLabel + System`None + System`Rule + System`PlotRange + System`List + System`List + System`Real -1.5 + System`Real 1.5 + System`List + System`Real -1.5 + System`Real 1.5 + System`List + System`Real -1.5 + System`Real 1.5 + System`Rule + System`PlotRangeClipping + System`False + System`Rule + System`PlotRangePadding + System`Automatic + System`Rule + System`PlotRegion + System`Automatic + System`Rule + System`PreserveImageOptions + System`Automatic + System`Rule + System`Prolog + System`List + System`Rule + System`RotateLabel + System`True + System`Rule + System`RotationAction + System`Fit + System`Rule + System`SphericalRegion + System`Automatic + System`Rule + System`Ticks + System`Automatic + System`Rule + System`TicksStyle + System`List + System`Rule + System`TouchscreenAutoZoom + System`False + System`Rule + System`ViewAngle + System`Automatic + System`Rule + System`ViewCenter + System`Automatic + System`Rule + System`ViewMatrix + System`Automatic + System`Rule + System`ViewPoint + System`List + System`Real 1.3 + System`Real -2.4 + System`Real 2.0 + System`Rule + System`ViewProjection + System`Automatic + System`Rule + System`ViewRange + System`All + System`Rule + System`ViewVector + System`Automatic + System`Rule + System`ViewVertical + System`List + System`Integer 0 + System`Integer 0 + System`Integer 1 diff --git a/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-sphere-vec.txt b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-sphere-vec.txt new file mode 100644 index 000000000..e58a0f62a --- /dev/null +++ b/test/builtin/drawing/test_plot_detail_ref/vec-contourplot3d-sphere-vec.txt @@ -0,0 +1,216 @@ +System`Graphics3D + System`List + System`RGBColor + System`Real 1.0 + System`Real 0.690196 + System`Real 0.0 + System`GraphicsComplex + System`NumericArray NumericArray[Real*, 5040×3] + [[-0.2755102 -0.94903273 -0.15306122] + [-0.27568023 -0.94897959 -0.15306122] + [-0.2755102 -0.94897959 -0.15334464] + ... + [ 0.2755102 0.96051243 0.03061224] + [ 0.2755102 0.9566859 0.09183673] + [ 0.2755102 0.94903284 0.15306122]] + System`Polygon + System`NumericArray NumericArray[Integer*, 10076×3] + [[ 3 2 1] + [ 5 4 1] + [ 5 1 2] + ... + [5039 4937 4939] + [5039 4939 5040] + [5040 4939 4923]] + System`Rule + System`AlignmentPoint + System`Center + System`Rule + System`AspectRatio + System`Integer 1 + System`Rule + System`Axes + System`True + System`Rule + System`AxesEdge + System`Automatic + System`Rule + System`AxesLabel + System`None + System`Rule + System`AxesOrigin + System`Automatic + System`Rule + System`AxesStyle + System`List + System`Rule + System`Background + System`Automatic + System`Rule + System`BaseStyle + System`List + System`Rule + System`BaselinePosition + System`Automatic + System`Rule + System`BoxRatios + System`List + System`Integer 1 + System`Integer 1 + System`Integer 1 + System`Rule + System`BoxStyle + System`List + System`Rule + System`Boxed + System`True + System`Rule + System`ClipPlanes + System`None + System`Rule + System`ClipPlanesStyle + System`Automatic + System`Rule + System`ContentSelectable + System`Automatic + System`Rule + System`ControllerLinking + System`False + System`Rule + System`ControllerPath + System`Automatic + System`Rule + System`CoordinatesToolOptions + System`Automatic + System`Rule + System`Epilog + System`List + System`Rule + System`FaceGrids + System`None + System`Rule + System`FaceGridsStyle + System`List + System`Rule + System`FormatType + System`TraditionalForm + System`Rule + System`Frame + System`False + System`Rule + System`FrameLabel + System`None + System`Rule + System`FrameStyle + System`List + System`Rule + System`FrameTicks + System`Automatic + System`Rule + System`FrameTicksStyle + System`List + System`Rule + System`GridLines + System`None + System`Rule + System`GridLinesStyle + System`List + System`Rule + System`ImageMargins + System`Real 0.0 + System`Rule + System`ImagePadding + System`All + System`Rule + System`ImageSize + System`Automatic + System`Rule + System`LabelStyle + System`List + System`Rule + System`Lighting + System`Automatic + System`Rule + System`LogPlot + System`False + System`Rule + System`Method + System`Automatic + System`Rule + System`PlotLabel + System`None + System`Rule + System`PlotRange + System`List + System`List + System`Real -1.5 + System`Real 1.5 + System`List + System`Real -1.5 + System`Real 1.5 + System`List + System`Real -1.5 + System`Real 1.5 + System`Rule + System`PlotRangeClipping + System`False + System`Rule + System`PlotRangePadding + System`Automatic + System`Rule + System`PlotRegion + System`Automatic + System`Rule + System`PreserveImageOptions + System`Automatic + System`Rule + System`Prolog + System`List + System`Rule + System`RotateLabel + System`True + System`Rule + System`RotationAction + System`Fit + System`Rule + System`SphericalRegion + System`Automatic + System`Rule + System`Ticks + System`Automatic + System`Rule + System`TicksStyle + System`List + System`Rule + System`TouchscreenAutoZoom + System`False + System`Rule + System`ViewAngle + System`Automatic + System`Rule + System`ViewCenter + System`Automatic + System`Rule + System`ViewMatrix + System`Automatic + System`Rule + System`ViewPoint + System`List + System`Real 1.3 + System`Real -2.4 + System`Real 2.0 + System`Rule + System`ViewProjection + System`Automatic + System`Rule + System`ViewRange + System`All + System`Rule + System`ViewVector + System`Automatic + System`Rule + System`ViewVertical + System`List + System`Integer 0 + System`Integer 0 + System`Integer 1 diff --git a/test/builtin/drawing/vec_tests.yaml b/test/builtin/drawing/vec_tests.yaml index 6025a2a56..91c23e367 100644 --- a/test/builtin/drawing/vec_tests.yaml +++ b/test/builtin/drawing/vec_tests.yaml @@ -24,6 +24,40 @@ vec-contourplot-two-equations: vec-contourplot-one-equation-two-branches: expr: ContourPlot[{a b + Sqrt[a^2 + b^2] == a+b}, {a,-10,10}, {b,-10,10}] # +# ContourPlot3D +# +vec-contourplot3d-sphere: + expr: ' + ContourPlot3D[ + x^2 + y^2 == 1 - z^2, + {x,-1.5,1.5}, {y,-1.5,1.5}, {z,-1.5,1.5} + ] + ' +vec-contourplot3d-sphere-multi: + expr: ' + ContourPlot3D[ + x^2 + y^2 + z^2, + {x,-1.5,0.5}, {y,-1.5,1.5}, {z,-1.5,1.5}, + PlotRange->{{-1.5,1.5},Automatic,Automatic}, + Contours->10 + ] + ' +vec-contourplot3d-hyperboloids: + expr: ' + ContourPlot3D[ + x^2 + y^2 - z^2, + {x,-1,1}, {y,-1,1}, {z,-1,1} + ] + ' +vec-contourplot3d-mesh: + expr: ' + ContourPlot3D[ + x^2 + y^2 + z^2 == 1, + {x,-1,1}, {y,-1,1}, {z,-1,1}, + PlotPoints->20, Mesh->Full + ] + ' +# # ComplexPlot # vec-complexplot-poles-zeros: diff --git a/test/builtin/numbers/test_calculus.py b/test/builtin/numbers/test_calculus.py index 1276d0d54..c612697dc 100644 --- a/test/builtin/numbers/test_calculus.py +++ b/test/builtin/numbers/test_calculus.py @@ -234,6 +234,12 @@ def test_Solve(str_expr: str, str_expected: str, expected_messages): ), ("Integrate[sin[x], x]", None, "Integrate[sin[x], x]", None), ("Integrate[x ^ 3.5 + x, x]", None, "x ^ 2 / 2 + 0.222222 x ^ 4.5", None), + ( + 'Integrate[F[a, "x"],{x,"p","q"}]', + None, + "Integrate[F[a, x], {x, p, q}]", + "Integrand cannot be converted to sympy because of the string", + ), ("Integrate[ArcTan(x), x]", None, "x ^ 2 ArcTan / 2", None), ("Integrate[E[x], x]", None, "Integrate[E[x], x]", None), ("Integrate[Exp[-(x/2)^2],{x,-Infinity,+Infinity}]", None, "2 Sqrt[Pi]", None), diff --git a/test/builtin/test_file_operations.py b/test/builtin/test_file_operations.py index 5e9db2845..04c44e95d 100644 --- a/test/builtin/test_file_operations.py +++ b/test/builtin/test_file_operations.py @@ -100,8 +100,8 @@ ], ) @pytest.mark.skipif( - os.getenv("SANDBOX", False), - reason="Test doesn't work in a sandboxed environment with access to local files", + os.getenv("MATHICS3_SANDBOX"), + reason="Files module is disabled in sandbox mode", ) def test_private_doctests_file_properties(str_expr, msgs, str_expected, fail_msg): """file_opertions.file_properties""" diff --git a/test/builtin/test_forms.py b/test/builtin/test_forms.py index 7bc9e5f59..890c988c4 100644 --- a/test/builtin/test_forms.py +++ b/test/builtin/test_forms.py @@ -403,7 +403,7 @@ def test_makeboxes_form(expr, form, head, subhead): ), ], ) -def test_private_doctests_output(str_expr, msgs, str_expected, fail_msg): +def test_output(str_expr, msgs, str_expected, fail_msg): """ """ check_evaluation( str_expr, diff --git a/test/builtin/test_system.py b/test/builtin/test_system.py index c48eefc5f..9868cba7f 100644 --- a/test/builtin/test_system.py +++ b/test/builtin/test_system.py @@ -4,17 +4,36 @@ """ +import os from test.helper import check_evaluation import pytest +from mathics import settings + @pytest.mark.parametrize( ("str_expr", "str_expected", "assert_tag_message"), [ ('MemberQ[$Packages, "System`"]', "True", "$Packages"), - ("Head[$ParentProcessID] == Integer", "True", "$ParentProcessID"), - ("Head[$ProcessID] == Integer", "True", "$ProcessID"), + pytest.param( + "Head[$ParentProcessID] == Integer", + "True", + "$ParentProcessID", + marks=pytest.mark.skipif( + not settings.ENABLE_SYSTEM_COMMANDS, + reason="In sandbox mode, $ParentProcessID returns $Failed", + ), + ), + pytest.param( + "Head[$ProcessID] == Integer", + "True", + "$ProcessID", + marks=pytest.mark.skipif( + not settings.ENABLE_SYSTEM_COMMANDS, + reason="In sandbox mode, $ProcessID returns $Failed", + ), + ), ("Head[$SessionID] == Integer", "True", "$SessionID"), ("Head[$SystemWordLength] == Integer", "True", "$SystemWordLength"), ], @@ -29,3 +48,34 @@ def test_private_doctests_system(str_expr, str_expected, assert_tag_message): hold_expected=True, failure_message=assert_tag_message, ) + + +@pytest.mark.skipif( + settings.ENABLE_SYSTEM_COMMANDS, + reason="These tests are used to a Sandboxed environment", +) +@pytest.mark.parametrize( + "str_expr", + [ + "$CommandLine", + "$MachineName", + "$ParentProcessID", + "$ProcessID", + "$ScriptCommandLine", + "$SystemMemory", + "$UserName", + "Breakpoint[]", + 'Environment["HOME"]', + 'GetEnvironment["HOME"]', + "MemoryAvailable[]", + 'Run["date"]', + 'SetEnvironment["FOO"->"bar"]', + ], +) +def test_sandboxing_system_functions(str_expr): + """ """ + check_evaluation( + str_expr, + str_expected="$Failed", + expected_messages=["Execution of external commands is disabled."], + ) diff --git a/test/doc/test_doctests.py b/test/doc/test_doctests.py index d721001e1..cd1ec562f 100644 --- a/test/doc/test_doctests.py +++ b/test/doc/test_doctests.py @@ -2,6 +2,8 @@ Pytests for the documentation system. Basic functions and classes. """ +from unittest.mock import patch + from mathics.core.evaluation import Message, Print from mathics.core.load_builtin import import_and_load_builtins from mathics.doc.common_doc import MathicsMainDocumentation @@ -92,10 +94,11 @@ def test_create_doctest(): }, }, ] - for index, test_case in enumerate(test_cases): - doctest = DocTest(1, test_case["test"], key) - for property_key, value in test_case["properties"].items(): - if property_key == "result" and value is None: - assert getattr(doctest, property_key) == "" - else: - assert getattr(doctest, property_key) == value + with patch("mathics.settings.ENABLE_SYSTEM_COMMANDS", True): + for index, test_case in enumerate(test_cases): + doctest = DocTest(1, test_case["test"], key) + for property_key, value in test_case["properties"].items(): + if property_key == "result" and value is None: + assert getattr(doctest, property_key) == "" + else: + assert getattr(doctest, property_key) == value diff --git a/test/doc/test_latex.py b/test/doc/test_latex.py index d96d9b6bb..307a5d573 100644 --- a/test/doc/test_latex.py +++ b/test/doc/test_latex.py @@ -83,8 +83,8 @@ def test_load_latex_documentation(): r"\begin{testresult}o\end{testresult}\end{testcase}" ) assert ( - doc_in_section.latex(doc_data)[:40] - ).strip() == "Let\\'s sketch the function\n\\begin{tests}" + doc_in_section.latex(doc_data)[:39] + ).strip() == "Let's sketch the function\n\\begin{tests}" assert ( first_section.latex(doc_data)[:30] ).strip() == "\\section{Curve Sketching}{}" diff --git a/test/format/format_tests-WMA.yaml b/test/format/format_tests-WMA.yaml index f0e35a50e..4a00ee4b1 100644 --- a/test/format/format_tests-WMA.yaml +++ b/test/format/format_tests-WMA.yaml @@ -23,98 +23,118 @@ # because we use both in documentation and in the web interface. # + '"-7.32"': + msg: A String with a number latex: - InputForm: \text{``-7.32''} - OutputForm: \text{-7.32} - StandardForm: \text{-7.32} - TraditionalForm: \text{-7.32} + InputForm: '-7.32' + OutputForm: '-7.32' + StandardForm: '-7.32' + TraditionalForm: '-7.32' mathml: InputForm: -7.32 - OutputForm: -7.32 - StandardForm: -7.32 - TraditionalForm: -7.32 - msg: A String with a number + StandardForm: -7.32 + TraditionalForm: -7.32 text: InputForm: '"-7.32"' OutputForm: "-7.32" StandardForm: '\!\(\*RowBox[{"\"-7.32\""}]\)' TraditionalForm: '\!\(\*FormBox["\"-7.32\"", TraditionalForm]\)' + + '"Hola!"': + msg: A String latex: - InputForm: \text{``Hola!''} + InputForm: \text{Hola!} OutputForm: \text{Hola!} StandardForm: \text{Hola!} TraditionalForm: \text{Hola!} mathml: InputForm: Hola! - OutputForm: Hola! - StandardForm: Hola! - TraditionalForm: Hola! - msg: A String + StandardForm: Hola! + TraditionalForm: Hola! text: InputForm: '"Hola!"' OutputForm: Hola! StandardForm: "\\!\\(\\*RowBox[{\"\\\"Hola!\\\"\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[\"\\\"Hola!\\\"\", TraditionalForm]\\)" + + '"\[Pi] is a trascendental number"': + msg: A String latex: - InputForm: "\\text{``\u03C0 is a trascendental number''}" - OutputForm: "\\text{\u03C0 is a trascendental number}" - StandardForm: "\\text{\u03C0 is a trascendental number}" - TraditionalForm: "\\text{\u03C0 is a trascendental number}" + InputForm: '\text{$\pi $ is a trascendental number}' + OutputForm: '\text{$\pi $ is a trascendental number}' + StandardForm: '\text{$\pi $ is a trascendental number}' + TraditionalForm: '\text{$\pi $ is a trascendental number}' mathml: - InputForm: "\u03C0 is a trascendental number" - OutputForm: "\u03C0 is a trascendental number" - StandardForm: "\u03C0 is a trascendental number" - TraditionalForm: "\u03C0 is a trascendental number" - msg: A String + InputForm: 'π is a trascendental number' + StandardForm: 'π is a trascendental number' + TraditionalForm: 'π is a trascendental number' text: InputForm: "\"\\[Pi] is a trascendental number\"" OutputForm: "\\[Pi] is a trascendental number" StandardForm: "\\!\\(\\*RowBox[{\"\\\"\\[Pi] is a trascendental number\\\"\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[\"\\\"\\[Pi] is a trascendental number\\\"\", TraditionalForm]\\)" + + -1. 10^-5: + msg: small negative real number (>10^-5) latex: InputForm: '-0.00001' OutputForm: '-0.00001' - mathml: {} - msg: small negative real number (>10^-5) + mathml: + InputForm: '-0.00001' + StandardForm: "\n -\n 0.00001\n " + TraditionalForm: "\n -\n 0.00001\n " text: InputForm: '-0.00001' OutputForm: '-0.00001' + + -1. 10^-6: + msg: very small negative real number (<10^-6) latex: - InputForm: -1.*{}^{\wedge}-6 - OutputForm: -1.\times 10^{-6} + InputForm: '\text{-1.*${}^{\wedge}$-6}' + # WMA gives something wrong: this should be the pretty-print + # form, but replace line breaks by escaped bar followed by n. + # OutputForm: '\text{ -6$\backslash $n-1. 10}' mathml: {} - msg: very small negative real number (<10^-6) text: InputForm: -1.*^-6 OutputForm: " -6\n-1. 10" + + -1. 10^5: + msg: large negative real number (<10^6) latex: InputForm: '-100000.' OutputForm: '-100000.' mathml: {} - msg: large negative real number (<10^6) text: InputForm: '-100000.' OutputForm: '-100000.' + + -1. 10^6: + msg: very large negative real number (>10^6) latex: - InputForm: -1.*{}^{\wedge}6 - OutputForm: -1.\times 10^6 - StandardForm: -1.0\text{*${}^{\wedge}$}6 - TraditionalForm: -1.0\times 10^6 - mathml: {} - msg: large negative real number (>10^6) + InputForm: '\text{-1.*${}^{\wedge}$6}' + StandardForm: -1.\times 10^6 + TraditionalForm: -1.\times 10^6 + mathml: + InputForm: '-1.*^6' + StandardForm: "\n -\n 1000000.0\n " + TraditionalForm: "\n -\n 1000000.0\n " text: InputForm: -1.*^6 OutputForm: " 6\n-1. 10" StandardForm: "\\!\\(\\*RowBox[{\"-\", \"1.`*^6\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"-\", \"1.`*^6\"}], TraditionalForm]\\)" + + '-4': + msg: An Integer latex: InputForm: '-4' OutputForm: '-4' @@ -122,362 +142,221 @@ TraditionalForm: '-4' mathml: InputForm: -4 - OutputForm: -4 - StandardForm: -4 - TraditionalForm: -4 - msg: An Integer + StandardForm: "\n -\n 4\n " + TraditionalForm: "\n -\n 4\n " text: InputForm: '-4' OutputForm: '-4' StandardForm: "\\!\\(\\*RowBox[{\"-\", \"4\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"-\", \"4\"}], TraditionalForm]\\)" + + '-4.32': + msg: A MachineReal number latex: InputForm: '-4.32' OutputForm: '-4.32' StandardForm: '-4.32' TraditionalForm: '-4.32' mathml: - InputForm: -4.32 - OutputForm: -4.32 - StandardForm: -4.32 - TraditionalForm: -4.32 - msg: A MachineReal number + InputForm: -4.32 + StandardForm: "\n -\n 4.32\n " + TraditionalForm: "\n -\n 4.32\n " text: InputForm: '-4.32' OutputForm: '-4.32' StandardForm: "\\!\\(\\*RowBox[{\"-\", \"4.32`\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"-\", \"4.32`\"}], TraditionalForm]\\)" + + '-4.326563712`2': + msg: A String with a Precision Real number latex: - InputForm: '-4.33' + InputForm: '-4.3' OutputForm: '-4.3' - StandardForm: '-4.33' - TraditionalForm: '-4.33' + StandardForm: '-4.3' + TraditionalForm: '-4.3' mathml: - InputForm: -4.33 - OutputForm: -4.3 - StandardForm: -4.33 - TraditionalForm: -4.33 - msg: A Real number + InputForm: -4.326563712`2. + StandardForm: "\n -\n 4.3\n " + TraditionalForm: "\n -\n 4.3\n " text: InputForm: "-4.326563712`2." OutputForm: "-4.3" StandardForm: "\\!\\(\\*RowBox[{\"-\", \"4.326563712`2.\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"-\", \"4.326563712`2.\"}], TraditionalForm]\\)" + + -4.32`4: + msg: A PrecisionReal number latex: - InputForm: '-4.32' + InputForm: '-4.320' OutputForm: '-4.320' - StandardForm: '-4.32' - TraditionalForm: '-4.32' + StandardForm: '-4.320' + TraditionalForm: '-4.320' mathml: - InputForm: -4.32 - OutputForm: -4.320 - StandardForm: -4.32 - TraditionalForm: -4.32 - msg: A PrecisionReal number + InputForm: -4.32`4. + StandardForm: "\n -\n 4.32\n " + TraditionalForm: "\n -\n 4.32\n " text: InputForm: -4.32`4. OutputForm: '-4.320' StandardForm: "\\!\\(\\*RowBox[{\"-\", \"4.32`4.\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"-\", \"4.32`4.\"}], TraditionalForm]\\)" + + 1. 10^-5: + msg: small real number (<10^-5) latex: InputForm: '0.00001' OutputForm: '0.00001' mathml: {} - msg: small real number (<10^-5) text: InputForm: '0.00001' OutputForm: '0.00001' + + 1. 10^-6: - latex: - InputForm: 1.*{}^{\wedge}-6 - OutputForm: 1.\times 10^{-6} - mathml: {} msg: very small real number (<10^-6) + latex: + InputForm: '\text{1.*${}^{\wedge}$-6}' + mathml: + InputForm: '0.000001' + StandardForm: '0.000001' text: InputForm: 1.*^-6 OutputForm: " -6\n1. 10" + + 1. 10^5: + msg: large real number (<10^6) latex: InputForm: '100000.' OutputForm: '100000.' StandardForm: '100000.' TraditionalForm: '100000.' mathml: {} - msg: large real number (<10^6) text: InputForm: '100000.' OutputForm: '100000.' StandardForm: "\\!\\(\\*RowBox[{\"100000.`\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[\"100000.`\", TraditionalForm]\\)" + + 1. 10^6: - latex: - InputForm: 1.*{}^{\wedge}6 - OutputForm: 1.\times 10^6 - StandardForm: 1.0\text{*${}^{\wedge}$}6 - TraditionalForm: 1.0\times 10^6 - mathml: {} msg: very large real number (>10^6) + latex: + InputForm: \text{1.*${}^{\wedge}$6} + StandardForm: 1.\times 10^6 + TraditionalForm: 1.\times 10^6 + mathml: + InputForm: '1000000.0' + StandardForm: '1000000.0' text: InputForm: 1.*^6 OutputForm: " 6\n1. 10" StandardForm: "\\!\\(\\*RowBox[{\"1.`*^6\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[\"1.`*^6\", TraditionalForm]\\)" + + 1/(1+1/(1+1/a)): + msg: FractionBox latex: - InputForm: 1/(1 + 1/(1 + 1/a)) - OutputForm: 1\text{ / }\left(1\text{ + }1\text{ / }\left(1\text{ + }1\text{ - / }a\right)\right) + InputForm: '\text{(1 + (1 + a${}^{\wedge}$(-1))${}^{\wedge}$(-1))${}^{\wedge}$(-1)}' StandardForm: \frac{1}{1+\frac{1}{1+\frac{1}{a}}} - TraditionalForm: \frac{1}{1+\frac{1}{1+\frac{1}{a}}} + TraditionalForm: '\frac{1}{\frac{1}{\frac{1}{a}+1}+1}' mathml: - InputForm: - - 1  /  ( 1 -  +  1  /  - ( 1  +  1 -  /  a ) - ) - - Fragile! - OutputForm: - - 1  /  ( 1 -  +  1  /  ( - 1  +  1  /  - a ) ) - - Fragile! - StandardForm: &id001 - - 1 1 + 1 1 - + 1 a - - Fragile! - TraditionalForm: "*id001" - msg: FractionBox + InputForm: '(1 + (1 + a^(-1))^(-1))^(-1)' + StandardForm: "\n 1\n \n 1\n +\n \n 1\n \n 1\n +\n \n 1\n a\n \n \n \n \n " + TraditionalForm: "\n 1\n \n \n 1\n \n \n 1\n a\n \n +\n 1\n \n \n +\n 1\n \n " text: InputForm: "(1 + (1 + a^(-1))^(-1))^(-1)" OutputForm: " 1\n---------\n 1\n1 + -----\n 1\n 1 + -\n a" StandardForm: "\\!\\(\\*FractionBox[\"1\", RowBox[{\"1\", \"+\", FractionBox[\"1\", RowBox[{\"1\", \"+\", FractionBox[\"1\", \"a\"]}]]}]]\\)" TraditionalForm: "\\!\\(\\*FormBox[FractionBox[\"1\", RowBox[{FractionBox[\"1\", RowBox[{FractionBox[\"1\", \"a\"], \"+\", \"1\"}]], \"+\", \"1\"}]], TraditionalForm]\\)" -# Nested Association + + +# Association <|a \[Rule] x, b \[Rule] y, c \[Rule] <|d \[Rule] t|>|>: + msg: Association latex: - InputForm: \text{<$\vert$a -> x, b -> y, c -> <$\vert$d -> t$\vert$>$\vert$>} - OutputForm: \text{<$\vert$}a\text{ -> }x, b\text{ -> }y, c\text{ -> }\text{<$\vert$}d\text{ - -> }t\text{$\vert$>}\text{$\vert$>} - StandardForm: \text{<$\vert$}a \[Rule] x,b \[Rule] y,c \[Rule] \text{<$\vert$}d \[Rule] t\text{$\vert$>}\text{$\vert$>} - TraditionalForm: \text{<$\vert$}a\[Rule]x,b\[Rule]y,c\[Rule]\text{<$\vert$}d\[Rule]t\text{$\vert$>}\text{$\vert$>} + InputForm: '\text{$<|$a -$>$ x, b -$>$ y, c -$>$ $<|$d -$>$ t$|>|>$}' + OutputForm: '\text{$<|$a -$>$ x, b -$>$ y, c -$>$ $<|$d -$>$ t$|>|>$}' + StandardForm: '\unicode{f113}a\to x,b\to y,c\to \unicode{f113}d\to t\unicode{f114}\unicode{f114}' + TraditionalForm: '\unicode{f113}a\to x,b\to y,c\to \unicode{f113}d\to t\unicode{f114}\unicode{f114}' mathml: - InputForm: <| a  ->  - x b  ->  - y c  ->  - <| d  ->  - t |> |> - OutputForm: <| a  ->  - x b  ->  - y c  ->  - <| d  ->  - t |> |> - StandardForm: <| a -> - x , b -> y - , c -> <| d - -> t |> |> - TraditionalForm: "<| a -> - x , b -> y - , c -> <| d - -> t |> |>" - msg: Association + InputForm: "<|a -> x, b -> y, c -> <|d -> t|>|>" + StandardForm: "\n \n \n \n \n a\n \n x\n \n ,\n \n b\n \n y\n \n ,\n \n c\n \n \n \n \n \n d\n \n t\n \n \n \n \n \n \n \n \n " + TraditionalForm: '' text: InputForm: "<|a -> x, b -> y, c -> <|d -> t|>|>" OutputForm: "<|a -> x, b -> y, c -> <|d -> t|>|>" StandardForm: "\\!\\(\\*RowBox[{\"\\[LeftAssociation]\", RowBox[{RowBox[{\"a\", \"\\[Rule]\", \"x\"}], \",\", RowBox[{\"b\", \"\\[Rule]\", \"y\"}], \",\", RowBox[{\"c\", \"\\[Rule]\", RowBox[{\"\\[LeftAssociation]\", RowBox[{\"d\", \"\\[Rule]\", \"t\"}], \"\\[RightAssociation]\"}]}]}], \"\\[RightAssociation]\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"\\[LeftAssociation]\", RowBox[{RowBox[{\"a\", \"\\[Rule]\", \"x\"}], \",\", RowBox[{\"b\", \"\\[Rule]\", \"y\"}], \",\", RowBox[{\"c\", \"\\[Rule]\", RowBox[{\"\\[LeftAssociation]\", RowBox[{\"d\", \"\\[Rule]\", \"t\"}], \"\\[RightAssociation]\"}]}]}], \"\\[RightAssociation]\"}], TraditionalForm]\\)" + + # Association nested 2 Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: + msg: Nested Association latex: - InputForm: \text{<$\vert$a -> x, b -> y, c -> <$\vert$d -> t, e -> u$\vert$>$\vert$>} - OutputForm: \text{<$\vert$}a\text{ -> }x, b\text{ -> }y, c\text{ -> }\text{<$\vert$}d\text{ - -> }t, e\text{ -> }u\text{$\vert$>}\text{$\vert$>} - StandardForm: \text{<$\vert$}a->x,b->y,c->\text{<$\vert$}d->t,e->u\text{$\vert$>}\text{$\vert$>} - TraditionalForm: \text{<$\vert$}a->x,b->y,c->\text{<$\vert$}d->t,e->u\text{$\vert$>}\text{$\vert$>} + InputForm: '\text{$<|$a -$>$ x, b -$>$ y, c -$>$ $<|$d -$>$ t, e -$>$ u$|>|>$}' + OutputForm: '\text{$<|$a -$>$ x, b -$>$ y, c -$>$ $<|$d -$>$ t, e -$>$ u$|>|>$}' + StandardForm: '\unicode{f113}a\to x,b\to y,c\to \unicode{f113}d\to t,e\to u\unicode{f114}\unicode{f114}' + TraditionalForm: '\unicode{f113}a\to x,b\to y,c\to \unicode{f113}d\to t,e\to u\unicode{f114}\unicode{f114}' mathml: - InputForm: <| a  ->  - x b  ->  - y c  ->  - <| d  ->  - t e  ->  - u |> |> - OutputForm: <| a  ->  - x b  ->  - y c  ->  - <| d  ->  - t e  ->  - u |> |> - StandardForm: <| a -> - x , b -> y - , c -> <| d - -> t , e -> - u |> |> - TraditionalForm: <| a -> - x , b -> y - , c -> <| d - -> t , e -> - u |> |> - msg: Nested Association + InputForm: "<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>" + StandardForm: "\n \n \n \n \n a\n \n x\n \n ,\n \n b\n \n y\n \n ,\n \n c\n \n \n \n \n \n \n d\n \n t\n \n ,\n \n e\n \n u\n \n \n \n \n \n \n \n \n \n " text: InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> OutputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> StandardForm: "\\!\\(\\*RowBox[{\"\\[LeftAssociation]\", RowBox[{RowBox[{\"a\", \"\\[Rule]\", \"x\"}], \",\", RowBox[{\"b\", \"\\[Rule]\", \"y\"}], \",\", RowBox[{\"c\", \"\\[Rule]\", RowBox[{\"\\[LeftAssociation]\", RowBox[{RowBox[{\"d\", \"\\[Rule]\", \"t\"}], \",\", RowBox[{\"e\", \"\\[Rule]\", \"u\"}]}], \"\\[RightAssociation]\"}]}]}], \"\\[RightAssociation]\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"\\[LeftAssociation]\", RowBox[{RowBox[{\"a\", \"\\[Rule]\", \"x\"}], \",\", RowBox[{\"b\", \"\\[Rule]\", \"y\"}], \",\", RowBox[{\"c\", \"\\[Rule]\", RowBox[{\"\\[LeftAssociation]\", RowBox[{RowBox[{\"d\", \"\\[Rule]\", \"t\"}], \",\", RowBox[{\"e\", \"\\[Rule]\", \"u\"}]}], \"\\[RightAssociation]\"}]}]}], \"\\[RightAssociation]\"}], TraditionalForm]\\)" + + +# Complex Complex[1.09*^12, 3.]: + msg: Complex number latex: - InputForm: 1.09*{}^{\wedge}12 + 3.*I - OutputForm: 1.09\times 10^{12}\text{ + }3. I - StandardForm: 1.09\text{*${}^{\wedge}$}12+3. I - TraditionalForm: 1.09\times 10^{12}+3. I + InputForm: \text{1.09*${}^{\wedge}$12 + 3.*I} + StandardForm: 1.09\times 10^{12}+3. i + TraditionalForm: 1.09\times 10^{12}+3. i mathml: - InputForm: 1.09 *^ 12 -  +  3. * I - OutputForm: "1.09 \xD7 10\ - \ 12  +  3.  \ - \ I" - StandardForm: 1.09 *^ 12 - + 3.   I - TraditionalForm: "1.09 \xD7 10\ - \ 12 + 3. \u2062 I" - msg: Complex number + InputForm: 1.09*^12 + 3.*I + StandardForm: "\n \n 1.09\n ×\n \n 10\n 12\n \n \n +\n \n 3.\n \n \n \n " + TraditionalForm: "\n \n 1.09\n ×\n \n 10\n 12\n \n \n +\n \n 3.\n \n \n \n " text: InputForm: 1.09*^12 + 3.*I OutputForm: " 12\n1.09 10 + 3. I" StandardForm: "\\!\\(\\*RowBox[{\"1.09`*^12\", \"+\", RowBox[{\"3.`\", \" \", \"\\[ImaginaryI]\"}]}]\\)" TraditionalForm: "\\!\\(\\*FormBox[RowBox[{\"1.09`*^12\", \"+\", RowBox[{\"3.`\", \" \", \"\\[ImaginaryI]\"}]}], TraditionalForm]\\)" -Graphics[{Text[a^b,{0,0}]}]: - latex: - InputForm: \text{Graphics[\{Text[a${}^{\wedge}$b, \{0, 0\}]\}]} - OutputForm: ' - - \begin{asy} - - usepackage("amsmath"); - - size(4.9cm, 5.8333cm); - - - // InsetBox - - label("$a^b$", (147.0,175.0), align=SW, rgb(0, 0, 0)+fontsize(3)); - - - clip(box((136.5,162.5), (157.5,187.5))); - - - \end{asy} - - ' - StandardForm: ' - - \begin{asy} - usepackage("amsmath"); - size(4.9cm, 5.8333cm); - - - // InsetBox - - label("$a^b$", (147.0,175.0), align=SW, rgb(0, 0, 0)+fontsize(3)); - - - clip(box((136.5,162.5), (157.5,187.5))); - - - \end{asy} - - ' - TraditionalForm: ' - - \begin{asy} - - usepackage("amsmath"); - - size(4.9cm, 5.8333cm); - - - // InsetBox - - label("$a^b$", (147.0,175.0), align=SW, rgb(0, 0, 0)+fontsize(3)); - - - clip(box((136.5,162.5), (157.5,187.5))); - - - \end{asy} - - ' +Graphics[{Text[a^b,{0,0}]}]: msg: Nontrivial Graphics - Fragile! + latex: + InputForm: '\text{Graphics[$\{$Text[a${}^{\wedge}$b, $\{$0, 0$\}$]$\}$]}' + OutputForm: '\text{-Graphics-}' text: InputForm: Graphics[{Text[a^b, {0, 0}]}] OutputForm: -Graphics- StandardForm: "\\!\\(\\*GraphicsBox[List[InsetBox[FormBox[SuperscriptBox[\"a\", \"b\"], TraditionalForm], List[0, 0]]]]\\)" TraditionalForm: "\\!\\(\\*FormBox[GraphicsBox[List[InsetBox[FormBox[SuperscriptBox[\"a\", \"b\"], TraditionalForm], List[0, 0]]]], TraditionalForm]\\)" -Graphics[{}]: - latex: - InputForm: \text{Graphics[\{\}]} - OutputForm: ' - - \begin{asy} - - usepackage("amsmath"); - - size(5.8333cm, 5.8333cm); - - clip(box((-1,-1), (1,1))); - - - \end{asy} - - ' - StandardForm: ' - - \begin{asy} - - usepackage("amsmath"); - - size(5.8333cm, 5.8333cm); - - - - clip(box((-1,-1), (1,1))); - - - \end{asy} - - ' - TraditionalForm: ' - - \begin{asy} - - usepackage("amsmath"); - - size(5.8333cm, 5.8333cm); - - - - clip(box((-1,-1), (1,1))); - - - \end{asy} - - ' +Graphics[{}]: msg: GraphicsBox - Fragile! + latex: + InputForm: '\text{Graphics[$\{\}$]}' + OutputForm: '\text{-Graphics-}' text: InputForm: Graphics[{}] OutputForm: -Graphics- StandardForm: "\\!\\(\\*GraphicsBox[List[]]\\)" TraditionalForm: "\\!\\(\\*FormBox[GraphicsBox[List[]], TraditionalForm]\\)" + + "Grid[{{\"Spanish\", \"Hola!\"},{\"Portuguese\", \"Ol\\[AGrave]!\"},{\"English\", \"Hi!\"}}]": + msg: Strings in a GridBox latex: InputForm: \text{Grid[\{\{"Spanish", "Hola!"\}, \{"Portuguese", "Olà!"\}, \{"English", "Hi!"\}\}]} OutputForm: "\\begin{array}{cc} \\text{Spanish} & \\text{Hola!}\\\\ \\\ @@ -511,7 +390,6 @@ Graphics[{}]: center\">Ol\xE0!\nEnglishHi!\n\ " - msg: Strings in a GridBox text: InputForm: "Grid[{{\"Spanish\", \"Hola!\"}, {\"Portuguese\", \"Ol\\[AGrave]!\"\ }, {\"English\", \"Hi!\"}}]" @@ -519,7 +397,10 @@ Graphics[{}]: \ Hi!\n" StandardForm: "\\!\\(\\*TagBox[GridBox[{{\"\\\"Spanish\\\"\", \"\\\"Hola!\\\"\"}, {\"\\\"Portuguese\\\"\", \"\\\"Ol\\[AGrave]!\\\"\"}, {\"\\\"English\\\"\", \"\\\"Hi!\\\"\"}}, Rule[AutoDelete, False], Rule[GridBoxItemSize, List[Rule[\"Columns\", List[List[Automatic]]], Rule[\"Rows\", List[List[Automatic]]]]]], \"Grid\"]\\)" TraditionalForm: "\\!\\(\\*FormBox[TagBox[GridBox[{{\"\\\"Spanish\\\"\", \"\\\"Hola!\\\"\"}, {\"\\\"Portuguese\\\"\", \"\\\"Ol\\[AGrave]!\\\"\"}, {\"\\\"English\\\"\", \"\\\"Hi!\\\"\"}}, Rule[AutoDelete, False], Rule[GridBoxItemSize, List[Rule[\"Columns\", List[List[Automatic]]], Rule[\"Rows\", List[List[Automatic]]]]]], \"Grid\"], TraditionalForm]\\)" + + Grid[{{a,b},{c,d}}]: + msg: GridBox latex: InputForm: \text{Grid[\{\{a, b\}, \{c, d\}\}]} OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} @@ -551,7 +432,6 @@ Grid[{{a,b},{c,d}}]: cd ' - msg: GridBox text: InputForm: Grid[{{a, b}, {c, d}}] OutputForm: 'a b @@ -560,10 +440,13 @@ Grid[{{a,b},{c,d}}]: c d' StandardForm: "\\!\\(\\*TagBox[GridBox[{{\"a\", \"b\"}, {\"c\", \"d\"}}, Rule[AutoDelete, False], Rule[GridBoxItemSize, List[Rule[\"Columns\", List[List[Automatic]]], Rule[\"Rows\", List[List[Automatic]]]]]], \"Grid\"]\\)" TraditionalForm: "\\!\\(\\*FormBox[TagBox[GridBox[{{\"a\", \"b\"}, {\"c\", \"d\"}}, Rule[AutoDelete, False], Rule[GridBoxItemSize, List[Rule[\"Columns\", List[List[Automatic]]], Rule[\"Rows\", List[List[Automatic]]]]]], \"Grid\"], TraditionalForm]\\)" + + # # Integrate # Integrate[F[x], {x, a, g[b]}]: + msg: Definite Integral - Nontrivial SubsuperscriptBox latex: InputForm: \text{Integrate[F[x], \{x, a, g[b]\}]} OutputForm: \text{Integrate}\left[F\left[x\right], \left\{x, a, g\left[b\right]\right\}\right] @@ -578,10 +461,12 @@ Integrate[F[x], {x, a, g[b]}]: [ x ] { x a g [ b ] } ] - msg: Nontrivial SubsuperscriptBox text: OutputForm: Integrate[F[x], {x, a, g[b]}] + + MatrixForm[{{a,b},{c,d}}]: + msg: GridBox in a matrix latex: InputForm: \text{MatrixForm[\{\{a, b\}, \{c, d\}\}]} OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} @@ -613,7 +498,6 @@ MatrixForm[{{a,b},{c,d}}]: cd )' - msg: GridBox in a matrix text: InputForm: MatrixForm[{{a, b}, {c, d}}] OutputForm: 'a b @@ -622,7 +506,10 @@ MatrixForm[{{a,b},{c,d}}]: c d' StandardForm: "\\!\\(\\*TagBox[RowBox[{\"(\", \"\\[NoBreak]\", GridBox[{{\"a\", \"b\"}, {\"c\", \"d\"}}, Rule[RowSpacings, 1], Rule[ColumnSpacings, 1], Rule[RowAlignments, Baseline], Rule[ColumnAlignments, Center]], \"\\[NoBreak]\", \")\"}], Function[BoxForm`e$, MatrixForm[BoxForm`e$]]]\\)" TraditionalForm: "\\!\\(\\*FormBox[TagBox[RowBox[{\"(\", \"\\[NoBreak]\", GridBox[{{\"a\", \"b\"}, {\"c\", \"d\"}}, Rule[RowSpacings, 1], Rule[ColumnSpacings, 1], Rule[RowAlignments, Baseline], Rule[ColumnAlignments, Center]], \"\\[NoBreak]\", \")\"}], Function[BoxForm`e$, MatrixForm[BoxForm`e$]]], TraditionalForm]\\)" + + Sqrt[1/(1+1/(1+1/a))]: + msg: SqrtBox latex: InputForm: \text{Sqrt[1/(1 + 1/(1 + 1/a))]} OutputForm: \text{Sqrt}\left[1\text{ / }\left(1\text{ + }1\text{ / }\left(1\text{ @@ -650,13 +537,15 @@ Sqrt[1/(1+1/(1+1/a))]: - Fragile! TraditionalForm: *id002 - msg: SqrtBox text: InputForm: Sqrt[(1 + (1 + a^(-1))^(-1))^(-1)] OutputForm: " 1\nSqrt[---------]\n 1\n 1 + -----\n 1\n 1 + -\n a" StandardForm: "\\!\\(\\*SqrtBox[FractionBox[\"1\", RowBox[{\"1\", \"+\", FractionBox[\"1\", RowBox[{\"1\", \"+\", FractionBox[\"1\", \"a\"]}]]}]]]\\)" TraditionalForm: "\\!\\(\\*FormBox[SqrtBox[FractionBox[\"1\", RowBox[{FractionBox[\"1\", RowBox[{FractionBox[\"1\", \"a\"], \"+\", \"1\"}]], \"+\", \"1\"}]]], TraditionalForm]\\)" + + Subscript[a, 4]: + msg: SubscriptBox latex: InputForm: \text{Subscript[a, 4]} OutputForm: \text{Subscript}\left[a, 4\right] @@ -673,7 +562,6 @@ Subscript[a, 4]: - Fragile! StandardForm: a 4 TraditionalForm: a 4 - msg: SubscriptBox text: InputForm: Subscript[a, 4] OutputForm: "a\n 4" @@ -681,7 +569,10 @@ Subscript[a, 4]: - Subscript[a, 4] - BoxError TraditionalForm: *id003 + + Subsuperscript[a, p, q]: + msg: SubsuperscriptBox latex: InputForm: \text{Subsuperscript[a, p, q]} OutputForm: \text{Subsuperscript}\left[a, p, q\right] @@ -694,13 +585,15 @@ Subsuperscript[a, p, q]: p q ] StandardForm: a p q TraditionalForm: a p q - msg: SubsuperscriptBox text: InputForm: Subsuperscript[a, p, q] OutputForm: " q\na\n p" StandardForm: "\\!\\(\\*TemplateBox[List[\"a\", \"p\", \"q\"], \"Subsuperscript\", Rule[SyntaxForm, SubsuperscriptBox]]\\)" TraditionalForm: "\\!\\(\\*FormBox[TemplateBox[List[\"a\", \"p\", \"q\"], \"Subsuperscript\", Rule[SyntaxForm, SubsuperscriptBox]], TraditionalForm]\\)" + + TableForm[{Graphics[{Text[a^b,{0,0}]}], Graphics[{Text[a^b,{0,0}]}]}]: + msg: A table of graphics - Fragile! latex: StandardForm: "\\begin{array}{c} \n\\begin{asy}\nusepackage(\"amsmath\"\ );\nsize(2.45cm, 2.9167cm);\n\n// InsetBox\nlabel(\"$a^b$\", (73.5,87.5), align=SW,\ @@ -714,7 +607,6 @@ TableForm[{Graphics[{Text[a^b,{0,0}]}], Graphics[{Text[a^b,{0,0}]}]}]: \\\\ \n\\begin{asy}\nusepackage(\"amsmath\");\nsize(2.45cm, 2.9167cm);\n\n//\ \ InsetBox\nlabel(\"$a^b$\", (73.5,87.5), align=SW, rgb(0, 0, 0)+fontsize(3));\n\ \nclip(box((63,75), (84,100)));\n\n\\end{asy}\n\\end{array}" - msg: A table of graphics - Fragile! text: InputForm: TableForm[{Graphics[{Text[a^b, {0, 0}]}], Graphics[{Text[a^b, {0, 0}]}]}] OutputForm: '-Graphics- @@ -723,10 +615,10 @@ TableForm[{Graphics[{Text[a^b,{0,0}]}], Graphics[{Text[a^b,{0,0}]}]}]: -Graphics-' StandardForm: "\\!\\(\\*TagBox[TagBox[GridBox[{{GraphicsBox[List[InsetBox[FormBox[SuperscriptBox[\"a\", \"b\"], TraditionalForm], List[0, 0]]]]}, {GraphicsBox[List[InsetBox[FormBox[SuperscriptBox[\"a\", \"b\"], TraditionalForm], List[0, 0]]]]}}, Rule[RowSpacings, 1], Rule[ColumnAlignments, Left], Rule[ColumnAlignments, Left]], Column], Function[BoxForm`e$, TableForm[BoxForm`e$]]]\\)" TraditionalForm: "\\!\\(\\*FormBox[TagBox[TagBox[GridBox[{{GraphicsBox[List[InsetBox[FormBox[SuperscriptBox[\"a\", \"b\"], TraditionalForm], List[0, 0]]]]}, {GraphicsBox[List[InsetBox[FormBox[SuperscriptBox[\"a\", \"b\"], TraditionalForm], List[0, 0]]]]}}, Rule[RowSpacings, 1], Rule[ColumnAlignments, Left], Rule[ColumnAlignments, Left]], Column], Function[BoxForm`e$, TableForm[BoxForm`e$]]], TraditionalForm]\\)" -# -# -# + + TableForm[{{a,b},{c,d}}]: + msg: GridBox in a table latex: InputForm: \text{TableForm[\{\{a, b\}, \{c, d\}\}]} OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} @@ -758,7 +650,6 @@ TableForm[{{a,b},{c,d}}]: cd ' - msg: GridBox in a table text: InputForm: TableForm[{{a, b}, {c, d}}] OutputForm: 'a b @@ -767,7 +658,10 @@ TableForm[{{a,b},{c,d}}]: c d' StandardForm: "\\!\\(\\*TagBox[GridBox[{{\"a\", \"b\"}, {\"c\", \"d\"}}, Rule[RowSpacings, 1], Rule[ColumnSpacings, 3], Rule[RowAlignments, Baseline], Rule[ColumnAlignments, Left]], Function[BoxForm`e$, TableForm[BoxForm`e$]]]\\)" TraditionalForm: "\\!\\(\\*FormBox[TagBox[GridBox[{{\"a\", \"b\"}, {\"c\", \"d\"}}, Rule[RowSpacings, 1], Rule[ColumnSpacings, 3], Rule[RowAlignments, Baseline], Rule[ColumnAlignments, Left]], Function[BoxForm`e$, TableForm[BoxForm`e$]]], TraditionalForm]\\)" + + \[Pi]: + msg: A greek letter Symbol latex: InputForm: \text{Pi} OutputForm: \text{Pi} @@ -778,13 +672,15 @@ TableForm[{{a,b},{c,d}}]: OutputForm: Pi StandardForm: "\u03C0" TraditionalForm: "\u03C0" - msg: A greek letter Symbol text: InputForm: Pi OutputForm: Pi StandardForm: "\\!\\(\\*RowBox[{\"\\[Pi]\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[\"\\[Pi]\", TraditionalForm]\\)" + + a: + msg: A Symbol latex: InputForm: a OutputForm: a @@ -795,13 +691,15 @@ a: OutputForm: a StandardForm: a TraditionalForm: a - msg: A Symbol text: InputForm: a OutputForm: a StandardForm: "\\!\\(\\*RowBox[{\"a\"}]\\)" TraditionalForm: "\\!\\(\\*FormBox[\"a\", TraditionalForm]\\)" + + a^(g[b]/c): + msg: SuperscriptBox with a nested expression. latex: InputForm: \text{a${}^{\wedge}$(g[b]/c)} OutputForm: a\text{ ${}^{\wedge}$ }\left(g(b)\text{ / }c\right) @@ -814,13 +712,15 @@ a^(g[b]/c): b  /  c ) StandardForm: a b c TraditionalForm: a g ( b ) c - msg: SuperscriptBox with a nested expression. text: InputForm: a^(g[b]/c) OutputForm: " g[b]/c\na" StandardForm: "\\!\\(\\*SuperscriptBox[\"a\", FractionBox[RowBox[{\"g\", \"[\", \"b\", \"]\"}], \"c\"]]\\)" TraditionalForm: "\\!\\(\\*FormBox[SuperscriptBox[\"a\", FractionBox[RowBox[{\"g\", \"(\", \"b\", \")\"}], \"c\"]], TraditionalForm]\\)" + + a^4: + msg: SuperscriptBox latex: InputForm: \text{a${}^{\wedge}$4} OutputForm: a\text{ ${}^{\wedge}$ }4 @@ -831,7 +731,6 @@ a^4: OutputForm: a  ^  4 StandardForm: a 4 TraditionalForm: a 4 - msg: SuperscriptBox text: InputForm: a^4 OutputForm: " 4\na" diff --git a/test/format/format_tests.m b/test/format/format_tests.m index 44f184cf7..1b20c5416 100755 --- a/test/format/format_tests.m +++ b/test/format/format_tests.m @@ -9,6 +9,31 @@ ISMATHICSINTERPRETER=(StringTake[$Version, 8]==="Mathics3"); +If[ISMATHICSINTERPRETER, + (*Mathics Interpreter*) + STRIPMATHML[strg_]:= Module[ + {start=(StringPosition[strg,""][[1]][[2]]+1), + end=(StringPosition[strg,""][[1]][[1]]-1) + }, + StringTrim[StringTake[strg, {start, end}]] + ], + (*WMA interpreter*) + STRIPMATHML[strg_]:= Module[ + {semanticpos, start, end}, + semanticpos = StringPosition[strg,""]; + (*If have a field use it as a reference.*) + If[Length[semanticpos]>0, + start=(semanticpos[[1]][[2]]+1); + end=(StringPosition[strg,"...*) + start=StringPosition[strg,""][[1]][[2]]+1; + end=StringPosition[strg,""][[1]][[1]]-1; + ]; + StringTrim[StringTake[strg, {start, end}]] + ] +] + + Print["Read json"]; data = Import["format_tests-WMA.json"]; @@ -36,7 +61,7 @@ {subtest, text}] ]; (*LaTeX*) - If[And[ISMATHICSINTERPRETER, Head[latex]===List], + If[Head[latex]===List, Print[" latex", "\n -----", "\n"]; Do[form = ToExpression[subtest[[1]]]; expr = form[key]; result = ToString[expr, TeXForm, CharacterEncoding->"ASCII"]; @@ -49,10 +74,10 @@ {subtest, latex}] ]; (*MathML*) - If[And[ISMATHICSINTERPRETER, Head[mathml]===List], + If[Head[mathml]===List, Print[" mathml", "\n ------", "\n"]; Do[form = ToExpression[subtest[[1]]]; expr = form[key]; - result = ToString[expr, MathMLForm, CharacterEncoding->"ASCII"]; + result = STRIPMATHML[ToString[expr, MathMLForm, CharacterEncoding->"ASCII"]]; expected = subtest[[2]]; If[result != subtest[[2]], Print[" * ", key, " //", form, "(mathml) [Failed]\n result:", diff --git a/test/format/format_tests.yaml b/test/format/format_tests.yaml index 0664d3586..14d511606 100644 --- a/test/format/format_tests.yaml +++ b/test/format/format_tests.yaml @@ -23,7 +23,9 @@ # because we use both in documentation and in the web interface. # + '"-7.32"': + msg: A String with a number latex: System`InputForm: \text{``-7.32''} System`OutputForm: \text{-7.32} @@ -34,13 +36,15 @@ System`OutputForm: -7.32 System`StandardForm: -7.32 System`TraditionalForm: -7.32 - msg: A String with a number text: System`InputForm: '"-7.32"' System`OutputForm: '-7.32' System`StandardForm: '-7.32' System`TraditionalForm: '-7.32' + + '"Hola!"': + msg: A String latex: System`InputForm: \text{``Hola!''} System`OutputForm: \text{Hola!} @@ -51,61 +55,70 @@ System`OutputForm: Hola! System`StandardForm: Hola! System`TraditionalForm: Hola! - msg: A String text: System`InputForm: '"Hola!"' System`OutputForm: Hola! System`StandardForm: Hola! System`TraditionalForm: Hola! + + '"\[Pi] is a trascendental number"': + msg: A String latex: - System`InputForm: "\\text{``\u03C0 is a trascendental number''}" - System`OutputForm: "\\text{\u03C0 is a trascendental number}" - System`StandardForm: "\\text{\u03C0 is a trascendental number}" - System`TraditionalForm: "\\text{\u03C0 is a trascendental number}" + System`InputForm: "\\text{``$\\pi$ is a trascendental number''}" + System`OutputForm: "\\text{$\\pi$ is a trascendental number}" + System`StandardForm: "\\text{$\\pi$ is a trascendental number}" + System`TraditionalForm: "\\text{$\\pi$ is a trascendental number}" mathml: System`InputForm: "\u03C0 is a trascendental number" System`OutputForm: "\u03C0 is a trascendental number" System`StandardForm: "\u03C0 is a trascendental number" System`TraditionalForm: "\u03C0 is a trascendental number" - msg: A String text: System`InputForm: "\"\u03C0 is a trascendental number\"" System`OutputForm: "\u03C0 is a trascendental number" System`StandardForm: "\u03C0 is a trascendental number" System`TraditionalForm: "\u03C0 is a trascendental number" + + -1. 10^-5: msg: small negative real number (>10^-5) latex: System`InputForm: '-0.00001' - System`OutputForm: '-0.00001' + System`OutputForm: '\text{-0.00001}' mathml: {} text: System`InputForm: '-0.00001' System`OutputForm: '-0.00001' + + -1. 10^-6: msg: very small negative real number (<10^-6) latex: System`InputForm: -1.*{}^{\wedge}-6 - System`OutputForm: -1.\times 10^{-6} + System`OutputForm: '\text{-1.$\times$10${}^{\wedge}$-6}' mathml: {} text: System`InputForm: -1.*^-6 System`OutputForm: "-1.\xD710^-6" + + -1. 10^5: + msg: large negative real number (<10^6) latex: System`InputForm: '-100000.' - System`OutputForm: '-100000.' + System`OutputForm: '\text{-100000.}' mathml: {} - msg: large negative real number (>10^6) text: System`InputForm: '-100000.' System`OutputForm: '-100000.' + + -1. 10^6: - msg: large negative real number (>10^6) + msg: very large negative real number (>10^6) latex: System`InputForm: -1.*{}^{\wedge}6 - System`OutputForm: -1.\times 10^6 + System`OutputForm: '\text{-1.$\times$10${}^{\wedge}$6}' System`StandardForm: -1.\text{*${}^{\wedge}$}6 System`TraditionalForm: -1.\times 10^6 mathml: {} @@ -114,16 +127,18 @@ System`OutputForm: "-1.\xD710^6" System`StandardForm: -1.`*^6 System`TraditionalForm: "-1.`\xD710^6" + + '-4': msg: An Integer latex: System`InputForm: '-4' - System`OutputForm: '-4' + System`OutputForm: '\text{-4}' System`StandardForm: '-4' System`TraditionalForm: '-4' mathml: System`InputForm: -4 - System`OutputForm: -4 + System`OutputForm: -4 System`StandardForm: - 4 System`TraditionalForm: - 4 text: @@ -131,16 +146,18 @@ System`OutputForm: '-4' System`StandardForm: '-4' System`TraditionalForm: '-4' + + '-4.32': msg: A MachineReal number latex: System`InputForm: '-4.32' - System`OutputForm: '-4.32' + System`OutputForm: '\text{-4.32}' System`StandardForm: '-4.32' System`TraditionalForm: '-4.32' mathml: System`InputForm: -4.32 - System`OutputForm: -4.32 + System`OutputForm: -4.32 System`StandardForm: - 4.32 System`TraditionalForm: - 4.32 text: @@ -148,76 +165,88 @@ System`OutputForm: '-4.32' System`StandardForm: -4.32` System`TraditionalForm: -4.32` + + -4.326563712`2: + msg: A negative Real number latex: System`InputForm: '-4.33' - System`OutputForm: '-4.3' + System`OutputForm: '\text{-4.3}' System`StandardForm: '-4.33' System`TraditionalForm: '-4.33' mathml: System`InputForm: -4.33 - System`OutputForm: -4.3 + System`OutputForm: -4.3 System`StandardForm: - 4.33 System`TraditionalForm: - 4.33 - msg: A Real number text: System`InputForm: -4.33`2. System`OutputForm: '-4.3' System`StandardForm: -4.33`2. System`TraditionalForm: -4.33`2. + + -4.32`4: + msg: A negative PrecisionReal number latex: System`InputForm: '-4.32' - System`OutputForm: '-4.320' + System`OutputForm: '\text{-4.320}' System`StandardForm: '-4.32' System`TraditionalForm: '-4.32' mathml: System`InputForm: -4.32 - System`OutputForm: -4.320 + System`OutputForm: -4.320 System`StandardForm: - 4.32 System`TraditionalForm: - 4.32 - msg: A PrecisionReal number text: System`InputForm: -4.32`4. System`OutputForm: '-4.320' System`StandardForm: -4.32`4. System`TraditionalForm: -4.32`4. + + 1. 10^-5: - msg: small real number (<10^-5) + msg: small positive real number (>10^-6) latex: System`InputForm: '0.00001' - System`OutputForm: '0.00001' + System`OutputForm: '\text{0.00001}' mathml: {} text: System`InputForm: '0.00001' System`OutputForm: '0.00001' + + 1. 10^-6: - msg: very small real number (<10^-6) + msg: very small positive real number (<10^-6) latex: System`InputForm: 1.*{}^{\wedge}-6 - System`OutputForm: 1.\times 10^{-6} + System`OutputForm: '\text{1.$\times$10${}^{\wedge}$-6}' mathml: {} text: System`InputForm: 1.*^-6 System`OutputForm: "1.\xD710^-6" + + 1. 10^5: + msg: large positive real number (<10^6) latex: System`InputForm: '100000.' - System`OutputForm: '100000.' + System`OutputForm: '\text{100000.}' System`StandardForm: '100000.' System`TraditionalForm: '100000.' mathml: {} - msg: large real number (>10^6) text: System`InputForm: '100000.' System`OutputForm: '100000.' System`StandardForm: 100000.` System`TraditionalForm: 100000.` + + 1. 10^6: - msg: very large real number (>10^6) + msg: very large positive real number (>10^6) latex: System`InputForm: 1.*{}^{\wedge}6 - System`OutputForm: 1.\times 10^6 + System`OutputForm: '\text{1.$\times$10${}^{\wedge}$6}' System`StandardForm: 1.\text{*${}^{\wedge}$}6 System`TraditionalForm: 1.\times 10^6 mathml: {} @@ -226,11 +255,13 @@ System`OutputForm: "1.\xD710^6" System`StandardForm: 1.`*^6 System`TraditionalForm: "1.`\xD710^6" + + 1/(1+1/(1+1/a)): + msg: FractionBox latex: System`InputForm: 1/(1 + 1/(1 + 1/a)) - System`OutputForm: 1\text{ / }\left(1\text{ + }1\text{ / }\left(1\text{ + }1\text{ - / }a\right)\right) + System`OutputForm: '\text{1 / (1 + 1 / (1 + 1 / a))}' System`StandardForm: \frac{1}{1+\frac{1}{1+\frac{1}{a}}} System`TraditionalForm: \frac{1}{1+\frac{1}{1+\frac{1}{a}}} mathml: @@ -238,37 +269,30 @@ - 1/(1 + 1/(1 + 1/a)) - Fragile! System`OutputForm: - - 1  /  ( 1 -  +  1  /  ( - 1  +  1  /  - a ) ) + - '1 / (1 + 1 / (1 + 1 / a))' - Fragile! System`StandardForm: &id001 - 1 1 + 1 1 + 1 a - Fragile! System`TraditionalForm: *id001 - msg: FractionBox text: System`InputForm: 1/(1 + 1/(1 + 1/a)) System`OutputForm: 1 / (1 + 1 / (1 + 1 / a)) System`StandardForm: 1 / (1+1 / (1+1 / a)) System`TraditionalForm: 1 / (1+1 / (1+1 / a)) + + <|a -> x, b -> y, c -> <|d -> t|>|>: msg: Association latex: - System`InputForm: \text{<$\vert$a -> x, b -> y, c -> <$\vert$d -> t$\vert$>$\vert$>} - System`OutputForm: \text{<$\vert$}a\text{ -> }x, b\text{ -> }y, c\text{ -> }\text{<$\vert$}d\text{ - -> }t\text{$\vert$>}\text{$\vert$>} - System`StandardForm: \text{<$\vert$}a->x, b->y, c->\text{<$\vert$}d->t\text{$\vert$>}\text{$\vert$>} - System`TraditionalForm: \text{<$\vert$}a->x, b->y, c->\text{<$\vert$}d->t\text{$\vert$>}\text{$\vert$>} + System`InputForm: '\text{$<$$\vert$a -$>$ x, b -$>$ y, c -$>$ $<$$\vert$d -$>$ t$\vert$$>$$\vert$$>$}' + System`OutputForm: '\text{$<$$\vert$a -$>$ x, b -$>$ y, c -$>$ $<$$\vert$d -$>$ t$\vert$$>$$\vert$$>$}' + System`StandardForm: '\langle\vert a->x, b->y, c->\langle\vert d->t\vert\rangle \vert\rangle' + System`TraditionalForm: '\langle\vert a->x, b->y, c->\langle\vert d->t\vert\rangle \vert\rangle' mathml: System`InputForm: <|a -> x, b -> y, c -> <|d -> t|>|> - System`OutputForm: <| a  ->  - x b  ->  - y c  ->  - <| d  ->  - t |> |> + System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t|>|>' System`StandardForm: <| a -> x , b -> y , c -> <| d @@ -282,22 +306,18 @@ System`OutputForm: <|a -> x, b -> y, c -> <|d -> t|>|> System`StandardForm: <|a->x, b->y, c-><|d->t|>|> System`TraditionalForm: <|a->x, b->y, c-><|d->t|>|> + + Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: msg: Nested Association latex: - System`InputForm: \text{<$\vert$a -> x, b -> y, c -> <$\vert$d -> t, e -> u$\vert$>$\vert$>} - System`OutputForm: \text{<$\vert$}a\text{ -> }x, b\text{ -> }y, c\text{ -> }\text{<$\vert$}d\text{ - -> }t, e\text{ -> }u\text{$\vert$>}\text{$\vert$>} - System`StandardForm: \text{<$\vert$}a->x, b->y, c->\text{<$\vert$}d->t, e->u\text{$\vert$>}\text{$\vert$>} - System`TraditionalForm: \text{<$\vert$}a->x, b->y, c->\text{<$\vert$}d->t, e->u\text{$\vert$>}\text{$\vert$>} + System`InputForm: '\text{$<$$\vert$a -$>$ x, b -$>$ y, c -$>$ $<$$\vert$d -$>$ t, e -$>$ u$\vert$$>$$\vert$$>$}' + System`OutputForm: '\text{$<$$\vert$a -$>$ x, b -$>$ y, c -$>$ $<$$\vert$d -$>$ t, e -$>$ u$\vert$$>$$\vert$$>$}' + System`StandardForm: '\langle\vert a->x, b->y, c->\langle\vert d->t, e->u\vert\rangle \vert\rangle' + System`TraditionalForm: '\langle\vert a->x, b->y, c->\langle\vert d->t, e->u\vert\rangle \vert\rangle' mathml: System`InputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> - System`OutputForm: <| a  ->  - x b  ->  - y c  ->  - <| d  ->  - t e  ->  - u |> |> + System`OutputForm: '<|a -> x, b -> y, c -> <|d -> t, e -> u|>|>' System`StandardForm: <| a -> x , b -> y , c -> <| d @@ -313,52 +333,35 @@ Association[a -> x, b -> y, c -> Association[d -> t, Association[e -> u]]]: System`OutputForm: <|a -> x, b -> y, c -> <|d -> t, e -> u|>|> System`StandardForm: <|a->x, b->y, c-><|d->t, e->u|>|> System`TraditionalForm: <|a->x, b->y, c-><|d->t, e->u|>|> + + Complex[1.09*^12, 3.]: + msg: Complex number latex: System`InputForm: 1.09*{}^{\wedge}12 + 3.*I - System`OutputForm: 1.09\times 10^{12}\text{ + }3. I + System`OutputForm: '\text{1.09$\times$10${}^{\wedge}$12 + 3. I}' System`StandardForm: 1.09\text{*${}^{\wedge}$}12+3. I System`TraditionalForm: 1.09\times 10^{12}+3. I mathml: System`InputForm: 1.09*^12 + 3.*I - System`OutputForm: "1.09 \xD7 10\ - \ 12  +  3.  \ - \ I" + System`OutputForm: '1.09×10^12 + 3. I' System`StandardForm: 1.09 *^ 12 + 3.   I System`TraditionalForm: "1.09 \xD7 10\ \ 12 + 3. \u2062 I" - msg: Complex number text: System`InputForm: 1.09*^12 + 3.*I System`OutputForm: "1.09\xD710^12 + 3. I" System`StandardForm: 1.09`*^12+3.` I System`TraditionalForm: "1.09`\xD710^12+3.`\u2062I" + + Graphics[{Text[a^b,{0,0}]}]: msg: Nontrivial Graphics - Fragile! latex: System`InputForm: \text{Graphics[\{Text[a${}^{\wedge}$b, \{0, 0\}]\}]} - System`OutputForm: ' - - \begin{asy} - - usepackage("amsmath"); - - size(4.9cm, 5.8333cm); - - - // InsetBox - - label("$a^b$", (147.0,175.0), align=SW, rgb(0, 0, 0)+fontsize(3)); - - - clip(box((136.5,162.5), (157.5,187.5))); - - - \end{asy} - - ' + System`OutputForm: '\text{-Graphics-}' System`StandardForm: ' \begin{asy} @@ -404,25 +407,13 @@ Graphics[{Text[a^b,{0,0}]}]: System`OutputForm: -Graphics- System`StandardForm: -Graphics- System`TraditionalForm: -Graphics- + + Graphics[{}]: + msg: GraphicsBox - Fragile! latex: System`InputForm: \text{Graphics[\{\}]} - System`OutputForm: ' - - \begin{asy} - - usepackage("amsmath"); - - size(5.8333cm, 5.8333cm); - - - - clip(box((-1,-1), (1,1))); - - - \end{asy} - - ' + System`OutputForm: '\text{-Graphics-}' System`StandardForm: ' \begin{asy} @@ -455,29 +446,36 @@ Graphics[{}]: \end{asy} ' - msg: GraphicsBox - Fragile! text: System`InputForm: Graphics[{}] System`OutputForm: -Graphics- System`StandardForm: -Graphics- System`TraditionalForm: -Graphics- + + "Grid[{{\"Spanish\", \"Hola!\"},{\"Portuguese\", \"Ol\xE0!\"},{\"English\", \"Hi!\"}}]": + msg: Strings in a GridBox latex: - System`InputForm: \text{Grid[\{\{"Spanish", "Hola!"\}, \{"Portuguese", "Olà!"\}, \{"English", "Hi!"\}\}]} - System`OutputForm: "\\begin{array}{cc} \\text{Spanish} & \\text{Hola!}\\\\ \\\ - text{Portuguese} & \\text{Ol\xE0!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" + System`InputForm: \text{Grid[\{\{"Spanish", "Hola!"\}, \{"Portuguese", "Ol\`{a}!"\}, \{"English", "Hi!"\}\}]} + System`OutputForm: '\text{Spanish Hola!\newline + + \newline + + Portuguese Ol\`{a}!\newline + + \newline + + English Hi!\newline + + } +' System`StandardForm: "\\begin{array}{cc} \\text{Spanish} & \\text{Hola!}\\\\ \\\ - text{Portuguese} & \\text{Ol\xE0!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" + text{Portuguese} & \\text{Ol\\`{a}!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" System`TraditionalForm: "\\begin{array}{cc} \\text{Spanish} & \\text{Hola!}\\\\\ - \ \\text{Portuguese} & \\text{Ol\xE0!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" + \ \\text{Portuguese} & \\text{Ol\\`{a}!}\\\\ \\text{English} & \\text{Hi!}\\end{array}" mathml: System`InputForm: "Grid[{{"Spanish", "Hola!"}, {"Portuguese", "Olà!"}, {"English", "Hi!"}}]" - System`OutputForm: "\nSpanishHola!\n\ - PortugueseOl\xE0!\nEnglishHi!\n\ - " + System`OutputForm: 'Spanish      Hola!Portuguese   Olà!English      Hi!' System`StandardForm: "\nSpanishHola!\n\ PortugueseOl\xE0!\nEnglishHi!\n\ " - msg: Strings in a GridBox text: System`InputForm: "Grid[{{\"Spanish\", \"Hola!\"}, {\"Portuguese\", \"Ol\xE0!\"\ }, {\"English\", \"Hi!\"}}]" @@ -500,21 +497,24 @@ Graphics[{}]: \ Hi!\n" System`TraditionalForm: "Spanish Hola!\n\nPortuguese Ol\xE0!\n\nEnglish\ \ Hi!\n" + + Grid[{{a,b},{c,d}}]: + msg: GridBox latex: System`InputForm: \text{Grid[\{\{a, b\}, \{c, d\}\}]} - System`OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} - System`StandardForm: \begin{array}{cc} a & b\\ c & d\end{array} - System`TraditionalForm: \begin{array}{cc} a & b\\ c & d\end{array} + System`OutputForm: ' + \text{a b\newline + + \newline + + c d\newline + + } + ' mathml: System`InputForm: Grid[{{a, b}, {c, d}}] - System`OutputForm: ' - - ab - - cd - - ' + System`OutputForm: 'a   bc   d' System`StandardForm: ' ab @@ -529,7 +529,6 @@ Grid[{{a,b},{c,d}}]: cd ' - msg: GridBox text: System`InputForm: Grid[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -550,36 +549,39 @@ Grid[{{a,b},{c,d}}]: c d ' + + Integrate[F[x], {x, a, g[b]}]: + msg: Definite Integral - Nontrivial SubsuperscriptBox latex: System`InputForm: \text{Integrate[F[x], \{x, a, g[b]\}]} - System`OutputForm: \text{Integrate}\left[F\left[x\right], \left\{x, a, g\left[b\right]\right\}\right] - System`StandardForm: \int_a^{g\left[b\right]} F\left[x\right] \, dx - System`TraditionalForm: \int_a^{g\left(b\right)} F\left(x\right) \, dx + System`OutputForm: '\text{Integrate[F[x], \{x, a, g[b]\}]}' + System`StandardForm: \int_a^{g[b]} F[x] \, dx + System`TraditionalForm: \int_a^{g(b)} F(x) \, dx mathml: System`InputForm: Integrate[F[x], {x, a, g[b]}] - System`OutputForm: Integrate [ F - [ x ] { - x a g - [ b ] } ] - msg: Nontrivial SubsuperscriptBox + System`OutputForm: 'Integrate[F[x], {x, a, g[b]}]' text: System`OutputForm: Integrate[F[x], {x, a, g[b]}] + + MatrixForm[{{a,b},{c,d}}]: + msg: GridBox in a matrix latex: System`InputForm: \text{MatrixForm[\{\{a, b\}, \{c, d\}\}]} - System`OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} + System`OutputForm: '\text{a b\newline + + \newline + + c d\newline + + } +' System`StandardForm: \left(\begin{array}{cc} a & b\\ c & d\end{array}\right) System`TraditionalForm: \left(\begin{array}{cc} a & b\\ c & d\end{array}\right) mathml: System`InputForm: MatrixForm[{{a, b}, {c, d}}] - System`OutputForm: ' - - ab - - cd - - ' + System`OutputForm: 'a   bc   d' System`StandardForm: '( ab @@ -594,7 +596,6 @@ MatrixForm[{{a,b},{c,d}}]: cd )' - msg: GridBox in a matrix text: System`InputForm: MatrixForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -615,12 +616,13 @@ MatrixForm[{{a,b},{c,d}}]: c d )' + + Sqrt[1/(1+1/(1+1/a))]: msg: SqrtBox latex: System`InputForm: \text{Sqrt[1/(1 + 1/(1 + 1/a))]} - System`OutputForm: \text{Sqrt}\left[1\text{ / }\left(1\text{ + }1\text{ / }\left(1\text{ - + }1\text{ / }a\right)\right)\right] + System`OutputForm: '\text{Sqrt[1 / (1 + 1 / (1 + 1 / a))]}' System`StandardForm: \sqrt{\frac{1}{1+\frac{1}{1+\frac{1}{a}}}} System`TraditionalForm: \sqrt{\frac{1}{1+\frac{1}{1+\frac{1}{a}}}} mathml: @@ -628,11 +630,7 @@ Sqrt[1/(1+1/(1+1/a))]: - Sqrt[1/(1 + 1/(1 + 1/a))] - Fragile! System`OutputForm: - - Sqrt [ 1  /  - ( 1  +  1 -  /  ( 1  +  - 1  /  a ) - ) ] + - 'Sqrt[1 / (1 + 1 / (1 + 1 / a))]' - Fragile! System`StandardForm: &id002 - 1 1 + 1 1 @@ -645,10 +643,13 @@ Sqrt[1/(1+1/(1+1/a))]: System`OutputForm: Sqrt[1 / (1 + 1 / (1 + 1 / a))] System`StandardForm: Sqrt[1 / (1+1 / (1+1 / a))] System`TraditionalForm: Sqrt[1 / (1+1 / (1+1 / a))] + + Subscript[a, 4]: + msg: SubscriptBox latex: System`InputForm: \text{Subscript[a, 4]} - System`OutputForm: \text{Subscript}\left[a, 4\right] + System`OutputForm: '\text{Subscript[a, 4]}' System`StandardForm: a_4 System`TraditionalForm: a_4 mathml: @@ -656,12 +657,10 @@ Subscript[a, 4]: - Subscript[a, 4] - Fragile! System`OutputForm: - - Subscript [ a - 4 ] + - 'Subscript[a, 4]' - Fragile! System`StandardForm: a 4 System`TraditionalForm: a 4 - msg: SubscriptBox text: System`InputForm: Subscript[a, 4] System`OutputForm: Subscript[a, 4] @@ -669,24 +668,27 @@ Subscript[a, 4]: - Subscript[a, 4] - BoxError System`TraditionalForm: *id003 + + Subsuperscript[a, p, q]: + msg: SubsuperscriptBox latex: System`InputForm: \text{Subsuperscript[a, p, q]} - System`OutputForm: \text{Subsuperscript}\left[a, p, q\right] + System`OutputForm: '\text{Subsuperscript[a, p, q]}' System`StandardForm: a_p^q System`TraditionalForm: a_p^q mathml: System`InputForm: Subsuperscript[a, p, q] - System`OutputForm: Subsuperscript [ a - p q ] + System`OutputForm: 'Subsuperscript[a, p, q]' System`StandardForm: a p q System`TraditionalForm: a p q - msg: SubsuperscriptBox text: System`InputForm: Subsuperscript[a, p, q] System`OutputForm: Subsuperscript[a, p, q] System`StandardForm: Subsuperscript[a, p, q] System`TraditionalForm: Subsuperscript[a, p, q] + + TableForm[{Graphics[{Text[a^b,{0,0}]}], Graphics[{Text[a^b,{0,0}]}]}]: msg: A table of graphics - Fragile! latex: @@ -722,21 +724,24 @@ TableForm[{Graphics[{Text[a^b,{0,0}]}], Graphics[{Text[a^b,{0,0}]}]}]: -Graphics- ' + + TableForm[{{a,b},{c,d}}]: + msg: GridBox in a table latex: System`InputForm: \text{TableForm[\{\{a, b\}, \{c, d\}\}]} - System`OutputForm: \begin{array}{cc} a & b\\ c & d\end{array} + System`OutputForm: '\text{a b\newline + + \newline + + c d\newline + + }' System`StandardForm: \begin{array}{cc} a & b\\ c & d\end{array} System`TraditionalForm: \begin{array}{cc} a & b\\ c & d\end{array} mathml: System`InputForm: TableForm[{{a, b}, {c, d}}] - System`OutputForm: ' - - ab - - cd - - ' + System`OutputForm: 'a   bc   d' System`StandardForm: ' ab @@ -751,7 +756,6 @@ TableForm[{{a,b},{c,d}}]: cd ' - msg: GridBox in a table text: System`InputForm: TableForm[{{a, b}, {c, d}}] System`OutputForm: 'a b @@ -772,7 +776,10 @@ TableForm[{{a,b},{c,d}}]: c d ' + + \[Pi]: + msg: Pi latex: System`InputForm: \text{Pi} System`OutputForm: \text{Pi} @@ -780,63 +787,130 @@ TableForm[{{a,b},{c,d}}]: System`TraditionalForm: \pi mathml: System`InputForm: Pi - System`OutputForm: Pi + System`OutputForm: 'Pi' System`StandardForm: "\u03C0" System`TraditionalForm: "\u03C0" - msg: A greek letter Symbol text: System`InputForm: Pi System`OutputForm: Pi System`StandardForm: "\u03C0" System`TraditionalForm: "\u03C0" + + +\[Alpha]: + msg: A greek letter symbol + latex: + System`InputForm: \alpha + System`OutputForm: \text{$\alpha$} + System`StandardForm: \alpha + System`TraditionalForm: \alpha + mathml: + System`InputForm: 'α' + System`OutputForm: 'α' + System`StandardForm: 'α' + System`TraditionalForm: 'α' + text: + System`InputForm: 'α' + System`OutputForm: 'α' + System`StandardForm: "α" + System`TraditionalForm: "α" + + a: + msg: A Symbol latex: System`InputForm: a - System`OutputForm: a + System`OutputForm: '\text{a}' System`StandardForm: a System`TraditionalForm: a mathml: System`InputForm: a - System`OutputForm: a + System`OutputForm: 'a' System`StandardForm: a System`TraditionalForm: a - msg: A Symbol text: System`InputForm: a System`OutputForm: a System`StandardForm: a System`TraditionalForm: a + + a^(g[b]/c): + msg: SuperscriptBox with a nested expression. latex: System`InputForm: \text{a${}^{\wedge}$(g[b]/c)} - System`OutputForm: a\text{ ${}^{\wedge}$ }\left(g\left[b\right]\text{ / }c\right) - System`StandardForm: a^{\frac{g\left[b\right]}{c}} - System`TraditionalForm: a^{\frac{g\left(b\right)}{c}} + System`OutputForm: '\text{a ${}^{\wedge}$ (g[b] / c)}' + System`StandardForm: a^{\frac{g[b]}{c}} + System`TraditionalForm: a^{\frac{g(b)}{c}} mathml: System`InputForm: a^(g[b]/c) - System`OutputForm: a  ^  ( g [ b ]  /  c ) - System`StandardForm: a g [ b ] c + System`OutputForm: 'a ^ (g[b] / c)' System`TraditionalForm: a g ( b ) c - msg: SuperscriptBox with a nested expression. text: System`InputForm: a^(g[b]/c) System`OutputForm: a ^ (g[b] / c) System`StandardForm: a^((g[b]) / c) System`TraditionalForm: a^((g(b)) / c) + + a^4: + msg: SuperscriptBox latex: System`InputForm: \text{a${}^{\wedge}$4} - System`OutputForm: a\text{ ${}^{\wedge}$ }4 + System`OutputForm: '\text{a ${}^{\wedge}$ 4}' System`StandardForm: a^4 System`TraditionalForm: a^4 mathml: - System`InputForm: a^4 - System`OutputForm: a  ^  4 + System`InputForm: 'a^4' + System`OutputForm: 'a ^ 4' System`StandardForm: a 4 System`TraditionalForm: a 4 - msg: SuperscriptBox text: System`InputForm: a^4 System`OutputForm: a ^ 4 System`StandardForm: a^4 System`TraditionalForm: a^4 + + +Optional[x__]: + msg: Optional with one argument + latex: + System`OutputForm: '\text{x\_\_.}' + System`StandardForm: '\text{x\_\_.}' + mathml: + System`OutputForm: 'x__.' + System`StandardForm: 'x__.' + text: + System`InputForm: '(x__.)' + System`OutputForm: 'x__.' + System`StandardForm: 'x__.' + System`TraditionalForm: 'x__.' + + +Optional[x__, a+b]: + msg: Optional with two arguments + latex: + System`OutputForm: ' \text{x\_\_ : a + b}' + System`StandardForm: '\text{x\_\_}:a+b' + mathml: + System`OutputForm: 'x__ : a + b' + text: + System`InputForm: 'x__ : a + b' + System`OutputForm: 'x__ : a + b' + System`StandardForm: 'x__:a+b' + System`TraditionalForm: 'x__:a+b' + + +a+PrecedenceForm[b+c,10]: + msg: "PrecedenceForm" + latex: + System`OutputForm: '\text{a + (b + c)}' + System`StandardForm: 'a+(b+c)' + mathml: + System`OutputForm: 'a + (b + c)' + System`StandardForm: 'a + ( b + c )' + text: + System`InputForm: 'a + (PrecedenceForm[b + c, 10])' + System`OutputForm: 'a + (b + c)' + System`StandardForm: 'a+(b+c)' + System`TraditionalForm: 'a+(b+c)' diff --git a/test/format/makeboxes_tests.yaml b/test/format/makeboxes_tests.yaml index 23d2e5990..4363450bb 100644 --- a/test/format/makeboxes_tests.yaml +++ b/test/format/makeboxes_tests.yaml @@ -65,10 +65,10 @@ Basic Forms: Arithmetic: FullForm: - expect: TagBox[StyleBox[RowBox[{"Plus", "[", RowBox[{"a", ",", RowBox[{"Times", "[", RowBox[{RowBox[{"-", "1"}], ",", "b"}], "]"}]}], "]"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm] + expect: 'TagBox[StyleBox[RowBox[{"Plus", "[", RowBox[{"a", ",", RowBox[{"Times", "[", RowBox[{RowBox[{"-", "1"}], ",", "b"}], "]"}]}], "]"}], System`ShowSpecialCharacters-> False, System`ShowStringCharacters -> True, System`NumberMarks -> True], FullForm]' expr: MakeBoxes[a-b//FullForm] InputForm: - expect: InterpretationBox[StyleBox["a - b", ShowStringCharacters -> True, NumberMarks-> True], InputForm[a - b], Editable -> True, AutoDelete -> True] + expect: InterpretationBox[StyleBox["a - b", System`ShowStringCharacters -> True, NumberMarks-> True], InputForm[a - b], Editable -> True, AutoDelete -> True] expr: MakeBoxes[a-b//InputForm] OutputForm: expect: InterpretationBox[PaneBox["\"a - b\""], OutputForm[a - b], Editable-> False] @@ -87,10 +87,10 @@ Basic Forms: expect: TagBox[FormBox[RowBox[List["F", "(", "x", ")"]], TraditionalForm], TraditionalForm, Editable-> True] expr: MakeBoxes[F[x]//TraditionalForm] FullForm: - expect: TagBox[StyleBox[RowBox[{"F", "[", "x", "]"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm] + expect: TagBox[StyleBox[RowBox[{"F", "[", "x", "]"}], ShowSpecialCharacters-> False, System`ShowStringCharacters -> True, System`NumberMarks -> True], FullForm] expr: MakeBoxes[F[x]//FullForm] InputForm: - expect: InterpretationBox[StyleBox["F[x]", ShowStringCharacters -> True, NumberMarks-> True], InputForm[F[x]], Editable -> True, AutoDelete -> True] + expect: InterpretationBox[StyleBox["F[x]", System`ShowStringCharacters -> True, NumberMarks-> True], InputForm[F[x]], Editable -> True, AutoDelete -> True] expr: MakeBoxes[F[x]//InputForm] OutputForm: expect: InterpretationBox[PaneBox["\"F[x]\""], OutputForm[F[x]], Editable ->False] @@ -103,10 +103,10 @@ Basic Forms: expr: MakeBoxes[F[x]//TeXForm] Integer_negative: FullForm: - expect: TagBox[StyleBox[RowBox[{"-", "14"}], ShowSpecialCharacters-> False, ShowStringCharacters -> True, NumberMarks -> True], FullForm] + expect: TagBox[StyleBox[RowBox[{"-", "14"}], ShowSpecialCharacters-> False, System`ShowStringCharacters -> True, System`NumberMarks -> True], FullForm] expr: MakeBoxes[-14//FullForm] InputForm: - expect: InterpretationBox[StyleBox["-14", ShowStringCharacters -> True, NumberMarks -> True], InputForm[-14], Editable -> True, AutoDelete -> True] + expect: InterpretationBox[StyleBox["-14", System`ShowStringCharacters -> True, System`NumberMarks -> True], InputForm[-14], Editable -> True, AutoDelete -> True] expr: MakeBoxes[-14//InputForm] OutputForm: expect: InterpretationBox[PaneBox["\"-14\""], OutputForm[-14], Editable -> False] @@ -119,10 +119,10 @@ Basic Forms: expr: MakeBoxes[-14//TeXForm] Integer_positive: FullForm: - expect: TagBox[StyleBox["14", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm] + expect: TagBox[StyleBox["14", System`ShowSpecialCharacters -> False, System`ShowStringCharacters-> True, System`NumberMarks -> True], FullForm] expr: MakeBoxes[14//FullForm] InputForm: - expect: InterpretationBox[StyleBox["14", ShowStringCharacters -> True, NumberMarks-> True], InputForm[14], Editable -> True, AutoDelete -> True] + expect: InterpretationBox[StyleBox["14", System`ShowStringCharacters -> True, NumberMarks-> True], InputForm[14], Editable -> True, AutoDelete -> True] expr: MakeBoxes[14//InputForm] OutputForm: expect: InterpretationBox[PaneBox["\"14\""], OutputForm[14], Editable -> False] @@ -135,12 +135,12 @@ Basic Forms: expr: MakeBoxes[14//TeXForm] PrecisionReal: FullForm: - expect: TagBox[StyleBox[RowBox[{"-", "14.`3."}], ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm] + expect: TagBox[StyleBox[RowBox[{"-", "14.`3."}], System`ShowSpecialCharacters -> False, System`ShowStringCharacters-> True, System`NumberMarks -> True], FullForm] expr: MakeBoxes[-14.`3//FullForm] msg: "In Mathics3, precision is always an integer number." InputForm: expr: MakeBoxes[-14.`3//InputForm] - expect: InterpretationBox[StyleBox["-14.`3.", ShowStringCharacters -> True, NumberMarks-> True], InputForm[-14.`3], Editable -> True, AutoDelete -> True] + expect: InterpretationBox[StyleBox["-14.`3.", System`ShowStringCharacters -> True, NumberMarks-> True], InputForm[-14.`3], Editable -> True, AutoDelete -> True] OutputForm: expect: InterpretationBox[PaneBox["\"-14.\""], OutputForm[-14.0], Editable-> False] expr: MakeBoxes[-14.0//OutputForm] @@ -153,10 +153,10 @@ Basic Forms: -> True] Symbol: FullForm: - expect: TagBox[StyleBox["x", ShowSpecialCharacters -> False, ShowStringCharacters-> True, NumberMarks -> True], FullForm] + expect: TagBox[StyleBox["x", System`ShowSpecialCharacters -> False, System`ShowStringCharacters-> True, System`NumberMarks -> True], FullForm] expr: MakeBoxes[x//FullForm] InputForm: - expect: InterpretationBox[StyleBox["x", ShowStringCharacters -> True, NumberMarks-> True], InputForm[x], Editable -> True, AutoDelete -> True] + expect: InterpretationBox[StyleBox["x", System`ShowStringCharacters -> True, NumberMarks-> True], InputForm[x], Editable -> True, AutoDelete -> True] expr: MakeBoxes[x//InputForm] OutputForm: expect: InterpretationBox[PaneBox["\"x\""], OutputForm[x], Editable -> False] diff --git a/test/format/test_boxexpressions.py b/test/format/test_boxexpressions.py new file mode 100644 index 000000000..53f4ff6a8 --- /dev/null +++ b/test/format/test_boxexpressions.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from test.helper import check_evaluation, session + +import pytest + +from mathics.core.element import BoxElementMixin +from mathics.core.expression import Expression + + +@pytest.mark.parametrize( + ("expr_str",), + [ + # Notice that in the evaluation, Box Expression objects + # removes options with the corresponding default values, and + # options are sorted in lexicographical order. + # In the examples, also use only expressions in their fully evaluated + # forms. For example, do not use `Color->Red` but + # `Color->RGBColor[1, 0, 0]`. + ( + 'StyleBox[FractionBox[StyleBox["3", Color->RGBColor[1, 0, 0], ShowStringCharacters->True], "2"], Alignment->Left]', + ), + ('StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True]',), + ( + 'InterpretationBox[StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True], AutoDelete->True, Editable->True]', + ), + ( + 'RowBox[{"Say hi!:", StyleBox["Hola", Color->RGBColor[1, 0, 0], ShowStringCharacters->True]}]', + ), + ], +) +def test_boxexpressions(expr_str): + expr = session.parse(expr_str) + boxes = expr.evaluate(session.evaluation) + assert isinstance( + boxes, BoxElementMixin + ), f"boxes of type {type(boxes)} should be a Box expression or string." + + expr_from_boxes = boxes.to_expression() + assert isinstance( + expr_from_boxes, Expression + ), f"{type(boxes)}.to_expression() should be Expression." + assert expr_from_boxes.sameQ( + expr + ), f"The initial and the reconstructed expressions must be the same:\n{expr}\n{expr_from_boxes}\n" + assert boxes.sameQ( + expr_from_boxes + ), "Boxes should be equivalent to the reconstructed expression.\n{expr_from_boxes}\n{boxes}\n" + # This seems to be an issue with the sameQ method of `Expression`. + assert expr_from_boxes.sameQ( + boxes + ), f"Boxes should be equivalent to the reconstructed expression.\n{expr_from_boxes}\n{boxes}\n" diff --git a/test/format/test_latex.py b/test/format/test_latex.py index bd631a191..c50fb1c29 100644 --- a/test/format/test_latex.py +++ b/test/format/test_latex.py @@ -22,15 +22,44 @@ def get_latex(wl_expression): @pytest.mark.parametrize( ("testcase", "expected"), [ + ("_", r"\_"), + ('"_"', r"\text{\_}"), ('"["', r"\text{[}"), ('"]"', r"\text{]}"), - ("HoldForm[A[[1,2]]]", r"A\left[\left[1, 2\right]\right]"), + ("HoldForm[A[[1,2]]]", r"A[[1, 2]]"), ('"wrong ]"', r"\text{wrong ]}"), - ("Integrate[F[x],x]", r"\int F\left[x\right] \, dx"), + ("Integrate[F[x],x]", r"\int F[x] \, dx"), ("Cap[c,b]", r"c \cap b"), ("CupCap[c,b]", r"c \stackrel{\smile}{\frown} b"), ("Congruent[c,b]", r"c \equiv b"), ("Pi", r"\pi"), + # In symbols and expressions + (r"\[Alpha]", r"\alpha"), + # In this case, without the linebreak, the tokeniser + # produce an error... + ("\\[Alpha]s\n", r"\text{$\alpha$s}"), + (r"\[Alpha] s", r"s \alpha"), + (r"\[AAcute]", r"\text{\'{a}}"), + # In this case, without the linebreak, the tokeniser + # produce an error... + ("\\[AAcute]s\n", r"\text{\'{a}s}"), + (r"\[AAcute] s", r"s \text{\'{a}}"), + # In strings + (r'"\[Alpha]"', r"\text{$\alpha$}"), + (r'"\[Alpha]s"', r"\text{$\alpha$s}"), + (r'"\[AAcute]"', r"\text{\'{a}}"), + (r'"M\[AAcute]s!"', r"\text{M\'{a}s!}"), + # $ + ("$Failed", r"\text{\$Failed}"), + ('"$Failed"', r"\text{\$Failed}"), + ("a < b", r"a"', r"\text{$<$a$\vert$b$>$}"), + ('"a & b"', r"\text{a $\&$ b}"), + ("a&", r"a\&"), + ('"a # b"', r"\text{a $\#$ b}"), + ("#1", r"\text{$\#$1}"), + ("%1", r"\text{$\%$1}"), + ('"%1"', r"\text{$\%$1}"), ], ) def test_expressions(testcase, expected): diff --git a/test/format/test_makeboxes.py b/test/format/test_makeboxes.py index e4a1386b1..2d12e2bc3 100644 --- a/test/format/test_makeboxes.py +++ b/test/format/test_makeboxes.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import os -from test.helper import check_evaluation +from test.helper import check_evaluation, session import pytest import yaml @@ -28,8 +28,8 @@ def makeboxes_basic_forms_iterator(block): for key, tests in MAKEBOXES_TESTS[block].items(): for form, entry in tests.items(): msg = f"{key}, {form}" - expr = entry["expr"] - expect = entry["expect"] + expr = entry["expr"] + "//InputForm" + expect = entry["expect"] + "//InputForm" yield expr, expect, msg @@ -44,7 +44,7 @@ def test_makeboxes_basic_forms(str_expr, str_expected, fail_msg): str_expected, to_string_expr=True, to_string_expected=True, - hold_expected=True, + hold_expected=False, failure_message=fail_msg, ) @@ -62,7 +62,7 @@ def test_makeboxes_real(str_expr, str_expected, msg): str_expected, to_string_expr=True, to_string_expected=True, - hold_expected=True, + hold_expected=False, failure_message=msg, ) @@ -478,3 +478,64 @@ def test_makeboxes_custom2(str_expr, str_expected, msg): to_python_expected=False, failure_message=msg, ) + + +@pytest.mark.parametrize( + ["expr", "expect"], + ( + ( + '"Hola"', + False, + ), + ( + '"Hola\nqué tal?"', + True, + ), + ( + "a/b//MakeBoxes", + True, + ), + ( + "Sqrt[a]//MakeBoxes", + True, + ), + ( + "a + b * c//MakeBoxes", + False, + ), + ( + "a + b / c//MakeBoxes", + True, + ), + ( + "a + b * c // InputForm//MakeBoxes", + False, + ), + ( + "a + b / c //InputForm//MakeBoxes", + False, + ), + ( + "a + b * c // OutputForm//MakeBoxes", + False, + ), + ( + "a + b / c // OutputForm//MakeBoxes", + False, + ), + ( + "a + b * c // FullForm//MakeBoxes", + False, + ), + ( + "a + b / c // FullForm//MakeBoxes", + False, + ), + ), +) +def test_multiline(expr, expect): + boxexpr = session.evaluate(expr) + print(expr, "->", boxexpr, (expect)) + assert ( + boxexpr.is_multiline == expect + ), f"{boxexpr} must {'not ' if not expect else ''}be multiline. Got ({boxexpr.is_multiline})" diff --git a/test/format/test_svg.py b/test/format/test_svg.py index 5f19ada56..395b6cead 100644 --- a/test/format/test_svg.py +++ b/test/format/test_svg.py @@ -57,19 +57,12 @@ def extract_svg_body(svg): def get_svg(expression): + from mathics.format.box.graphics import prepare_elements as prepare_elements2d + options = {} boxes = MakeBoxes(expression).evaluate(evaluation) - - # Would be nice to DRY this boilerplate from boxes_to_mathml - elements = boxes._elements - elements, calc_dimensions = boxes._prepare_elements( - elements, options=options, neg_y=True - ) - xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() - data = (elements, xmin, xmax, ymin, ymax, w, h, width, height) - format_fn = lookup_method(boxes, "svg") - return format_fn(boxes, elements, data=data, options=options) + return format_fn(boxes, options=options) def test_svg_circle(): diff --git a/test/helper.py b/test/helper.py index f9df14b31..0baa2205c 100644 --- a/test/helper.py +++ b/test/helper.py @@ -126,10 +126,10 @@ def check_evaluation( print(time.asctime()) if failure_message: - print(f"got: {result}, expect: {expected} -- {failure_message}") + print(f"got: \n{result}\nexpect:\n{expected}\n -- {failure_message}") assert result == expected, failure_message else: - print(f"got: {result}, expect: {expected}") + print(f"got: \n{result}\nexpect:\n{expected}\n --") if isinstance(expected, re.Pattern): assert expected.match(result) else: diff --git a/test/test_evaluation.py b/test/test_evaluation.py index d21b7ac07..b4f3503f6 100644 --- a/test/test_evaluation.py +++ b/test/test_evaluation.py @@ -3,6 +3,8 @@ import pytest +from mathics import settings + from .helper import check_evaluation, evaluate, session @@ -25,15 +27,50 @@ (r"Sum[2^(-i), {i, 1, \[Infinity]}]", "1"), # Global System Information (r"Abs[$ByteOrdering]", "1"), - (r"Head[$CommandLine]", "List"), + pytest.param( + r"Head[$CommandLine]", + "List", + marks=pytest.mark.skipif( + not settings.ENABLE_SYSTEM_COMMANDS, + reason="In sandbox mode, $CommandLine returns {}", + ), + ), (r"Head[$Machine]", "String"), - (r"Head[$MachineName]", "String"), + pytest.param( + r"Head[$MachineName]", + "String", + marks=pytest.mark.skipif( + not settings.ENABLE_SYSTEM_COMMANDS, + reason="In sandbox mode, $MachineName returns $Failed", + ), + ), (r"""Length[Names["System`*"]] > 1024""", "True"), (r"Length[$Packages] >= 5", "True"), - (r"Head[$ParentProcessID]", "Integer"), - (r"Head[$ProcessID]", "Integer"), + pytest.param( + r"Head[$ParentProcessID]", + "Integer", + marks=pytest.mark.skipif( + not settings.ENABLE_SYSTEM_COMMANDS, + reason="In sandbox mode, $ParentProcessID returns $Failed", + ), + ), + pytest.param( + r"Head[$ProcessID]", + "Integer", + marks=pytest.mark.skipif( + not settings.ENABLE_SYSTEM_COMMANDS, + reason="In sandbox mode, $ProcessID returns $Failed", + ), + ), (r"Head[$ProcessorType]", "String"), - (r"Head[$ScriptCommandLine]", "List"), + pytest.param( + r"Head[$ScriptCommandLine]", + "List", + marks=pytest.mark.skipif( + not settings.ENABLE_SYSTEM_COMMANDS, + reason="In sandbox mode, $ScriptCommandLine returns {}", + ), + ), (r"Head[$SystemID]", "String"), (r"Head[$SystemWordLength]", "Integer"), # This doesn't work if not logged or in some OS's