SF.net SVN: docutils:[9535 ] trunk/docutils
milde--- via Docutils-checkins <[email protected]>
| Newsgroups | gmane.text.docutils.cvs |
|---|---|
| Message-ID | <[email protected]> |
Revision: 9535
http://sourceforge.net/p/docutils/code/9535
Author: milde
Date: 2024-02-01 13:04:04 +0000 (Thu, 01 Feb 2024)
Log Message:
-----------
Base MathML element classes on xml.etree.
Use `xml.etree` for a more standard compatible implementation of
MathML element classes.
`xml.etree` comes with methods to represent XML elements,
to convert to XML (since Python 3.9 also indented XML),
search for elements, iterate over nodes etc.
The new module adds an interface to the requirements of latex2mathml:
* Simpler instantiation
- tag name taken from class name,
- attribute names are normalized to lowercase
(allows 'CLASS' for 'class'),
- attribute values may be specified as numbers, booleans, or strings
(converted to `str` before storing).
* "internal" attributes to store a reference to the parent element
and the number of expected children.
* "appending" to and "closing" an element returns the new "active node"
(insertion point).
* Remove redundant `<mrow>` elements when closing.
* toxml() by default returns a Unicode `str` (not `bytes`).
Modified Paths:
--------------
trunk/docutils/docutils/utils/math/latex2mathml.py
trunk/docutils/docutils/utils/math/mathml_elements.py
trunk/docutils/test/test_utils/test_math/test_mathml_elements.py
Modified: trunk/docutils/docutils/utils/math/latex2mathml.py
===================================================================
--- trunk/docutils/docutils/utils/math/latex2mathml.py 2024-02-01 13:03:52 UTC (rev 9534)
+++ trunk/docutils/docutils/utils/math/latex2mathml.py 2024-02-01 13:04:04 UTC (rev 9535)
@@ -578,7 +578,7 @@
attributes = {}
if c == '-' and len(node):
previous_node = node[-1]
- if (getattr(previous_node, 'text', '-') in '([='
+ if (previous_node.text and previous_node.text in '([='
or previous_node.get('class') == 'mathopen'):
attributes['form'] = 'prefix'
node = node.append(mo(anomalous_chars[c], **attributes))
@@ -1163,7 +1163,8 @@
math_tree.append(mtable(mtr(node), CLASS='ams-align',
displaystyle=True))
parse_latex_math(node, tex_math)
- return math_tree.toprettyxml()
+ math_tree.indent_xml()
+ return math_tree.toxml()
# >>> print(tex2mathml('3'))
# <math xmlns="http://www.w3.org/1998/Math/MathML">
Modified: trunk/docutils/docutils/utils/math/mathml_elements.py
===================================================================
--- trunk/docutils/docutils/utils/math/mathml_elements.py 2024-02-01 13:03:52 UTC (rev 9534)
+++ trunk/docutils/docutils/utils/math/mathml_elements.py 2024-02-01 13:04:04 UTC (rev 9535)
@@ -10,7 +10,7 @@
#
# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
-"""MathML element classes.
+"""MathML element classes based on `xml.etree`.
The module is intended for programmatic generation of MathML
and covers the part of `MathML Core`_ that is required by
@@ -27,6 +27,7 @@
# >>> from mathml_elements import *
import numbers
+import xml.etree.ElementTree as ET
GLOBAL_ATTRIBUTES = (
@@ -52,7 +53,7 @@
# Base classes
# ------------
-class MathElement:
+class MathElement(ET.Element):
"""Base class for MathML elements."""
nchildren = None
@@ -60,13 +61,6 @@
# cf. https://www.w3.org/TR/MathML3/chapter3.html#id.3.1.3.2
parent = None
"""Parent node in MathML element tree."""
- xml_entities = {
- # for invalid and invisible characters
- ord('<'): '<',
- ord('>'): '>',
- ord('&'): '&',
- 0x2061: '⁡',
- }
def __init__(self, *children, **attributes):
"""Set up node with `children` and `attributes`.
@@ -78,13 +72,12 @@
>>> math(CLASS='test', level=3, split=True)
math(class='test', level='3', split='true')
- >>> math(CLASS='test', level=3, split=True).toprettyxml()
+ >>> math(CLASS='test', level=3, split=True).toxml()
'<math class="test" level="3" split="true"></math>'
"""
- self.attrib = {k.lower(): self.a_str(v)
- for k, v in attributes.items()}
- self.children = []
+ attrib = {k.lower(): self.a_str(v) for k, v in attributes.items()}
+ super().__init__(self.__class__.__name__, **attrib)
self.extend(children)
@staticmethod
@@ -97,7 +90,7 @@
def __repr__(self):
"""Return full string representation."""
args = [repr(child) for child in self]
- if hasattr(self, 'text'):
+ if self.text:
args.append(repr(self.text))
if self.nchildren != self.__class__.nchildren:
args.append(f'nchildren={self.nchildren}')
@@ -104,41 +97,19 @@
if getattr(self, 'switch', None):
args.append('switch=True')
args += [f'{k}={v!r}' for k, v in self.items() if v is not None]
- return f'{self.__class__.__name__}({", ".join(args)})'
+ return f'{self.tag}({", ".join(args)})'
def __str__(self):
"""Return concise, informal string representation."""
- if getattr(self, 'text', ''):
+ if self.text:
args = repr(self.text)
else:
args = ', '.join(f'{child}' for child in self)
- return f'{self.__class__.__name__}({args})'
+ return f'{self.tag}({args})'
- # Emulate dictionary access methods for attributes
- # and list-like interface to the child elements
- # (differs from `docutils.nodes.Element` dict/list interface).
-
- def get(self, key, default=None):
- return self.attrib.get(key, default)
-
def set(self, key, value):
- self.attrib[key] = self.a_str(value)
+ super().set(key, self.a_str(value))
- def items(self):
- return self.attrib.items()
-
- def iter(self):
- """Return iterator over self and all subnodes, including nested."""
- yield self
- for child in self.children:
- yield from child.iter()
-
- def __len__(self):
- return len(self.children)
-
- def __getitem__(self, key):
- return self.children.__getitem__(key)
-
def __setitem__(self, key, value):
if self.nchildren == 0:
raise TypeError(f'Element "{self}" does not take children.')
@@ -147,14 +118,8 @@
else: # value may be an iterable
for e in value:
e.parent = self
- self.children.__setitem__(key, value)
+ super().__setitem__(key, value)
- def __delitem__(self, key):
- self.children.__delitem__(key)
-
- def __iter__(self):
- return self.children.__iter__()
-
def is_full(self):
"""Return boolean indicating whether children may be appended."""
return self.nchildren is not None and len(self) >= self.nchildren
@@ -183,7 +148,7 @@
else:
status = 'does not take children'
raise TypeError(f'Element "{self}" {status}.')
- self.children.append(element)
+ super().append(element)
element.parent = self
if self.is_full():
return self.close()
@@ -216,27 +181,47 @@
return False
return self.get('display') == 'block'
- # Conversion to (pretty) XML string
- def toprettyxml(self):
- """Return XML representation of self as string."""
- return ''.join(self._xml())
+ # XML output:
- def _xml(self, level=0):
- return [self.xml_starttag(),
- *self._xml_body(level),
- '</%s>' % self.__class__.__name__]
+ def indent_xml(self, space=' ', level=0):
+ """Format XML output with indents.
- def xml_starttag(self):
- attrs = (f'{k}="{v}"' for k, v in self.items() if v is not None)
- return '<%s>' % ' '.join((self.__class__.__name__, *attrs))
+ Use with care:
+ Formatting whitespace is permanently added to the
+ `text` and `tail` attributes of `self` and anchestors!
+ """
+ ET.indent(self, space, level)
- def _xml_body(self, level=0):
- xml = []
- for child in self.children:
- xml.extend(['\n', ' ' * (level+1)])
- xml.extend(child._xml(level+1))
- if self.children:
- xml.extend(['\n', ' ' * level])
+ def unindent_xml(self):
+ """Strip whitespace at the end of `text` and `tail` attributes...
+
+ to revert changes made by the `indent_xml()` method.
+ Use with care, trailing whitespace from the original may be lost.
+ """
+ for e in self.iter():
+ if not isinstance(e, MathToken) and e.text:
+ e.text = e.text.rstrip()
+ if e.tail:
+ e.tail = e.tail.rstrip()
+
+ def toxml(self, encoding=None):
+ """Return an XML representation of the element.
+
+ By default, the return value is a `str` instance. With an explicit
+ `encoding` argument, the result is a `bytes` instance in the
+ specified encoding. The XML default encoding is UTF-8, any other
+ encoding must be specified in an XML document header.
+
+ Name and encoding handling match `xml.dom.minidom.Node.toxml()`;
+ `etree.Element.tostring()` returns `bytes` by default.
+ """
+ xml = ET.tostring(self, encoding or 'unicode',
+ short_empty_elements=False)
+ # Visible representation for "Apply Function" character:
+ try:
+ xml = xml.replace('\u2061', '⁡')
+ except TypeError:
+ xml = xml.replace('\u2061'.encode(encoding), b'⁡')
return xml
@@ -266,7 +251,7 @@
def __init__(self, *children, **kwargs):
self.switch = kwargs.pop('switch', False)
- math.__init__(self, *children, **kwargs)
+ super().__init__(*children, **kwargs)
def append(self, element):
"""Append element. Normalize order and close if full."""
@@ -294,10 +279,7 @@
f' not "{text}".')
self.text = str(text)
- def _xml_body(self, level=0):
- return [str(self.text).translate(self.xml_entities)]
-
# MathML element classes
# ----------------------
@@ -319,7 +301,7 @@
class mn(MathToken):
"""Numeric literal.
- >>> mn(3.41).toprettyxml()
+ >>> mn(3.41).toxml()
'<mn>3.41</mn>'
Normally a sequence of digits with a possible separator (a dot or a comma).
@@ -330,7 +312,7 @@
class mo(MathToken):
"""Operator, Fence, Separator, or Accent.
- >>> mo('<').toprettyxml()
+ >>> mo('<').toxml()
'<mo><</mo>'
Besides operators in strict mathematical meaning, this element also
@@ -382,7 +364,7 @@
if parent is not None and len(self) == 1:
child = self[0]
try:
- parent[parent.children.index(self)] = child
+ parent[list(parent).index(self)] = child
child.parent = parent
except (AttributeError, ValueError):
return None
@@ -454,15 +436,15 @@
# Examples:
#
# The `switch` attribute reverses the order of the last two children:
-# >>> msub(mn(1), mn(2)).toprettyxml()
-# '<msub>\n <mn>1</mn>\n <mn>2</mn>\n</msub>'
-# >>> msub(mn(1), mn(2), switch=True).toprettyxml()
-# '<msub>\n <mn>2</mn>\n <mn>1</mn>\n</msub>'
+# >>> msub(mn(1), mn(2)).toxml()
+# '<msub><mn>1</mn><mn>2</mn></msub>'
+# >>> msub(mn(1), mn(2), switch=True).toxml()
+# '<msub><mn>2</mn><mn>1</mn></msub>'
#
-# >>> msubsup(mi('base'), mn(1), mn(2)).toprettyxml()
-# '<msubsup>\n <mi>base</mi>\n <mn>1</mn>\n <mn>2</mn>\n</msubsup>'
-# >>> msubsup(mi('base'), mn(1), mn(2), switch=True).toprettyxml()
-# '<msubsup>\n <mi>base</mi>\n <mn>2</mn>\n <mn>1</mn>\n</msubsup>'
+# >>> msubsup(mi('base'), mn(1), mn(2)).toxml()
+# '<msubsup><mi>base</mi><mn>1</mn><mn>2</mn></msubsup>'
+# >>> msubsup(mi('base'), mn(1), mn(2), switch=True).toxml()
+# '<msubsup><mi>base</mi><mn>2</mn><mn>1</mn></msubsup>'
class munder(msub):
Modified: trunk/docutils/test/test_utils/test_math/test_mathml_elements.py
===================================================================
--- trunk/docutils/test/test_utils/test_math/test_mathml_elements.py 2024-02-01 13:03:52 UTC (rev 9534)
+++ trunk/docutils/test/test_utils/test_math/test_mathml_elements.py 2024-02-01 13:04:04 UTC (rev 9535)
@@ -81,6 +81,7 @@
# No arguments required, the class name is used as XML tag:
e1 = mml.MathElement()
+ self.assertEqual(e1.tag, 'MathElement')
# Positional arguments are stored as children,
# named arguments are stored as element attributes:
@@ -187,8 +188,7 @@
result = e1.append(mml.MathElement(id='c1'))
self.assertEqual(e1[0].parent, e1)
# ... which is hidden in XML ...
- self.assertEqual(e1[0].toprettyxml(),
- '<MathElement id="c1"></MathElement>')
+ self.assertEqual(e1[0].toxml(), '<MathElement id="c1"></MathElement>')
# ... and returns the new "insertion point".
# If more children may be appended, return self
self.assertEqual(result, e1)
@@ -234,13 +234,40 @@
self.assertTrue(e2.in_block())
self.assertTrue(e2[0].in_block())
- def test_toprettyxml(self):
+ def test_indent_xml(self):
+ """Modify `text` and `tail` to get indented XML output."""
+ c1 = mml.math(id='c1')
+ cc1 = mml.math(id='cc1')
+ c2 = mml.math(cc1, id='c2')
+ root = mml.math(c1, c2, id='root')
+ self.assertTrue('\n' not in str(root))
+ root.indent_xml()
+ self.assertEqual(root.toxml(), self.prettyXML)
+ # You can easily remove the indentation (but not the newlines):
+ root.indent_xml(space='')
+ self.assertEqual(c2.toxml(),
+ '<math id="c2">\n<math id="cc1"></math>\n</math>\n')
+ # Reverting `indent_xml()` requires iterating over all descendants
+ root.unindent_xml()
+ self.assertEqual(c2.toxml(),
+ '<math id="c2"><math id="cc1"></math></math>')
+
+ def test_unindent_xml(self):
+ # see also last assertion in `test_indent_xml()`
+ e1 = mml.math(mml.mtext('Hallo welt!\n'))
+ e1.indent_xml()
+ self.assertEqual(e1.toxml(),
+ '<math>\n <mtext>Hallo welt!\n</mtext>\n</math>')
+ # don't strip whitespace from MathToken's text attributes:
+ e1.unindent_xml()
+ self.assertEqual(e1.toxml(),
+ '<math><mtext>Hallo welt!\n</mtext></math>')
+
+ def test_toxml(self):
"""XML representation of the element/subtree as `str`."""
e1 = mml.math(mml.math(level=2), CLASS='root')
- self.assertEqual(e1.toprettyxml(),
- '<math class="root">\n'
- ' <math level="2"></math>\n'
- '</math>')
+ self.assertEqual(e1.toxml(),
+ '<math class="root"><math level="2"></math></math>')
class MathSchemaTests(unittest.TestCase):
@@ -251,7 +278,7 @@
ms1 = mml.MathSchema(switch=True, id='ms1')
self.assertEqual(repr(ms1), "MathSchema(switch=True, id='ms1')")
# internal attributes are not exported to XML.
- self.assertEqual(ms1.toprettyxml(),
+ self.assertEqual(ms1.toxml(),
'<MathSchema id="ms1"></MathSchema>')
# the default value is dropped from ``repr()``
ms1.switch = False
@@ -262,9 +289,8 @@
# the children are switched and `switch` is reset:
ms2 = mml.MathSchema(mml.mn(1), mml.mn(2), switch=True)
self.assertEqual(repr(ms2), "MathSchema(mn('2'), mn('1'))")
- self.assertEqual(
- ms2.toprettyxml(),
- '<MathSchema>\n <mn>2</mn>\n <mn>1</mn>\n</MathSchema>')
+ self.assertEqual(ms2.toxml(),
+ '<MathSchema><mn>2</mn><mn>1</mn></MathSchema>')
def test_append(self):
# appending normalizes the order before switching
@@ -296,7 +322,7 @@
# optional named arguments become XML attributes
e1 = mml.mo('[', stretchy=False)
- self.assertEqual(e1.toprettyxml(), '<mo stretchy="false">[</mo>')
+ self.assertEqual(e1.toxml(), '<mo stretchy="false">[</mo>')
def test_append(self):
# MathTokens don't take child elements.
@@ -343,8 +369,8 @@
root = mml.math(row1) # provide a parent
row1.close() # try again
self.assertEqual(c1.parent, root)
- self.assertEqual(root.toprettyxml(),
- '<math>\n <math class="c1 row1"></math>\n</math>')
+ self.assertEqual(root.toxml(),
+ '<math><math class="c1 row1"></math></math>')
class MathMLElementTests(unittest.TestCase):
@@ -359,10 +385,10 @@
cls = getattr(mml, element)
if issubclass(cls, mml.MathToken):
e = cls('x')
- self.assertEqual(e.toprettyxml(), f'<{element}>x</{element}>')
+ self.assertEqual(e.toxml(), f'<{element}>x</{element}>')
else:
e = cls()
- self.assertEqual(e.toprettyxml(), f'<{element}></{element}>')
+ self.assertEqual(e.toxml(), f'<{element}></{element}>')
if nchildren == '*':
self.assertTrue(e.nchildren is None,
f'{element}.nchildren == {e.nchildren}')
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.