diff --git a/mathics/builtin/drawing/graphics3d.py b/mathics/builtin/drawing/graphics3d.py index f7e9df4477..efbf2c4cbf 100644 --- a/mathics/builtin/drawing/graphics3d.py +++ b/mathics/builtin/drawing/graphics3d.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -Graphics (3D) +Three-Dimensional Graphics """ @@ -778,9 +778,6 @@ def __init__(self, content, evaluation, neg_y=False): def extent(self, completely_visible_only=False): return total_extent_3d([element.extent() for element in self.elements]) - def to_svg(self): - return "\n".join(element.to_svg() for element in self.elements) - def to_asy(self): return "\n".join([element.to_asy() for element in self.elements]) diff --git a/mathics/builtin/graphics.py b/mathics/builtin/graphics.py index fc0c9a0302..f5b0125923 100644 --- a/mathics/builtin/graphics.py +++ b/mathics/builtin/graphics.py @@ -7,7 +7,6 @@ from math import floor, ceil, log10, sin, cos, pi, sqrt, atan2, degrees, radians, exp -import json import base64 from itertools import chain @@ -33,6 +32,8 @@ system_symbols_dict, from_python, ) +from mathics.core.formatter import lookup_method + from mathics.builtin.drawing.colors import convert as convert_color from mathics.core.numbers import machine_epsilon @@ -323,7 +324,11 @@ def _extract_graphics(graphics, format, evaluation): if format == "asy": code = "\n".join(element.to_asy() for element in elements.elements) elif format == "svg": - code = elements.to_svg() + format_fn = lookup_method(elements, "svg") + if format_fn is not None: + code = format_fn(elements) + else: + code = elements.to_svg() else: raise NotImplementedError @@ -1171,20 +1176,27 @@ class Disk(Builtin): class Circle(Builtin): """
-
'Circle[{$cx$, $cy$}, $r$]' -
draws a circle with center '($cx$, $cy$)' and radius $r$. -
'Circle[{$cx$, $cy$}, {$rx$, $ry$}]' -
draws an ellipse. -
'Circle[{$cx$, $cy$}]' -
chooses radius 1. -
'Circle[]' -
chooses center '(0, 0)' and radius 1. +
'Circle[{$cx$, $cy$}, $r$]' +
draws a circle with center '($cx$, $cy$)' and radius $r$. + +
'Circle[{$cx$, $cy$}, {$rx$, $ry$}]' +
draws an ellipse. + +
'Circle[{$cx$, $cy$}]' +
chooses radius 1. + +
'Circle[]' +
chooses center '(0, 0)' and radius 1.
>> Graphics[{Red, Circle[{0, 0}, {2, 1}]}] = -Graphics- >> Graphics[{Circle[], Disk[{0, 0}, {1, 1}, {0, 2.1}]}] = -Graphics- + + Target practice: + >> Graphics[Circle[], Axes-> True] + = -Graphics- """ rules = {"Circle[]": "Circle[{0, 0}]"} @@ -1231,26 +1243,6 @@ def extent(self): ) return result - def to_svg(self, offset=None): - l = self.style.get_line_width(face_element=True) - x1, y1 = self.p1.pos() - x2, y2 = self.p2.pos() - xmin = min(x1, x2) - ymin = min(y1, y2) - w = max(x1, x2) - xmin - h = max(y1, y2) - ymin - if offset: - x1, x2 = x1 + offset[0], x2 + offset[0] - y1, y2 = y1 + offset[1], y2 + offset[1] - style = create_css(self.edge_color, self.face_color, l) - return '' % ( - xmin, - ymin, - w, - h, - style, - ) - def to_asy(self): l = self.style.get_line_width(face_element=True) x1, y1 = self.p1.pos() @@ -1302,21 +1294,6 @@ def extent(self): ry += l return [(x - rx, y - ry), (x - rx, y + ry), (x + rx, y - ry), (x + rx, y + ry)] - def to_svg(self, offset=None): - x, y = self.c.pos() - rx, ry = self.r.pos() - rx -= x - ry = y - ry - l = self.style.get_line_width(face_element=self.face_element) - style = create_css(self.edge_color, self.face_color, stroke_width=l) - return '' % ( - x, - y, - rx, - ry, - style, - ) - def to_asy(self): x, y = self.c.pos() rx, ry = self.r.pos() @@ -1389,27 +1366,33 @@ def _arc_params(self): return x, y, abs(rx), abs(ry), sx, sy, ex, ey, large_arc - def to_svg(self, offset=None): - if self.arc is None: - return super(_ArcBox, self).to_svg(offset) + # FIXME: Why do we need this? If so, + # figure out how to put in svg.py + # -------------------------------- + # def to_svg(self, offset=None): + # if self.arc is None: + # return super(_ArcBox, self).to_svg(offset) - x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() + # x, y, rx, ry, sx, sy, ex, ey, large_arc = self._arc_params() - def path(closed): - if closed: - yield "M %f,%f" % (x, y) - yield "L %f,%f" % (sx, sy) - else: - yield "M %f,%f" % (sx, sy) + # format_fn = lookup_method(self, "svg") + # if format_fn is not None: + # return format_fn(self, offset) + # def path(closed): + # if closed: + # yield "M %f,%f" % (x, y) + # yield "L %f,%f" % (sx, sy) + # else: + # yield "M %f,%f" % (sx, sy) - yield "A %f,%f,0,%d,0,%f,%f" % (rx, ry, large_arc, ex, ey) + # yield "A %f,%f,0,%d,0,%f,%f" % (rx, ry, large_arc, ex, ey) - if closed: - yield "Z" + # if closed: + # yield "Z" - l = self.style.get_line_width(face_element=self.face_element) - style = create_css(self.edge_color, self.face_color, stroke_width=l) - return '' % (" ".join(path(self.face_element)), style) + # l = self.style.get_line_width(face_element=self.face_element) + # style = create_css(self.edge_color, self.face_color, stroke_width=l) + # return '' % (" ".join(path(self.face_element)), style) def to_asy(self): if self.arc is None: @@ -1522,26 +1505,6 @@ def init(self, graphics, style, item=None): else: raise BoxConstructError - def to_svg(self, offset=None): - point_size, _ = self.style.get_style(PointSize, face_element=False) - if point_size is None: - point_size = PointSize(self.graphics, value=0.005) - size = point_size.get_size() - - style = create_css( - edge_color=self.edge_color, stroke_width=0, face_color=self.face_color - ) - svg = "" - for line in self.lines: - for coords in line: - svg += '' % ( - coords.pos()[0], - coords.pos()[1], - size, - style, - ) - return svg - def to_asy(self): pen = create_pens(face_color=self.face_color, is_face_element=False) @@ -1586,17 +1549,6 @@ def init(self, graphics, style, item=None, lines=None): else: raise BoxConstructError - def to_svg(self, offset=None): - l = self.style.get_line_width(face_element=False) - style = create_css(edge_color=self.edge_color, stroke_width=l) - svg = "" - for line in self.lines: - svg += '' % ( - " ".join(["%f,%f" % coords.pos() for coords in line]), - style, - ) - return svg - def to_asy(self): l = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=l) @@ -1741,16 +1693,6 @@ def init(self, graphics, style, item, options): raise BoxConstructError self.spline_degree = spline_degree.get_int_value() - def to_svg(self, offset=None): - l = self.style.get_line_width(face_element=False) - style = create_css(edge_color=self.edge_color, stroke_width=l) - - svg = "" - for line in self.lines: - s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line]))) - svg += '' % (s, style) - return svg - def to_asy(self): l = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=l) @@ -1828,22 +1770,6 @@ def parse_component(segments): else: raise BoxConstructError - def to_svg(self, offset=None): - l = self.style.get_line_width(face_element=False) - style = create_css( - edge_color=self.edge_color, face_color=self.face_color, stroke_width=l - ) - - def components(): - for component in self.components: - transformed = [(k, [xy.pos() for xy in p]) for k, p in component] - yield " ".join(_svg_bezier(*transformed)) + " Z" - - return '' % ( - " ".join(components()), - style, - ) - def to_asy(self): l = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=l) @@ -1933,32 +1859,6 @@ def process_option(self, name, value): else: raise BoxConstructError - def to_svg(self, offset=None): - l = self.style.get_line_width(face_element=True) - if self.vertex_colors is None: - face_color = self.face_color - else: - face_color = None - style = create_css( - edge_color=self.edge_color, face_color=face_color, stroke_width=l - ) - svg = "" - if self.vertex_colors is not None: - mesh = [] - for index, line in enumerate(self.lines): - data = [ - [coords.pos(), color.to_js()] - for coords, color in zip(line, self.vertex_colors[index]) - ] - mesh.append(data) - svg += '' % json.dumps(mesh) - for line in self.lines: - svg += '' % ( - " ".join("%f,%f" % coords.pos() for coords in line), - style, - ) - return svg - def to_asy(self): l = self.style.get_line_width(face_element=True) if self.vertex_colors is None: @@ -2514,23 +2414,6 @@ def draw(px, py, vx, vy, t1, s): return make - def to_svg(self, offset=None): - width = self.style.get_line_width(face_element=False) - style = create_css(edge_color=self.edge_color, stroke_width=width) - polyline = self.curve.make_draw_svg(style) - - arrow_style = create_css(face_color=self.edge_color, stroke_width=width) - - def polygon(points): - yield '' % arrow_style - - extent = self.graphics.view_width or 0 - default_arrow = self._default_arrow(polygon) - custom_arrow = self._custom_arrow("svg", _SVGTransform) - return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) - def to_asy(self): width = self.style.get_line_width(face_element=False) pen = create_pens(edge_color=self.edge_color, stroke_width=width) @@ -2617,36 +2500,6 @@ def extent(self): y = p[1] - h / 2.0 + opos[1] * h / 2.0 return [(x, y), (x + w, y + h)] - def to_svg(self, offset=None): - x, y = self.pos.pos() - if offset: - x = x + offset[0] - y = y + offset[1] - - if hasattr(self.content, "to_svg"): - content = self.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, - opacity=self.opacity, - ) - text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"' - # FIXME: don't hard code text_style_opts, but allow these to be adjustable. - text_style_opts = "text-anchor:middle; dominant-baseline:middle;" - content = self.content.boxes_to_text(evaluation=self.graphics.evaluation) - svg = f'{content}' - - # content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation) - # style = create_css(font_color=self.color) - # svg = ( - # '' - # "%s") - - return svg - def to_asy(self): x, y = self.pos.pos() content = self.content.boxes_to_tex(evaluation=self.graphics.evaluation) @@ -2960,9 +2813,6 @@ def extent(self, completely_visible_only=False): ymax *= 2 return xmin, xmax, ymin, ymax - def to_svg(self, offset=None): - return "\n".join(element.to_svg(offset) for element in self.elements) - def to_asy(self): return "\n".join(element.to_asy() for element in self.elements) @@ -3250,54 +3100,7 @@ def boxes_to_tex(self, leaves=None, **options): return tex - def to_svg(self, leaves=None, **options): - if not leaves: - leaves = self._leaves - - data = options.get("data", None) - if data: - elements, xmin, xmax, ymin, ymax, w, h, width, height = data - else: - elements, calc_dimensions = self._prepare_elements( - leaves, options, neg_y=True - ) - xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() - - elements.view_width = w - - svg = elements.to_svg(offset=options.get("offset", None)) - - if self.background_color is not None: - svg = '%s' % ( - xmin, - ymin, - w, - h, - self.background_color.to_css()[0], - svg, - ) - - xmin -= 1 - ymin -= 1 - w += 2 - h += 2 - - if options.get("noheader", False): - return svg - svg_xml = """ - - %s - - """ % ( - " ".join("%f" % t for t in (xmin, ymin, w, h)), - svg, - ) - return svg_xml # , width, height - - def boxes_to_mathml(self, leaves=None, **options): + def boxes_to_mathml(self, leaves=None, **options) -> str: if not leaves: leaves = self._leaves @@ -3305,7 +3108,11 @@ def boxes_to_mathml(self, leaves=None, **options): xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() data = (elements, xmin, xmax, ymin, ymax, w, h, width, height) - svg_xml = self.to_svg(leaves, data=data, **options) + # FIXME: SVG is the only thing we can convert MathML into. + # Handle other graphics formats. + format_fn = lookup_method(self, "svg") + svg_main = format_fn(self, leaves, data=data, **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 behaviour. # template = '' @@ -3318,9 +3125,10 @@ def boxes_to_mathml(self, leaves=None, **options): # int(height), int(width), int(height), - base64.b64encode(svg_xml.encode("utf8")).decode("utf8"), + base64.b64encode(svg_main.encode("utf8")).decode("utf8"), ) + # FIXME: this isn't always properly align with overlaid SVG plots def axis_ticks(self, xmin, xmax): def round_to_zero(value): if value == 0: diff --git a/mathics/builtin/inout.py b/mathics/builtin/inout.py index 9930eacb70..adf4b2ce6d 100644 --- a/mathics/builtin/inout.py +++ b/mathics/builtin/inout.py @@ -2063,25 +2063,25 @@ def apply_mathml(self, expr, evaluation) -> Expression: boxes = MakeBoxes(expr).evaluate(evaluation) try: - xml = boxes.boxes_to_mathml(evaluation=evaluation) + mathml = boxes.boxes_to_mathml(evaluation=evaluation) except BoxError: evaluation.message( "General", "notboxes", Expression("FullForm", boxes).evaluate(evaluation), ) - xml = "" - is_a_picture = xml[:6] == " Callable: + """ + Find a conversion method for `format` in self's class method resolution order. + """ + for cls in inspect.getmro(type(self)): + format_fn = format2fn.get((format, cls), None) + if format_fn is not None: + # print(f"format function: {format_fn.__name__} for {type(self).__name__}") + return format_fn + raise RuntimeError( + f"Can't find formatter {format_fn.__name__} for {type(self).__name__}" + ) + + +def add_conversion_fn(cls) -> None: + """Add to `format2fn` a mapping from a conversion type and builtin-class + to a conversion method. + + The conversion type is determined form the module name. + For example, in module mathics.formatter.svg the conversion + type is "svg". + + The conversion method is assumed to be a method in the caller's + module, and is derived from lowercasing `cls`. + + For example function arrowbox in module mathics.formatter.svg would be + the SVG conversion routine for class ArrowBox. + + We use frame introspection to get all of this done. + """ + fr = inspect.currentframe().f_back + module_dict = fr.f_globals + + # The last part of the module name is expected to be the conversion routine. + conversion_type = module_dict["__name__"].split(".")[-1] + + # Derive the conversion function from the passed-in class argument. + module_fn_name = cls.__name__.lower() + + # Finally register the mapping: (Builtin-class, conversion name) -> conversion_function. + format2fn[(conversion_type, cls)] = module_dict[module_fn_name] diff --git a/mathics/formatter/__init__.py b/mathics/formatter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mathics/formatter/svg.py b/mathics/formatter/svg.py new file mode 100644 index 0000000000..395c2cae74 --- /dev/null +++ b/mathics/formatter/svg.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +import json + +""" +Format a Mathics object as an SVG string +""" + +from mathics.builtin.drawing.graphics3d import Graphics3DElements +from mathics.builtin.graphics import ( + ArrowBox, + BezierCurveBox, + FilledCurveBox, + GraphicsBox, + GraphicsElements, + InsetBox, + LineBox, + PointBox, + PointSize, + PolygonBox, + RectangleBox, + _RoundBox, + _svg_bezier, + _SVGTransform, +) + +from mathics.core.formatter import lookup_method, add_conversion_fn + + +def create_css( + edge_color=None, face_color=None, stroke_width=None, font_color=None, opacity=1.0 +) -> str: + """ + Return a string suitable for CSS inclusion setting the various parameters passed. + """ + css = [] + if edge_color is not None: + color, stroke_opacity = edge_color.to_css() + css.append("stroke: %s" % color) + css.append("stroke-opacity: %s" % stroke_opacity) + else: + css.append("stroke: none") + if stroke_width is not None: + css.append("stroke-width: %fpx" % stroke_width) + if face_color is not None: + color, fill_opacity = face_color.to_css() + css.append("fill: %s" % color) + css.append("fill-opacity: %s" % fill_opacity) + else: + css.append("fill: none") + if font_color is not None: + color, _ = font_color.to_css() + css.append("color: %s" % color) + css.append("opacity: %s" % opacity) + return "; ".join(css) + + +def arrowbox(self, offset=None): + width = self.style.get_line_width(face_element=False) + style = create_css(edge_color=self.edge_color, stroke_width=width) + polyline = self.curve.make_draw_svg(style) + + arrow_style = create_css(face_color=self.edge_color, stroke_width=width) + + def polygon(points): + yield '' % arrow_style + + extent = self.graphics.view_width or 0 + default_arrow = self._default_arrow(polygon) + custom_arrow = self._custom_arrow("svg", _SVGTransform) + return "".join(self._draw(polyline, default_arrow, custom_arrow, extent)) + + +add_conversion_fn(ArrowBox) + + +def beziercurvebox(self, offset=None): + l = self.style.get_line_width(face_element=False) + style = create_css(edge_color=self.edge_color, stroke_width=l) + + svg = "" + for line in self.lines: + s = " ".join(_svg_bezier((self.spline_degree, [xy.pos() for xy in line]))) + svg += '' % (s, style) + # print("XXX bezier", svg) + return svg + + +add_conversion_fn(BezierCurveBox) + + +def filledcurvebox(self, offset=None): + l = self.style.get_line_width(face_element=False) + style = create_css( + edge_color=self.edge_color, face_color=self.face_color, stroke_width=l + ) + + def components(): + for component in self.components: + transformed = [(k, [xy.pos() for xy in p]) for k, p in component] + yield " ".join(_svg_bezier(*transformed)) + " Z" + + # print("XXX FilledcurveBox", components) + return '' % ( + " ".join(components()), + style, + ) + + +add_conversion_fn(FilledCurveBox) + +# FIXME figure out how we can add this. +def graphicsbox(self, leaves=None, **options) -> str: + if not leaves: + leaves = self._leaves + + data = options.get("data", None) + if data: + elements, xmin, xmax, ymin, ymax, w, h, width, height = data + else: + elements, calc_dimensions = self._prepare_elements( + leaves, options, neg_y=True + ) + xmin, xmax, ymin, ymax, w, h, width, height = calc_dimensions() + + elements.view_width = w + + format_fn = lookup_method(elements, "svg") + if format_fn is not None: + svg_body = format_fn(elements, offset=options.get("offset", None)) + else: + svg_body = elements.to_svg(offset=options.get("offset", None)) + + if self.background_color is not None: + # Wrap svg_elements in a rectangle + svg_body = '%s' % ( + xmin, + ymin, + w, + h, + self.background_color.to_css()[0], + svg_body, + ) + + xmin -= 1 + ymin -= 1 + w += 2 + h += 2 + + if options.get("noheader", False): + return svg_body + svg_main = """ + + %s + + """ % ( + " ".join("%f" % t for t in (xmin, ymin, w, h)), + svg_body, + ) + return svg_main # , width, height + + +add_conversion_fn(GraphicsBox) + + +def graphicselements(self, offset=None): + result = [] + for element in self.elements: + format_fn = lookup_method(element, "svg") + if format_fn is None: + result.append(element.to_svg(offset)) + else: + result.append(format_fn(element, offset)) + + return "\n".join(result) + + +add_conversion_fn(GraphicsElements) +graphics3delements = graphicselements + +add_conversion_fn(Graphics3DElements) + + +def insetbox(self, offset=None): + x, y = self.pos.pos() + if offset: + x = x + offset[0] + y = y + offset[1] + + if hasattr(self.content, "to_svg"): + content = self.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, + opacity=self.opacity, + ) + text_pos_opts = f'x="{x}" y="{y}" ox="{self.opos[0]}" oy="{self.opos[1]}"' + # FIXME: don't hard code text_style_opts, but allow these to be adjustable. + text_style_opts = "text-anchor:middle; dominant-baseline:middle;" + content = self.content.boxes_to_text(evaluation=self.graphics.evaluation) + svg = f'{content}' + + # content = self.content.boxes_to_mathml(evaluation=self.graphics.evaluation) + # style = create_css(font_color=self.color) + # svg = ( + # '' + # "%s") + + return svg + + +add_conversion_fn(InsetBox) + + +def linebox(self, offset=None): + l = self.style.get_line_width(face_element=False) + style = create_css(edge_color=self.edge_color, stroke_width=l) + svg = "" + for line in self.lines: + svg += '' % ( + " ".join(["%f,%f" % coords.pos() for coords in line]), + style, + ) + # print("XXX linebox", svg) + return svg + + +add_conversion_fn(LineBox) + + +def pointbox(self, offset=None): + point_size, _ = self.style.get_style(PointSize, face_element=False) + if point_size is None: + point_size = PointSize(self.graphics, value=0.005) + size = point_size.get_size() + + style = create_css( + edge_color=self.edge_color, stroke_width=0, face_color=self.face_color + ) + svg = "" + for line in self.lines: + for coords in line: + svg += '' % ( + coords.pos()[0], + coords.pos()[1], + size, + style, + ) + # print("XXX PointBox", svg) + return svg + + +add_conversion_fn(PointBox) + + +def polygonbox(self, offset=None): + l = self.style.get_line_width(face_element=True) + if self.vertex_colors is None: + face_color = self.face_color + else: + face_color = None + style = create_css( + edge_color=self.edge_color, face_color=face_color, stroke_width=l + ) + svg = "" + if self.vertex_colors is not None: + mesh = [] + for index, line in enumerate(self.lines): + data = [ + [coords.pos(), color.to_js()] + for coords, color in zip(line, self.vertex_colors[index]) + ] + mesh.append(data) + svg += '' % json.dumps(mesh) + for line in self.lines: + svg += '' % ( + " ".join("%f,%f" % coords.pos() for coords in line), + style, + ) + # print("XXX PolygonBox", svg) + return svg + + +add_conversion_fn(PolygonBox) + + +def rectanglebox(self, offset=None): + l = self.style.get_line_width(face_element=True) + x1, y1 = self.p1.pos() + x2, y2 = self.p2.pos() + xmin = min(x1, x2) + ymin = min(y1, y2) + w = max(x1, x2) - xmin + h = max(y1, y2) - ymin + if offset: + x1, x2 = x1 + offset[0], x2 + offset[0] + y1, y2 = y1 + offset[1], y2 + offset[1] + style = create_css(self.edge_color, self.face_color, l) + return '' % ( + xmin, + ymin, + w, + h, + style, + ) + "\n".join(element.to_svg() for element in self.elements) + + +add_conversion_fn(RectangleBox) + + +def _roundbox(self, offset=None): + x, y = self.c.pos() + rx, ry = self.r.pos() + rx -= x + ry = y - ry + l = self.style.get_line_width(face_element=self.face_element) + style = create_css(self.edge_color, self.face_color, stroke_width=l) + return '' % ( + x, + y, + rx, + ry, + style, + ) + + +add_conversion_fn(_RoundBox) diff --git a/setup.py b/setup.py index 6dfc62621e..bd5c73e12b 100644 --- a/setup.py +++ b/setup.py @@ -134,6 +134,7 @@ def subdirs(root, file="*.*", depth=10): "mathics.builtin.pympler", "mathics.builtin.specialfns", "mathics.doc", + "mathics.formatter", ], install_requires=INSTALL_REQUIRES, dependency_links=DEPENDENCY_LINKS, diff --git a/test/test_formatter.py b/test/test_formatter.py new file mode 100644 index 0000000000..998141a9b0 --- /dev/null +++ b/test/test_formatter.py @@ -0,0 +1,86 @@ +import re +from mathics.core.expression import Symbol, Integer0, Expression +from mathics.core.evaluation import Evaluation +from mathics.session import MathicsSession +from mathics.builtin.inout import MakeBoxes +from mathics.core.formatter import lookup_method + +session = MathicsSession(add_builtin=True, catch_interrupt=False) +evaluation = Evaluation(session.definitions) + +GraphicsSymbol = Symbol("Graphics") +ListSymbol = Symbol("List") + +svg_wrapper_pat = r"""^\s* +\s*', inner_svg + ) + assert matches + assert matches.group(1) == matches.group(2) == matches.group(3) + +def test_svg_point(): + expression = Expression( + GraphicsSymbol, + Expression("Point", Expression(ListSymbol, Integer0, Integer0)), + ) + + svg = get_svg(expression) + inner_svg = extract_svg_body(svg) + + # Circles are implemented as ellipses with equal major and minor axes. + # Check for that. + print(inner_svg) + matches = re.match( + r'^', inner_svg + ) + assert matches + assert matches.group(1) == matches.group(2) + + +if __name__ == "__main__": + test_svg_point()