diff --git a/test/classes.ipynb b/test/classes.ipynb
new file mode 100644
index 0000000..86255e1
--- /dev/null
+++ b/test/classes.ipynb
@@ -0,0 +1,125 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import ziamath as zm\n",
+ "from pathlib import Path\n",
+ "zm.config.svg2 = False\n",
+ "zm.config.math.fontsize = 50\n",
+ "zm.config.svg_classes = True\n",
+ "zm.config.svg_style = r'''\n",
+ ".mrow { fill:blue }\n",
+ ".mn { fill:magenta ; }\n",
+ ".mo { fill:yellow ; stroke:grey; stroke-width:1px;}\n",
+ ".mi { fill:url(#grad1) ; stroke:darkslategrey; stroke-width:1px;}\n",
+ ".background { fill:violet ; stroke:none}\n",
+ ".hline {fill:green}\n",
+ "'''\n",
+ "zm.config.svg_defs = r'''\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ "'''\n",
+ "mml = '''\n",
+ "\n",
+ "'''\n",
+ "m = zm.Math(mml)\n",
+ "# Path(f\"/tmp/math.svg\").write_text(zm.Math(mml).svg())\n",
+ "m"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "env2",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/ziamath/config.py b/ziamath/config.py
index 7cefe8a..1e3979b 100644
--- a/ziamath/config.py
+++ b/ziamath/config.py
@@ -1,4 +1,5 @@
-''' Global configuration options '''
+"""Global configuration options"""
+
from typing import Optional, Callable
from dataclasses import dataclass, field
@@ -22,19 +23,19 @@ def off(self):
@dataclass
class TextStyle:
textfont: Optional[str] = None
- variant: str = 'serif'
+ variant: str = "serif"
fontsize: float = 24
- color: str = 'black'
+ color: str = "black"
linespacing: float = 1
@dataclass
class MathStyle:
mathfont: Optional[str] = None
- variant: str = ''
+ variant: str = ""
fontsize: float = 24
- color: str = 'black'
- background: str = 'none'
+ color: str = ""
+ background: str = "none"
bold_font: Optional[str] = None
italic_font: Optional[str] = None
bolditalic_font: Optional[str] = None
@@ -42,22 +43,23 @@ class MathStyle:
@dataclass
class NumberingStyle:
- ''' Style for equation numbers
-
- Args:
- autonumber: Automatically number all equations
- format: String formatter for equation numbers
- format_func: Function to return a formatted equation label
- columnwidth: Width of column or page. Equation numbers
- right-aligned with the columnwidth.
- '''
+ """Style for equation numbers
+
+ Args:
+ autonumber: Automatically number all equations
+ format: String formatter for equation numbers
+ format_func: Function to return a formatted equation label
+ columnwidth: Width of column or page. Equation numbers
+ right-aligned with the columnwidth.
+ """
+
autonumber: bool = False
- format: str = '({0})'
+ format: str = "({0})"
format_func: Optional[Callable] = None
- columnwidth: str = '6.5in'
+ columnwidth: str = "6.5in"
def getlabel(self, i):
- ''' Get equation label for equation number i'''
+ """Get equation label for equation number i"""
if self.format_func:
return self.format_func(i)
return self.format.format(i)
@@ -65,25 +67,29 @@ def getlabel(self, i):
@dataclass
class Config:
- ''' Global configuration options for Ziamath
-
- Attributes
- ----------
- minsizefraction: Smallest allowed text size, as fraction of
- base size, for text such as subscripts and superscripts
- debug: Debug mode, draws bounding boxes around
- svg2: Use SVG2.0. Disable for better browser compatibility,
- at the expense of SVG size
- precision: SVG decimal precision for coordinates
- decimal_separator: Use `.` or `,` as decimal separator. (only
- affects Latex math)
- '''
+ """Global configuration options for Ziamath
+
+ Attributes
+ ----------
+ minsizefraction: Smallest allowed text size, as fraction of
+ base size, for text such as subscripts and superscripts
+ debug: Debug mode, draws bounding boxes around
+ svg2: Use SVG2.0. Disable for better browser compatibility,
+ at the expense of SVG size
+ precision: SVG decimal precision for coordinates
+ decimal_separator: Use `.` or `,` as decimal separator. (only
+ affects Latex math)
+ """
+
math: MathStyle = field(default_factory=MathStyle)
text: TextStyle = field(default_factory=TextStyle)
- minsizefraction: float = .3
- decimal_separator = '.'
+ minsizefraction: float = 0.3
+ decimal_separator = "."
debug: DebugConfig = field(default_factory=DebugConfig)
numbering: NumberingStyle = field(default_factory=NumberingStyle)
+ svg_classes: bool = False
+ svg_style: str = ""
+ svg_defs: str = ""
@property
def svg2(self) -> bool:
diff --git a/ziamath/drawable.py b/ziamath/drawable.py
index c6988df..c54ef52 100644
--- a/ziamath/drawable.py
+++ b/ziamath/drawable.py
@@ -10,212 +10,250 @@
class Drawable:
- ''' Base class for drawable nodes '''
- mtag = 'drawable'
+ """Base class for drawable nodes"""
+
+ mtag = "drawable"
nodes: list[Drawable] = []
def __init__(self):
self.bbox = BBox(0, 0, 0, 0)
def firstglyph(self) -> Optional[SimpleGlyph]:
- ''' Get the first glyph in this node '''
+ """Get the first glyph in this node"""
return None
def lastglyph(self) -> Optional[SimpleGlyph]:
- ''' Get the last glyph in this node '''
+ """Get the last glyph in this node"""
return None
def lastchar(self) -> Optional[str]:
- ''' Get the last character in this node '''
+ """Get the last character in this node"""
return None
def xadvance(self) -> float:
- ''' X-advance for the glyph. Usually bbox.xmax '''
+ """X-advance for the glyph. Usually bbox.xmax"""
return self.bbox.xmax
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the element. Must be subclassed. '''
+ """Draw the element. Must be subclassed."""
raise NotImplementedError
class Glyph(Drawable):
- ''' A single glyph
-
- Args:
- glyph: The glyph to draw
- char: unicode character represented by the glyph
- size: point size
- style: font MathStyle
- '''
- def __init__(self, glyph: SimpleGlyph, char: str, size: float,
- style: Optional[MathStyle] = None, **kwargs):
+ """A single glyph
+
+ Args:
+ glyph: The glyph to draw
+ char: unicode character represented by the glyph
+ size: point size
+ style: font MathStyle
+ """
+
+ def __init__(
+ self,
+ glyph: SimpleGlyph,
+ char: str,
+ size: float,
+ style: Optional[MathStyle] = None,
+ **kwargs,
+ ):
super().__init__()
self.glyph = glyph
self.char = char
self.size = size
- self.phantom = kwargs.get('phantom', False)
+ self.phantom = kwargs.get("phantom", False)
self.style = style if style else MathStyle()
self._funits_to_pts = self.size / self.glyph.font.info.layout.unitsperem
self.bbox = BBox(
self.funit_to_points(self.glyph.path.bbox.xmin),
self.funit_to_points(self.glyph.path.bbox.xmax),
self.funit_to_points(self.glyph.path.bbox.ymin),
- self.funit_to_points(self.glyph.path.bbox.ymax))
-
+ self.funit_to_points(self.glyph.path.bbox.ymax),
+ )
+
def funit_to_points(self, value: float) -> float:
- ''' Convert font units to SVG points '''
+ """Convert font units to SVG points"""
return value * self._funits_to_pts
def firstglyph(self) -> Optional[SimpleGlyph]:
- ''' Get the first glyph in this node '''
+ """Get the first glyph in this node"""
return self.glyph
def lastglyph(self) -> Optional[SimpleGlyph]:
- ''' Get the last glyph in this node '''
+ """Get the last glyph in this node"""
return self.glyph
def lastchar(self) -> Optional[str]:
- ''' Get the last character in this node '''
+ """Get the last character in this node"""
return self.char
def xadvance(self) -> float:
- ''' X-advance for the glyph. Usually bbox.xmax '''
+ """X-advance for the glyph. Usually bbox.xmax"""
return self.funit_to_points(self.glyph.advance())
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the node on the SVG
-
- Args:
- x: Horizontal position in SVG coordinates
- y: Vertical position in SVG coordinates
- svg: SVG drawing as XML
- '''
- symbols = svg.findall('symbol')
- symids = [sym.attrib.get('id') for sym in symbols]
+ """Draw the node on the SVG
+
+ Args:
+ x: Horizontal position in SVG coordinates
+ y: Vertical position in SVG coordinates
+ svg: SVG drawing as XML
+ """
+ symbols = svg.findall("symbol")
+ symids = [sym.attrib.get("id") for sym in symbols]
if self.glyph.id not in symids and config.svg2:
svg.append(self.glyph.svgsymbol())
if not self.phantom:
path = self.glyph.place(x, y, self.size)
if path is not None:
svg.append(path)
+ if config.svg_classes:
+ pass
if self.style.mathcolor and len(svg) > 0:
- svg[-1].set('fill', str(self.style.mathcolor))
+ svg[-1].set("fill", str(self.style.mathcolor))
x += self.funit_to_points(self.glyph.advance())
return x, y
class HLine(Drawable):
- ''' Horizontal Line. '''
- def __init__(self, length: float, lw: float,
- style: Optional[MathStyle] = None, **kwargs):
+ """Horizontal Line."""
+
+ def __init__(
+ self, length: float, lw: float, style: Optional[MathStyle] = None, **kwargs
+ ):
super().__init__()
self.length = length
self.lw = lw
- self.phantom = kwargs.get('phantom', False)
- self.bbox = BBox(0, self.length, -self.lw/2, self.lw/2)
+ self.phantom = kwargs.get("phantom", False)
+ self.bbox = BBox(0, self.length, -self.lw / 2, self.lw / 2)
self.style = style if style else MathStyle()
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the node on the SVG
+ """Draw the node on the SVG
- Args:
- x: Horizontal position in SVG coordinates
- y: Vertical position in SVG coordinates
- svg: SVG drawing as XML
- '''
+ Args:
+ x: Horizontal position in SVG coordinates
+ y: Vertical position in SVG coordinates
+ svg: SVG drawing as XML
+ """
if not self.phantom:
# Use rectangle so it can change color with 'fill' attribute
# and not mess up glyphs with 'stroke' attribute
- bar = ET.SubElement(svg, 'rect')
- bar.attrib['x'] = fmt(x)
- bar.attrib['y'] = fmt(y)
- bar.attrib['width'] = fmt(self.length)
- bar.attrib['height'] = fmt(self.lw)
+ bar = ET.SubElement(svg, "rect")
+ bar.attrib["x"] = fmt(x)
+ bar.attrib["y"] = fmt(y)
+ bar.attrib["width"] = fmt(self.length)
+ bar.attrib["height"] = fmt(self.lw)
+ if config.svg_classes:
+ bar.set("class", "hline")
if self.style.mathcolor:
- bar.attrib['fill'] = str(self.style.mathcolor)
- return x+self.length, y
+ bar.attrib["fill"] = str(self.style.mathcolor)
+ return x + self.length, y
class VLine(Drawable):
- ''' Vertical Line. '''
- def __init__(self, height: float, lw: float,
- style: Optional[MathStyle] = None, **kwargs):
+ """Vertical Line."""
+
+ def __init__(
+ self, height: float, lw: float, style: Optional[MathStyle] = None, **kwargs
+ ):
super().__init__()
self.height = height
self.lw = lw
- self.phantom = kwargs.get('phantom', False)
+ self.phantom = kwargs.get("phantom", False)
self.bbox = BBox(0, self.lw, 0, self.height)
self.style = style if style else MathStyle()
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the node on the SVG
+ """Draw the node on the SVG
- Args:
- x: Horizontal position in SVG coordinates
- y: Vertical position in SVG coordinates
- svg: SVG drawing as XML
- '''
+ Args:
+ x: Horizontal position in SVG coordinates
+ y: Vertical position in SVG coordinates
+ svg: SVG drawing as XML
+ """
if not self.phantom:
# Use rectangle so it can change color with 'fill' attribute
# and not mess up glyphs with 'stroke' attribute
- bar = ET.SubElement(svg, 'rect')
- bar.attrib['x'] = fmt(x-self.lw/2)
- bar.attrib['y'] = fmt(y)
- bar.attrib['width'] = fmt(self.lw)
- bar.attrib['height'] = fmt(self.height)
+ bar = ET.SubElement(svg, "rect")
+ bar.attrib["x"] = fmt(x - self.lw / 2)
+ bar.attrib["y"] = fmt(y)
+ bar.attrib["width"] = fmt(self.lw)
+ bar.attrib["height"] = fmt(self.height)
+ if config.svg_classes:
+ bar.set("class", "vline")
if self.style.mathcolor:
- bar.attrib['fill'] = str(self.style.mathcolor)
+ bar.attrib["fill"] = str(self.style.mathcolor)
return x, y
class Box(Drawable):
- ''' Box '''
- def __init__(self, width: float, height: float, lw: float,
- cornerradius: Optional[float] = None,
- style: Optional[MathStyle] = None, **kwargs):
+ """Box"""
+
+ def __init__(
+ self,
+ width: float,
+ height: float,
+ lw: float,
+ cornerradius: Optional[float] = None,
+ style: Optional[MathStyle] = None,
+ **kwargs,
+ ):
super().__init__()
self.width = width
self.height = height
self.cornerradius = cornerradius
self.lw = lw
- self.phantom = kwargs.get('phantom', False)
+ self.phantom = kwargs.get("phantom", False)
self.bbox = BBox(0, self.width, 0, self.height)
self.style = style if style else MathStyle()
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the node on the SVG
+ """Draw the node on the SVG
- Args:
- x: Horizontal position in SVG coordinates
- y: Vertical position in SVG coordinates
- svg: SVG drawing as XML
- '''
+ Args:
+ x: Horizontal position in SVG coordinates
+ y: Vertical position in SVG coordinates
+ svg: SVG drawing as XML
+ """
if not self.phantom:
- bar = ET.SubElement(svg, 'rect')
- bar.set('x', fmt(x))
- bar.set('y', fmt(y-self.height))
- bar.set('width', fmt(self.width))
- bar.set('height', fmt(self.height))
- bar.set('stroke-width', fmt(self.lw))
- bar.set('stroke', self.style.mathcolor)
- bar.set('fill', self.style.mathbackground)
+ bar = ET.SubElement(svg, "rect")
+ bar.set("x", fmt(x))
+ bar.set("y", fmt(y - self.height))
+ bar.set("width", fmt(self.width))
+ bar.set("height", fmt(self.height))
+ if config.svg_classes:
+ bar.set("class", "box")
+ if self.lw >= 0:
+ bar.set("stroke-width", fmt(self.lw))
+ if self.style.mathcolor:
+ bar.set("stroke", self.style.mathcolor)
+ if self.style.mathbackground:
+ bar.set("fill", self.style.mathbackground)
if self.cornerradius:
- bar.set('rx', fmt(self.cornerradius))
+ bar.set("rx", fmt(self.cornerradius))
- return x+self.width, y
+ return x + self.width, y
class Diagonal(Drawable):
- ''' Diagonal Line - corners of Box '''
- def __init__(self, width: float, height: float, lw: float,
- arrow: bool = False,
- style: Optional[MathStyle] = None, **kwargs):
+ """Diagonal Line - corners of Box"""
+
+ def __init__(
+ self,
+ width: float,
+ height: float,
+ lw: float,
+ arrow: bool = False,
+ style: Optional[MathStyle] = None,
+ **kwargs,
+ ):
super().__init__()
self.width = width
self.height = height
self.lw = lw
self.arrow = arrow
- self.phantom = kwargs.get('phantom', False)
+ self.phantom = kwargs.get("phantom", False)
self.bbox = BBox(0, self.width, 0, self.height)
self.style = style if style else MathStyle()
@@ -224,67 +262,85 @@ def __init__(self, width: float, height: float, lw: float,
if self.arrow:
# Bbox needs to be a bit bigger to accomodate arrowhead
theta = math.atan2(-self.height, self.width)
- self.arroww = (10+self.lw*2) * math.cos(theta)
- self.arrowh = (10+self.lw*2) * math.sin(theta)
+ self.arroww = (10 + self.lw * 2) * math.cos(theta)
+ self.arrowh = (10 + self.lw * 2) * math.sin(theta)
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the node on the SVG
+ """Draw the node on the SVG
- Args:
- x: Horizontal position in SVG coordinates
- y: Vertical position in SVG coordinates
- svg: SVG drawing as XML
- '''
+ Args:
+ x: Horizontal position in SVG coordinates
+ y: Vertical position in SVG coordinates
+ svg: SVG drawing as XML
+ """
if not self.phantom:
- bar = ET.SubElement(svg, 'path')
+ bar = ET.SubElement(svg, "path")
if self.arrow:
- arrowdef = ET.SubElement(svg, 'defs')
- marker = ET.SubElement(arrowdef, 'marker')
- marker.set('id', 'arrowhead')
- marker.set('markerWidth', '10')
- marker.set('markerHeight', '7')
- marker.set('refX', '0')
- marker.set('refY', '3.5')
- marker.set('orient', 'auto')
- poly = ET.SubElement(marker, 'polygon')
- poly.set('points', '0 0 10 3.5 0 7')
-
- bar.set('d', f'M {fmt(x)} {fmt(y-self.height)} L {fmt(x+self.width)} {fmt(y)}')
- bar.set('stroke-width', fmt(self.lw))
- bar.set('stroke', self.style.mathcolor)
+ arrowdef = ET.SubElement(svg, "defs")
+ marker = ET.SubElement(arrowdef, "marker")
+ marker.set("id", "arrowhead")
+ marker.set("markerWidth", "10")
+ marker.set("markerHeight", "7")
+ marker.set("refX", "0")
+ marker.set("refY", "3.5")
+ marker.set("orient", "auto")
+ poly = ET.SubElement(marker, "polygon")
+ poly.set("points", "0 0 10 3.5 0 7")
+
+ bar.set(
+ "d", f"M {fmt(x)} {fmt(y-self.height)} L {fmt(x+self.width)} {fmt(y)}"
+ )
+ if self.lw >= 0:
+ bar.set("stroke-width", fmt(self.lw))
+ if self.style.mathcolor:
+ bar.set("stroke", self.style.mathcolor)
if self.arrow:
- bar.set('marker-end', 'url(#arrowhead)')
+ bar.set("marker-end", "url(#arrowhead)")
+ if config.svg_classes:
+ bar.set("class", "dline")
- return x+self.width, y
+ return x + self.width, y
class Ellipse(Drawable):
- ''' Ellipse '''
- def __init__(self, width: float, height: float, lw: float,
- style: Optional[MathStyle] = None, **kwargs):
+ """Ellipse"""
+
+ def __init__(
+ self,
+ width: float,
+ height: float,
+ lw: float,
+ style: Optional[MathStyle] = None,
+ **kwargs,
+ ):
super().__init__()
self.width = width
self.height = height
self.lw = lw
- self.phantom = kwargs.get('phantom', False)
+ self.phantom = kwargs.get("phantom", False)
self.bbox = BBox(0, self.width, 0, self.height)
self.style = style if style else MathStyle()
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the node on the SVG
+ """Draw the node on the SVG
- Args:
- x: Horizontal position in SVG coordinates
- y: Vertical position in SVG coordinates
- svg: SVG drawing as XML
- '''
+ Args:
+ x: Horizontal position in SVG coordinates
+ y: Vertical position in SVG coordinates
+ svg: SVG drawing as XML
+ """
if not self.phantom:
- bar = ET.SubElement(svg, 'ellipse')
- bar.set('cx', fmt(x+self.width/2))
- bar.set('cy', fmt(y-self.height/2))
- bar.set('rx', fmt(self.width/2))
- bar.set('ry', fmt(self.height/2))
- bar.set('stroke-width', fmt(self.lw))
- bar.set('stroke', self.style.mathcolor)
- bar.set('fill', self.style.mathbackground)
- return x+self.width, y
+ bar = ET.SubElement(svg, "ellipse")
+ bar.set("cx", fmt(x + self.width / 2))
+ bar.set("cy", fmt(y - self.height / 2))
+ bar.set("rx", fmt(self.width / 2))
+ bar.set("ry", fmt(self.height / 2))
+ if config.svg_classes:
+ bar.set("class", "ellipse")
+ if self.lw >= 0:
+ bar.set("stroke-width", fmt(self.lw))
+ if self.style.mathcolor:
+ bar.set("stroke", self.style.mathcolor)
+ if self.style.mathbackground:
+ bar.set("fill", self.style.mathbackground)
+ return x + self.width, y
diff --git a/ziamath/nodes/mnode.py b/ziamath/nodes/mnode.py
index 1deb884..f6c3a31 100644
--- a/ziamath/nodes/mnode.py
+++ b/ziamath/nodes/mnode.py
@@ -1,4 +1,5 @@
-''' Math node - parent class of all math nodes '''
+"""Math node - parent class of all math nodes"""
+
from __future__ import annotations
from typing import Optional, MutableMapping, Type
import logging
@@ -14,20 +15,21 @@
from .. import operators
from .nodetools import elementtext, infer_opform
-_node_classes: dict[str, Type['Mnode']] = {}
+_node_classes: dict[str, Type["Mnode"]] = {}
class Mnode(Drawable):
- ''' Math Drawing Node
+ """Math Drawing Node
- Args:
- element: XML element for the node
- size: base font size in points
- parent: Mnode of parent
- '''
- mtag = 'mnode'
+ Args:
+ element: XML element for the node
+ size: base font size in points
+ parent: Mnode of parent
+ """
+
+ mtag = "mnode"
- def __init__(self, element: ET.Element, parent: 'Mnode', **kwargs):
+ def __init__(self, element: ET.Element, parent: "Mnode", **kwargs):
super().__init__()
self.element = element
self.font: MathFont = parent.font
@@ -38,8 +40,11 @@ def __init__(self, element: ET.Element, parent: 'Mnode', **kwargs):
self.nodes: list[Drawable] = []
self.nodexy: list[tuple[float, float]] = []
self.glyphsize = max(
- self.size * (self.font.math.consts.scriptPercentScaleDown/100)**self.style.scriptlevel,
- self.font.basesize*config.minsizefraction)
+ self.size
+ * (self.font.math.consts.scriptPercentScaleDown / 100)
+ ** self.style.scriptlevel,
+ self.font.basesize * config.minsizefraction,
+ )
if self.style.mathsize:
self.glyphsize = self.size_px(self.style.mathsize)
@@ -48,58 +53,58 @@ def __init__(self, element: ET.Element, parent: 'Mnode', **kwargs):
self.bbox = BBox(0, 0, 0, 0)
def __init_subclass__(cls, tag: str) -> None:
- ''' Register this subclass so fromelement() can find it '''
+ """Register this subclass so fromelement() can find it"""
_node_classes[tag] = cls
cls.mtag = tag
@classmethod
- def fromelement(cls, element: ET.Element, parent: 'Mnode', **kwargs) -> 'Mnode':
- ''' Construct a new node from the element and its parent '''
- if element.tag in ['math', 'mtd', 'mtr', 'none']:
- element.tag = 'mrow'
- elif element.tag == 'ms':
- element.tag = 'mtext'
- elif element.tag == 'mi' and elementtext(element) in operators.names:
+ def fromelement(cls, element: ET.Element, parent: "Mnode", **kwargs) -> "Mnode":
+ """Construct a new node from the element and its parent"""
+ if element.tag in ["math", "mtd", "mtr", "none"]:
+ element.tag = "mrow"
+ elif element.tag == "ms":
+ element.tag = "mtext"
+ elif element.tag == "mi" and elementtext(element) in operators.names:
# Workaround for some latex2mathml operators coming back as identifiers
- element.tag = 'mo'
+ element.tag = "mo"
- if element.tag == 'mo':
+ if element.tag == "mo":
infer_opform(0, element, parent)
node = _node_classes.get(element.tag, None)
if node:
return node(element, parent, **kwargs)
- logging.warning('Undefined element %s', element)
- return _node_classes['mrow'](element, parent, **kwargs)
+ logging.warning("Undefined element %s", element)
+ return _node_classes["mrow"](element, parent, **kwargs)
def _setup(self, **kwargs) -> None:
- ''' Calculate node position assuming this node is at 0, 0. Also set bbox. '''
+ """Calculate node position assuming this node is at 0, 0. Also set bbox."""
self.bbox = BBox(0, 0, 0, 0)
def units_to_points(self, value: float) -> float:
- ''' Convert value in font units to points at this glyph size '''
+ """Convert value in font units to points at this glyph size"""
return value * self._glyph_pts_per_unit
def font_units_to_points(self, value: float) -> float:
- ''' Convert value in font units to points at the base font size '''
+ """Convert value in font units to points at the base font size"""
return value * self._font_pts_per_unit
def points_to_units(self, value: float) -> float:
- ''' Convert points back to font units '''
+ """Convert points back to font units"""
return value / self._glyph_pts_per_unit
def increase_child_scriptlevel(self, element: ET.Element, n: int = 1) -> None:
- ''' Increase the child element's script level one higher
- than this element, if not overridden in child's attributes
- '''
- element.attrib.setdefault('scriptlevel', str(self.style.scriptlevel+n))
+ """Increase the child element's script level one higher
+ than this element, if not overridden in child's attributes
+ """
+ element.attrib.setdefault("scriptlevel", str(self.style.scriptlevel + n))
def leftsibling(self) -> Optional[Drawable]:
- ''' Left node sibling. The one that was just placed. '''
+ """Left node sibling. The one that was just placed."""
try:
node = self.parent.nodes[-1]
- if node.mtag == 'mrow' and node.nodes:
+ if node.mtag == "mrow" and node.nodes:
node = node.nodes[-1]
except (IndexError, AttributeError):
node = None
@@ -107,50 +112,51 @@ def leftsibling(self) -> Optional[Drawable]:
return node
def firstglyph(self) -> Optional[SimpleGlyph]:
- ''' Get the first glyph in this node '''
+ """Get the first glyph in this node"""
try:
return self.nodes[0].firstglyph()
except IndexError:
return None
def lastglyph(self) -> Optional[SimpleGlyph]:
- ''' Get the last glyph in this node '''
+ """Get the last glyph in this node"""
try:
return self.nodes[-1].lastglyph()
except IndexError:
return None
def lastchar(self) -> Optional[str]:
- ''' Get the last character in this node '''
+ """Get the last character in this node"""
try:
return self.nodes[-1].lastchar()
except IndexError:
return None
def size_px(self, size: str, fontsize: Optional[float] = None) -> float:
- ''' Get size in points from the attribute string '''
+ """Get size in points from the attribute string"""
if fontsize is None:
fontsize = self.glyphsize
- numsize = {"veryverythinmathspace": f'{1/18}em',
- "verythinmathspace": f'{2/18}em',
- "thinmathspace": f'{3/18}em',
- "mediummathspace": f'{4/18}em',
- "thickmathspace": f'{5/18}em',
- "verythickmathspace": f'{6/18}em',
- "veryverythickmathspace": f'{7/18}em',
- "negativeveryverythinmathspace": f'{-1/18}em',
- "negativeverythinmathspace": f'{-2/18}em',
- "negativethinmathspace": f'{-3/18}em',
- "negativemediummathspace": f'{-4/18}em',
- "negativethickmathspace": f'{-5/18}em',
- "negativeverythickmathspace": f'{-6/18}em',
- "negativeveryverythickmathspace": f'{-7/18}em',
- }.get(size, size)
+ numsize = {
+ "veryverythinmathspace": f"{1/18}em",
+ "verythinmathspace": f"{2/18}em",
+ "thinmathspace": f"{3/18}em",
+ "mediummathspace": f"{4/18}em",
+ "thickmathspace": f"{5/18}em",
+ "verythickmathspace": f"{6/18}em",
+ "veryverythickmathspace": f"{7/18}em",
+ "negativeveryverythinmathspace": f"{-1/18}em",
+ "negativeverythinmathspace": f"{-2/18}em",
+ "negativethinmathspace": f"{-3/18}em",
+ "negativemediummathspace": f"{-4/18}em",
+ "negativethickmathspace": f"{-5/18}em",
+ "negativeverythickmathspace": f"{-6/18}em",
+ "negativeveryverythickmathspace": f"{-7/18}em",
+ }.get(size, size)
try:
# Plain number, or value in px
- pxsize = float(numsize.rstrip('px'))
+ pxsize = float(numsize.rstrip("px"))
except ValueError as exc:
pass
else:
@@ -166,57 +172,67 @@ def size_px(self, size: str, fontsize: Optional[float] = None) -> float:
# Conversion values from:
# https://tex.stackexchange.com/questions/8260/what-are-the-various-units-ex-em-in-pt-bp-dd-pc-expressed-in-mm
UNITS_TO_PT = {
- 'pt': 1,
- 'mm': 2.84526,
- 'cm': 28.45274,
- 'ex': 4.30554,
- 'em': 10.00002,
- 'bp': 1.00374,
- 'dd': 1.07,
- 'pc': 12,
- 'in': 72.27,
- 'mu': 0.5555,
+ "pt": 1,
+ "mm": 2.84526,
+ "cm": 28.45274,
+ "ex": 4.30554,
+ "em": 10.00002,
+ "bp": 1.00374,
+ "dd": 1.07,
+ "pc": 12,
+ "in": 72.27,
+ "mu": 0.5555,
}
# Convert units to points, then to pixels (= 1.333 px/pt)
pxsize = value * UNITS_TO_PT.get(units, 0) * 1.333
- if units in ['em', 'ex', 'mu']:
+ if units in ["em", "ex", "mu"]:
# These are fontsize dependent, table is based
# on 10-point font
- pxsize *= fontsize/10
+ pxsize *= fontsize / 10
return pxsize
def draw(self, x: float, y: float, svg: ET.Element) -> tuple[float, float]:
- ''' Draw the node on the SVG
+ """Draw the node on the SVG
+
+ Args:
+ x: Horizontal position in SVG coordinates
+ y: Vertical position in SVG coordinates
+ svg: SVG drawing as XML
+ """
+ if config.svg_classes:
+ g = ET.SubElement(svg, "g")
+ g.set("class", self.element.tag)
+ svg = g
- Args:
- x: Horizontal position in SVG coordinates
- y: Vertical position in SVG coordinates
- svg: SVG drawing as XML
- '''
if config.debug.bbox:
- rect = ET.SubElement(svg, 'rect')
- rect.set('x', fmt(x + self.bbox.xmin))
- rect.set('y', fmt(y - self.bbox.ymax))
- rect.set('width', fmt((self.bbox.xmax - self.bbox.xmin)))
- rect.set('height', fmt((self.bbox.ymax - self.bbox.ymin)))
- rect.set('fill', 'none')
- rect.set('stroke', 'blue')
- rect.set('stroke-width', '0.2')
+ rect = ET.SubElement(svg, "rect")
+ rect.set("x", fmt(x + self.bbox.xmin))
+ rect.set("y", fmt(y - self.bbox.ymax))
+ rect.set("width", fmt((self.bbox.xmax - self.bbox.xmin)))
+ rect.set("height", fmt((self.bbox.ymax - self.bbox.ymin)))
+ rect.set("fill", "none")
+ rect.set("stroke", "blue")
+ rect.set("stroke-width", "0.2")
+ if config.svg_classes:
+ rect.set("class", "bbox")
if config.debug.baseline:
- base = ET.SubElement(svg, 'path')
- base.set('d', f'M {x} 0 L {x+self.bbox.xmax} 0')
- base.set('stroke', 'red')
-
- if self.style.mathbackground not in ['none', None]:
- rect = ET.SubElement(svg, 'rect')
- rect.set('x', fmt(x + self.bbox.xmin))
- rect.set('y', fmt(y - self.bbox.ymax))
- rect.set('width', fmt((self.bbox.xmax - self.bbox.xmin)))
- rect.set('height', fmt((self.bbox.ymax - self.bbox.ymin)))
- rect.set('fill', str(self.style.mathbackground))
-
- nodex = nodey = 0.
+ base = ET.SubElement(svg, "path")
+ base.set("d", f"M {x} 0 L {x+self.bbox.xmax} 0")
+ base.set("stroke", "red")
+ if config.svg_classes:
+ rect.set("class", "baseline")
+ if self.style.mathbackground not in ["none", None]:
+ rect = ET.SubElement(svg, "rect")
+ rect.set("x", fmt(x + self.bbox.xmin))
+ rect.set("y", fmt(y - self.bbox.ymax))
+ rect.set("width", fmt((self.bbox.xmax - self.bbox.xmin)))
+ rect.set("height", fmt((self.bbox.ymax - self.bbox.ymin)))
+ if self.style.mathbackground:
+ rect.set("fill", str(self.style.mathbackground))
+ if config.svg_classes:
+ rect.set("class", "background")
+ nodex = nodey = 0.0
for (nodex, nodey), node in zip(self.nodexy, self.nodes):
- node.draw(x+nodex, y+nodey, svg)
- return x+nodex, y+nodey
+ node.draw(x + nodex, y + nodey, svg)
+ return x + nodex, y + nodey
diff --git a/ziamath/styles.py b/ziamath/styles.py
index 1989012..7ea4e2f 100644
--- a/ziamath/styles.py
+++ b/ziamath/styles.py
@@ -1,7 +1,8 @@
-''' Apply italic, bold, and other font styles by shifting the unstyled ASCII
- characters [A-Z, a-z, and 0-9] to their higher unicode alternatives. Note
- this does not check whether the new character glyph exists in the font.
-'''
+"""Apply italic, bold, and other font styles by shifting the unstyled ASCII
+characters [A-Z, a-z, and 0-9] to their higher unicode alternatives. Note
+this does not check whether the new character glyph exists in the font.
+"""
+
from __future__ import annotations
from typing import Optional, Any, MutableMapping
from collections import ChainMap, namedtuple
@@ -11,14 +12,15 @@
from .config import config
-VARIANTS = ['serif', 'sans', 'script', 'double', 'mono', 'fraktur']
-Styletype = namedtuple('Styletype', 'bold italic')
+VARIANTS = ["serif", "sans", "script", "double", "mono", "fraktur"]
+Styletype = namedtuple("Styletype", "bold italic")
@dataclass
class MathVariant:
- ''' Math font variant, such as serif, sans, script, italic, etc. '''
- style: str = 'serif'
+ """Math font variant, such as serif, sans, script, italic, etc."""
+
+ style: str = "serif"
italic: bool = False
bold: bool = False
normal: bool = False
@@ -26,22 +28,23 @@ class MathVariant:
@dataclass
class MathStyle:
- ''' Math Style parameters '''
+ """Math Style parameters"""
+
mathvariant: MathVariant = field(default_factory=MathVariant)
displaystyle: bool = True
- mathcolor: str = 'black'
- mathbackground: str = 'none'
- mathsize: str = ''
+ mathcolor: str = ""
+ mathbackground: str = "none"
+ mathsize: str = ""
scriptlevel: int = 0
def parse_variant(variant: str, parent_variant: MathVariant) -> MathVariant:
- ''' Extract mathvariant from MathML attribute and parent's variant '''
- bold = True if 'bold' in variant else parent_variant.bold
- italic = True if 'italic' in variant else parent_variant.italic
- normal = True if 'normal' in variant else parent_variant.normal
+ """Extract mathvariant from MathML attribute and parent's variant"""
+ bold = True if "bold" in variant else parent_variant.bold
+ italic = True if "italic" in variant else parent_variant.italic
+ normal = True if "normal" in variant else parent_variant.normal
- variant = variant.replace('bold', '').replace('italic', '').strip()
+ variant = variant.replace("bold", "").replace("italic", "").strip()
if variant in VARIANTS:
style = variant
else:
@@ -51,17 +54,19 @@ def parse_variant(variant: str, parent_variant: MathVariant) -> MathVariant:
def parse_displaystyle(params: MutableMapping[str, Any]) -> bool:
- ''' Extract displaystyle mode from MathML attributes '''
+ """Extract displaystyle mode from MathML attributes"""
dstyle = True
- if 'displaystyle' in params:
- dstyle = params.get('displaystyle') in ['true', True]
- elif 'display' in params:
- dstyle = params.get('display', 'block') != 'inline'
+ if "displaystyle" in params:
+ dstyle = params.get("displaystyle") in ["true", True]
+ elif "display" in params:
+ dstyle = params.get("display", "block") != "inline"
return dstyle
-def parse_style(element: ET.Element, parent_style: Optional[MathStyle] = None) -> MathStyle:
- ''' Read element style attributes into MathStyle '''
+def parse_style(
+ element: ET.Element, parent_style: Optional[MathStyle] = None
+) -> MathStyle:
+ """Read element style attributes into MathStyle"""
params: MutableMapping[str, Any]
if parent_style:
params = ChainMap(element.attrib, asdict(parent_style))
@@ -71,173 +76,200 @@ def parse_style(element: ET.Element, parent_style: Optional[MathStyle] = None) -
parent_variant = MathVariant()
args: dict[str, Any] = {}
- args['mathcolor'] = params.get('mathcolor', config.math.color)
- args['mathbackground'] = params.get('mathbackground', config.math.background)
- args['mathsize'] = params.get('mathsize', '')
- args['scriptlevel'] = int(params.get('scriptlevel', 0))
- args['mathvariant'] = parse_variant(element.attrib.get('mathvariant', config.math.variant), parent_variant)
- args['displaystyle'] = parse_displaystyle(params)
-
- css = params.get('style', '')
+ args["mathcolor"] = params.get("mathcolor", config.math.color)
+ args["mathbackground"] = params.get("mathbackground", config.math.background)
+ args["mathsize"] = params.get("mathsize", "")
+ args["scriptlevel"] = int(params.get("scriptlevel", 0))
+ args["mathvariant"] = parse_variant(
+ element.attrib.get("mathvariant", config.math.variant), parent_variant
+ )
+ args["displaystyle"] = parse_displaystyle(params)
+
+ css = params.get("style", "")
if css:
- cssparams = css.split(';')
+ cssparams = css.split(";")
for cssparam in cssparams:
if not cssparam:
continue # blank lines
- key, val = cssparam.split(':')
+ key, val = cssparam.split(":")
key = key.strip()
val = val.strip()
- if key.lower() == 'background':
- args['mathbackground'] = val
- elif key.lower() == 'color':
- args['mathcolor'] = val
+ if key.lower() == "background":
+ args["mathbackground"] = val
+ elif key.lower() == "color":
+ args["mathcolor"] = val
return MathStyle(**args)
LATIN_CAP_RANGE = (0x41, 0x5A)
-LATIN_CAPS = \
- {'serif': {Styletype(bold=False, italic=False): 0x0000,
- Styletype(bold=True, italic=False): 0x1D400,
- Styletype(bold=False, italic=True): 0x1D434,
- Styletype(bold=True, italic=True): 0x1D468
- },
- 'sans': {Styletype(bold=False, italic=False): 0x1D5A0,
- Styletype(bold=True, italic=False): 0x1D5D4,
- Styletype(bold=False, italic=True): 0x1D608,
- Styletype(bold=True, italic=True): 0x1D63C
- },
- 'script': {Styletype(bold=False, italic=False): 0x1D49C,
- Styletype(bold=True, italic=False): 0x1D4D0,
- Styletype(bold=True, italic=True): 0x1D4D0 # No separate italic
- },
- 'fraktur': {Styletype(bold=False, italic=False): 0x1D504,
- Styletype(bold=True, italic=False): 0x1D56C,
- Styletype(bold=True, italic=True): 0x1D56C # No separate italic
- },
- 'mono': {Styletype(bold=False, italic=False): 0x1D670,
- },
- 'double': {Styletype(bold=False, italic=False): 0x1D538,
- },
- }
-
-LATIN_SMALL_RANGE = (0x61, 0x7a)
-LATIN_SMALL = \
- {'serif': {Styletype(bold=False, italic=False): 0x0000,
- Styletype(bold=True, italic=False): 0x1D41A,
- Styletype(bold=False, italic=True): 0x1D44E,
- Styletype(bold=True, italic=True): 0x1D482
- },
- 'sans': {Styletype(bold=False, italic=False): 0x1D5BA,
- Styletype(bold=True, italic=False): 0x1D5EE,
- Styletype(bold=False, italic=True): 0x1D622,
- Styletype(bold=True, italic=True): 0x1D656
- },
- 'script': {Styletype(bold=False, italic=False): 0x1D4B6,
- Styletype(bold=True, italic=False): 0x1D4EA,
- Styletype(bold=True, italic=True): 0x1D4EA # No separate italic
- },
- 'fraktur': {Styletype(bold=False, italic=False): 0x1D51E,
- Styletype(bold=True, italic=False): 0x1D586,
- Styletype(bold=True, italic=True): 0x1D586 # No separate italic
- },
- 'mono': {Styletype(bold=False, italic=False): 0x1D68A,
- },
- 'double': {Styletype(bold=False, italic=False): 0x1D552,
- },
- }
+LATIN_CAPS = {
+ "serif": {
+ Styletype(bold=False, italic=False): 0x0000,
+ Styletype(bold=True, italic=False): 0x1D400,
+ Styletype(bold=False, italic=True): 0x1D434,
+ Styletype(bold=True, italic=True): 0x1D468,
+ },
+ "sans": {
+ Styletype(bold=False, italic=False): 0x1D5A0,
+ Styletype(bold=True, italic=False): 0x1D5D4,
+ Styletype(bold=False, italic=True): 0x1D608,
+ Styletype(bold=True, italic=True): 0x1D63C,
+ },
+ "script": {
+ Styletype(bold=False, italic=False): 0x1D49C,
+ Styletype(bold=True, italic=False): 0x1D4D0,
+ Styletype(bold=True, italic=True): 0x1D4D0, # No separate italic
+ },
+ "fraktur": {
+ Styletype(bold=False, italic=False): 0x1D504,
+ Styletype(bold=True, italic=False): 0x1D56C,
+ Styletype(bold=True, italic=True): 0x1D56C, # No separate italic
+ },
+ "mono": {
+ Styletype(bold=False, italic=False): 0x1D670,
+ },
+ "double": {
+ Styletype(bold=False, italic=False): 0x1D538,
+ },
+}
+
+LATIN_SMALL_RANGE = (0x61, 0x7A)
+LATIN_SMALL = {
+ "serif": {
+ Styletype(bold=False, italic=False): 0x0000,
+ Styletype(bold=True, italic=False): 0x1D41A,
+ Styletype(bold=False, italic=True): 0x1D44E,
+ Styletype(bold=True, italic=True): 0x1D482,
+ },
+ "sans": {
+ Styletype(bold=False, italic=False): 0x1D5BA,
+ Styletype(bold=True, italic=False): 0x1D5EE,
+ Styletype(bold=False, italic=True): 0x1D622,
+ Styletype(bold=True, italic=True): 0x1D656,
+ },
+ "script": {
+ Styletype(bold=False, italic=False): 0x1D4B6,
+ Styletype(bold=True, italic=False): 0x1D4EA,
+ Styletype(bold=True, italic=True): 0x1D4EA, # No separate italic
+ },
+ "fraktur": {
+ Styletype(bold=False, italic=False): 0x1D51E,
+ Styletype(bold=True, italic=False): 0x1D586,
+ Styletype(bold=True, italic=True): 0x1D586, # No separate italic
+ },
+ "mono": {
+ Styletype(bold=False, italic=False): 0x1D68A,
+ },
+ "double": {
+ Styletype(bold=False, italic=False): 0x1D552,
+ },
+}
GREEK_CAP_RANGE = (0x0391, 0x3AA)
-GREEK_CAPS = \
- {'serif': {Styletype(bold=False, italic=False): 0x0000,
- Styletype(bold=True, italic=False): 0x1D6A8,
- Styletype(bold=False, italic=True): 0x1D6E2,
- Styletype(bold=True, italic=True): 0x1D71C
- },
- 'sans': {Styletype(bold=False, italic=False): 0x0000,
- Styletype(bold=True, italic=False): 0x1D756,
- Styletype(bold=True, italic=True): 0x1D790
- },
- }
-
-GREEK_LOWER_RANGE = (0x3b1, 0x3d0)
-GREEK_LOWER = \
- {'serif': {Styletype(bold=False, italic=False): 0x0000,
- Styletype(bold=True, italic=False): 0x1D6C2,
- Styletype(bold=False, italic=True): 0x1D6FC,
- Styletype(bold=True, italic=True): 0x1D736
- },
- 'sans': {Styletype(bold=False, italic=False): 0x0000,
- Styletype(bold=True, italic=False): 0x1D770,
- Styletype(bold=True, italic=True): 0x1D7AA
- },
- }
+GREEK_CAPS = {
+ "serif": {
+ Styletype(bold=False, italic=False): 0x0000,
+ Styletype(bold=True, italic=False): 0x1D6A8,
+ Styletype(bold=False, italic=True): 0x1D6E2,
+ Styletype(bold=True, italic=True): 0x1D71C,
+ },
+ "sans": {
+ Styletype(bold=False, italic=False): 0x0000,
+ Styletype(bold=True, italic=False): 0x1D756,
+ Styletype(bold=True, italic=True): 0x1D790,
+ },
+}
+
+GREEK_LOWER_RANGE = (0x3B1, 0x3D0)
+GREEK_LOWER = {
+ "serif": {
+ Styletype(bold=False, italic=False): 0x0000,
+ Styletype(bold=True, italic=False): 0x1D6C2,
+ Styletype(bold=False, italic=True): 0x1D6FC,
+ Styletype(bold=True, italic=True): 0x1D736,
+ },
+ "sans": {
+ Styletype(bold=False, italic=False): 0x0000,
+ Styletype(bold=True, italic=False): 0x1D770,
+ Styletype(bold=True, italic=True): 0x1D7AA,
+ },
+}
DIGIT_RANGE = (0x30, 0x39)
-DIGITS = \
- {'serif': {Styletype(bold=False, italic=False): 0x0000,
- Styletype(bold=True, italic=False): 0x1D7CE,
- },
- 'double': {Styletype(bold=False, italic=False): 0x1D7D8, },
- 'mono': {Styletype(bold=False, italic=False): 0x1D7F6, },
- 'sans': {Styletype(bold=False, italic=False): 0x1D7E2,
- Styletype(bold=True, italic=False): 0x1D7EC,
- Styletype(bold=True, italic=True): 0x1D7EC,
- },
- }
-
-
-subtables = ((LATIN_CAP_RANGE, LATIN_CAPS),
- (LATIN_SMALL_RANGE, LATIN_SMALL),
- (GREEK_CAP_RANGE, GREEK_CAPS),
- (GREEK_LOWER_RANGE, GREEK_LOWER),
- (DIGIT_RANGE, DIGITS))
+DIGITS = {
+ "serif": {
+ Styletype(bold=False, italic=False): 0x0000,
+ Styletype(bold=True, italic=False): 0x1D7CE,
+ },
+ "double": {
+ Styletype(bold=False, italic=False): 0x1D7D8,
+ },
+ "mono": {
+ Styletype(bold=False, italic=False): 0x1D7F6,
+ },
+ "sans": {
+ Styletype(bold=False, italic=False): 0x1D7E2,
+ Styletype(bold=True, italic=False): 0x1D7EC,
+ Styletype(bold=True, italic=True): 0x1D7EC,
+ },
+}
+
+
+subtables = (
+ (LATIN_CAP_RANGE, LATIN_CAPS),
+ (LATIN_SMALL_RANGE, LATIN_SMALL),
+ (GREEK_CAP_RANGE, GREEK_CAPS),
+ (GREEK_LOWER_RANGE, GREEK_LOWER),
+ (DIGIT_RANGE, DIGITS),
+)
# These are the yellow characters in wikipedia's table
OFFSET_EXCEPTIONS = {
- 'ϴ': 0x0391+0x11,
- '∇': 0x0391+0x19,
- '∂': 0x03B1+0x19,
- 'ϵ': 0x03B1+0x1A,
- 'ϑ': 0x03B1+0x1B,
- 'ϰ': 0x03B1+0x1C,
- 'ϕ': 0x03B1+0x1D,
- 'ϱ': 0x03B1+0x1E,
- 'ϖ': 0x03B1+0x1F}
+ "ϴ": 0x0391 + 0x11,
+ "∇": 0x0391 + 0x19,
+ "∂": 0x03B1 + 0x19,
+ "ϵ": 0x03B1 + 0x1A,
+ "ϑ": 0x03B1 + 0x1B,
+ "ϰ": 0x03B1 + 0x1C,
+ "ϕ": 0x03B1 + 0x1D,
+ "ϱ": 0x03B1 + 0x1E,
+ "ϖ": 0x03B1 + 0x1F,
+}
EXCEPTIONS = {
- 0x1D49C+0x01: 'ℬ', # latin cap scripts
- 0x1D49C+0x04: 'ℰ',
- 0x1D49C+0x05: 'ℱ',
- 0x1D49C+0x07: 'ℋ',
- 0x1D49C+0x08: 'ℐ',
- 0x1D49C+0x0B: 'ℒ',
- 0x1D49C+0x0C: 'ℳ',
- 0x1D49C+0x11: 'ℛ',
- 0x1D504+0x02: 'ℭ', # latin cap frakturs
- 0x1D504+0x07: 'ℌ',
- 0x1D504+0x08: 'ℑ',
- 0x1D504+0x11: 'ℜ',
- 0x1D504+0x19: 'ℨ',
- 0x1D538+0x02: 'ℂ', # latin cap doubles
- 0x1D538+0x07: 'ℍ',
- 0x1D538+0x0D: 'ℕ',
- 0x1D538+0x0F: 'ℙ',
- 0x1D538+0x10: 'ℚ',
- 0x1D538+0x11: 'ℝ',
- 0x1D538+0x19: 'ℤ',
- 0x1D44E+0x07: 'ℎ', # latin small italic
- 0x1D4B6+0x04: 'ℯ', # latin small script
- 0x1D4B6+0x06: 'ℊ',
- 0x1D4B6+0x0E: 'ℴ',
- }
+ 0x1D49C + 0x01: "ℬ", # latin cap scripts
+ 0x1D49C + 0x04: "ℰ",
+ 0x1D49C + 0x05: "ℱ",
+ 0x1D49C + 0x07: "ℋ",
+ 0x1D49C + 0x08: "ℐ",
+ 0x1D49C + 0x0B: "ℒ",
+ 0x1D49C + 0x0C: "ℳ",
+ 0x1D49C + 0x11: "ℛ",
+ 0x1D504 + 0x02: "ℭ", # latin cap frakturs
+ 0x1D504 + 0x07: "ℌ",
+ 0x1D504 + 0x08: "ℑ",
+ 0x1D504 + 0x11: "ℜ",
+ 0x1D504 + 0x19: "ℨ",
+ 0x1D538 + 0x02: "ℂ", # latin cap doubles
+ 0x1D538 + 0x07: "ℍ",
+ 0x1D538 + 0x0D: "ℕ",
+ 0x1D538 + 0x0F: "ℙ",
+ 0x1D538 + 0x10: "ℚ",
+ 0x1D538 + 0x11: "ℝ",
+ 0x1D538 + 0x19: "ℤ",
+ 0x1D44E + 0x07: "ℎ", # latin small italic
+ 0x1D4B6 + 0x04: "ℯ", # latin small script
+ 0x1D4B6 + 0x06: "ℊ",
+ 0x1D4B6 + 0x0E: "ℴ",
+}
def auto_italic(char: str) -> bool:
- ''' Determine whether the character should be automatically
- converted to italic
- '''
+ """Determine whether the character should be automatically
+ converted to italic
+ """
ordchr = ord(char)
for ordrange in (GREEK_LOWER_RANGE, LATIN_SMALL_RANGE, LATIN_CAP_RANGE):
if ordrange[0] <= ordchr <= ordrange[1]:
@@ -246,9 +278,9 @@ def auto_italic(char: str) -> bool:
def styledchr(char, variant: MathVariant):
- ''' Convert character to its styled (bold, italic, script, etc.) variant.
- See tables at: https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols
- '''
+ """Convert character to its styled (bold, italic, script, etc.) variant.
+ See tables at: https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols
+ """
script = variant.style
style = Styletype(variant.bold, variant.italic)
styledchr = char # Default is to return char unchanged
@@ -257,7 +289,7 @@ def styledchr(char, variant: MathVariant):
for ordrange, table in subtables:
if ordrange[0] <= charord <= ordrange[1]:
ordoffset = charord - ordrange[0]
- scripttable = table.get(script, table.get('serif'))
+ scripttable = table.get(script, table.get("serif"))
offset = scripttable.get(style, scripttable.get(Styletype(False, False))) # type: ignore
if offset:
styledchr = chr(ordoffset + offset)
@@ -268,5 +300,5 @@ def styledchr(char, variant: MathVariant):
def styledstr(st: str, variant: MathVariant) -> str:
- ''' Apply unicode styling conversion to a string '''
- return ''.join([styledchr(s, variant) for s in st])
+ """Apply unicode styling conversion to a string"""
+ return "".join([styledchr(s, variant) for s in st])
diff --git a/ziamath/zmath.py b/ziamath/zmath.py
index 5233731..196671d 100644
--- a/ziamath/zmath.py
+++ b/ziamath/zmath.py
@@ -1,4 +1,4 @@
-''' Main math rendering class '''
+"""Main math rendering class"""
from __future__ import annotations
from typing import Union, Literal, Tuple, Optional, Dict
@@ -21,47 +21,49 @@
from .tex import tex2mml
-Halign = Literal['left', 'center', 'right']
-Valign = Literal['top', 'center', 'base', 'axis', 'bottom']
+Halign = Literal["left", "center", "right"]
+Valign = Literal["top", "center", "base", "axis", "bottom"]
def denamespace(element: ET.Element) -> ET.Element:
- ''' Recursively remove namespace {...} from beginning of xml
- element names, so they can be searched easily.
- '''
- if element.tag.startswith('{'):
- element.tag = element.tag.split('}')[1]
+ """Recursively remove namespace {...} from beginning of xml
+ element names, so they can be searched easily.
+ """
+ if element.tag.startswith("{"):
+ element.tag = element.tag.split("}")[1]
for elm in element:
denamespace(elm)
return element
def apply_mstyle(element: ET.Element) -> ET.Element:
- ''' Take attributes defined in elements and add them
- to all the child elements, removing the original
- '''
+ """Take attributes defined in elements and add them
+ to all the child elements, removing the original
+ """
+
def flatten_attrib(element: ET.Element) -> None:
for child in element:
- if element.tag == 'mstyle':
+ if element.tag == "mstyle":
child.attrib = dict(ChainMap(child.attrib, element.attrib))
flatten_attrib(child)
flatten_attrib(element)
- elmstr = ET.tostring(element).decode('utf-8')
- elmstr = re.sub(r'', '', elmstr)
- elmstr = re.sub(r'', '', elmstr)
+ elmstr = ET.tostring(element).decode("utf-8")
+ elmstr = re.sub(r"", "", elmstr)
+ elmstr = re.sub(r"", "", elmstr)
return ET.fromstring(elmstr)
class EqNumbering:
- ''' Manage automatic equation numbers '''
+ """Manage automatic equation numbers"""
+
count: int = 1
enable: bool = True
@classmethod
def number(cls) -> Optional[int]:
- ''' Get next number in the sequence '''
+ """Get next number in the sequence"""
if EqNumbering.enable:
number = EqNumbering.count
EqNumbering.count += 1
@@ -71,19 +73,19 @@ def number(cls) -> Optional[int]:
@classmethod
@contextmanager
def pause(cls):
- ''' Context manager to pause equation numbering '''
+ """Context manager to pause equation numbering"""
EqNumbering.enable = False
yield
EqNumbering.enable = True
@classmethod
def reset(cls, number: int = 1) -> None:
- ''' Reset the current number '''
+ """Reset the current number"""
EqNumbering.count = number
@classmethod
- def text(cls, number = None) -> Optional[str]:
- ''' Get text to use as equation label '''
+ def text(cls, number=None) -> Optional[str]:
+ """Get text to use as equation label"""
if EqNumbering.enable:
if number is None:
number = EqNumbering.number()
@@ -92,26 +94,28 @@ def text(cls, number = None) -> Optional[str]:
def reset_numbering(number: int = 1):
- ''' Reset equation numbering '''
+ """Reset equation numbering"""
EqNumbering.reset(number)
-
class Math:
- ''' MathML Element Renderer
-
- Args:
- mathml: MathML expression, in string or XML Element
- size: Base font size, pixels
- font: Filename of font file. Must contain MATH typesetting table.
- title: Text for title alt-text tag in the SVG
- '''
- def __init__(self,
- mathml: Union[str, ET.Element],
- size: Optional[float] = None,
- font: Optional[str] = None,
- title: Optional[str] = None,
- number: Optional[str] = None):
+ """MathML Element Renderer
+
+ Args:
+ mathml: MathML expression, in string or XML Element
+ size: Base font size, pixels
+ font: Filename of font file. Must contain MATH typesetting table.
+ title: Text for title alt-text tag in the SVG
+ """
+
+ def __init__(
+ self,
+ mathml: Union[str, ET.Element],
+ size: Optional[float] = None,
+ font: Optional[str] = None,
+ title: Optional[str] = None,
+ number: Optional[str] = None,
+ ):
self.size = size if size else config.math.fontsize
font = font if font else config.math.mathfont
self.title = title
@@ -123,7 +127,7 @@ def __init__(self,
self.font: MathFont
if font is None:
- self.font = loadedfonts['default']
+ self.font = loadedfonts["default"]
elif font in loadedfonts:
self.font = loadedfonts[font]
else:
@@ -144,7 +148,9 @@ def register_altfont(path):
if config.math.italic_font:
self.font.alt_fonts.italic = register_altfont(config.math.italic_font)
if config.math.bolditalic_font:
- self.font.alt_fonts.bolditalic = register_altfont(config.math.bolditalic_font)
+ self.font.alt_fonts.bolditalic = register_altfont(
+ config.math.bolditalic_font
+ )
if isinstance(mathml, str):
mathml = unescape(mathml)
@@ -155,88 +161,118 @@ def register_altfont(path):
self.mathml = mathml
self.style = parse_style(mathml)
self.element = mathml
- self.mtag = 'math'
+ self.mtag = "math"
self.node = Mnode.fromelement(mathml, parent=self) # type: ignore
@classmethod
- def fromlatex(cls, latex: str, size: Optional[float] = None, mathstyle: Optional[str] = None,
- font: Optional[str] = None, color: Optional[str] = None, inline: bool = False):
- ''' Create Math Renderer from a single LaTeX expression. Requires
- latex2mathml Python package.
-
- Args:
- latex: Latex string
- size: Base font size
- mathstyle: Style parameter for math, equivalent to "mathvariant" MathML attribute
- font: Font file name
- color: Color parameter, equivalent to "mathcolor" attribute
- inline: Use inline math mode (default is block mode)
- '''
+ def fromlatex(
+ cls,
+ latex: str,
+ size: Optional[float] = None,
+ mathstyle: Optional[str] = None,
+ font: Optional[str] = None,
+ color: Optional[str] = None,
+ inline: bool = False,
+ ):
+ """Create Math Renderer from a single LaTeX expression. Requires
+ latex2mathml Python package.
+
+ Args:
+ latex: Latex string
+ size: Base font size
+ mathstyle: Style parameter for math, equivalent to "mathvariant" MathML attribute
+ font: Font file name
+ color: Color parameter, equivalent to "mathcolor" attribute
+ inline: Use inline math mode (default is block mode)
+ """
mathml: Union[str, ET.Element]
mathml = tex2mml(latex, inline=inline)
if mathstyle:
mathml = ET.fromstring(mathml)
- mathml.attrib['mathvariant'] = mathstyle
- mathml = ET.tostring(mathml, encoding='unicode')
+ mathml.attrib["mathvariant"] = mathstyle
+ mathml = ET.tostring(mathml, encoding="unicode")
if color:
mathml = ET.fromstring(mathml)
- mathml.attrib['mathcolor'] = color
+ mathml.attrib["mathcolor"] = color
return cls(mathml, size, font)
@classmethod
- def fromlatextext(cls, latex: str, size: float = 24, mathstyle: Optional[str] = None,
- textstyle: Optional[str] = None, font: Optional[str] = None,
- color: Optional[str] = None):
- ''' Create Math Renderer from a sentence containing zero or more LaTeX
- expressions delimited by $..$, resulting in single MathML element.
- Requires latex2mathml Python package.
-
- Args:
- latex: string
- size: Base font size
- mathstyle: Style parameter for math, equivalent to "mathvariant" MathML attribute
- textstyle: Style parameter for text, equivalent to "mathvariant" MathML attribute
- font: Font file name
- color: Color parameter, equivalent to "mathcolor" attribute
- '''
- warnings.warn(r'fromlatextext is deprecated. Use ziamath.Text or \text{} command.', DeprecationWarning, stacklevel=2)
+ def fromlatextext(
+ cls,
+ latex: str,
+ size: float = 24,
+ mathstyle: Optional[str] = None,
+ textstyle: Optional[str] = None,
+ font: Optional[str] = None,
+ color: Optional[str] = None,
+ ):
+ """Create Math Renderer from a sentence containing zero or more LaTeX
+ expressions delimited by $..$, resulting in single MathML element.
+ Requires latex2mathml Python package.
+
+ Args:
+ latex: string
+ size: Base font size
+ mathstyle: Style parameter for math, equivalent to "mathvariant" MathML attribute
+ textstyle: Style parameter for text, equivalent to "mathvariant" MathML attribute
+ font: Font file name
+ color: Color parameter, equivalent to "mathcolor" attribute
+ """
+ warnings.warn(
+ r"fromlatextext is deprecated. Use ziamath.Text or \text{} command.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
# Extract each $..$, convert to MathML, but the raw text in , and join
# into a single