Skip to content

Commit 600618b

Browse files
serhiy-storchakamiss-islington
authored andcommitted
gh-156658: Only XML white space characters are treated as white space (GH-156659)
XML defines white space as " \t\r\n" (see XML 1.0, 2.3), but str.strip() also strips other characters, such as U+00A0. Such characters could be lost in ElementTree.indent(), in canonicalize(strip_text=True), and when parsing with the whitespace-in-element-content feature turned off. (cherry picked from commit e7c93b7) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent e9585e2 commit 600618b

6 files changed

Lines changed: 59 additions & 8 deletions

File tree

Lib/test/test_minidom.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,30 @@ def test_toprettyxml_preserves_content_of_text_node(self):
640640
dom.getElementsByTagName('B')[0].childNodes[0].toxml(),
641641
dom2.getElementsByTagName('B')[0].childNodes[0].toxml())
642642

643+
def test_isWhitespaceInElementContent(self):
644+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
645+
dom = parseString('<!DOCTYPE a [<!ELEMENT a (b)*><!ELEMENT b (#PCDATA)>]>'
646+
'<a> <b>x</b>\xa0</a>')
647+
children = dom.documentElement.childNodes
648+
self.assertTrue(children[0].isWhitespaceInElementContent)
649+
self.assertFalse(children[2].isWhitespaceInElementContent)
650+
dom.unlink()
651+
652+
def test_remove_whitespace_in_element_content(self):
653+
from xml.dom.xmlbuilder import DOMBuilder, DOMInputSource
654+
builder = DOMBuilder()
655+
builder.setFeature("whitespace-in-element-content", False)
656+
source = DOMInputSource()
657+
source.byteStream = io.BytesIO(
658+
b'<!DOCTYPE a [<!ELEMENT a (b)*><!ELEMENT b (#PCDATA)>]>'
659+
b'<a> <b>x</b>\xc2\xa0</a>')
660+
dom = builder.parse(source)
661+
children = dom.documentElement.childNodes
662+
# ignorable whitespace is removed, other characters are not
663+
self.assertEqual([node.nodeName for node in children], ['b', '#text'])
664+
self.assertEqual(children[1].data, '\xa0')
665+
dom.unlink()
666+
643667
def testProcessingInstruction(self):
644668
dom = parseString('<e><?mypi \t\n data \t\n ?></e>')
645669
pi = dom.documentElement.firstChild

Lib/test/test_xml_etree.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,15 @@ def test_indent_space_caching(self):
845845
len({id(el.tail) for el in elem.iter()}),
846846
)
847847

848+
def test_indent_non_xml_whitespace(self):
849+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
850+
elem = ET.XML('<html>\xa0<body><p>text</p>\xa0</body></html>')
851+
ET.indent(elem)
852+
self.assertEqual(
853+
ET.tostring(elem),
854+
b'<html>&#160;<body>\n <p>text</p>&#160;</body>\n</html>'
855+
)
856+
848857
def test_indent_level(self):
849858
elem = ET.XML("<html><body><p>pre<br/>post</p><p>text</p></body></html>")
850859
with self.assertRaises(ValueError):
@@ -4734,6 +4743,11 @@ def test_simple_roundtrip(self):
47344743
xml = '<X xmlns="http://nps/a"><Y xmlns:b="http://nsp/b" b:targets="abc,xyz"></Y></X>'
47354744
self.assertEqual(c14n_roundtrip(xml), xml)
47364745

4746+
def test_c14n_strip_non_xml_whitespace(self):
4747+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
4748+
self.assertEqual(c14n_roundtrip("<a> \xa0x\xa0 </a>", strip_text=True),
4749+
"<a>\xa0x\xa0</a>")
4750+
47374751
def test_c14n_exclusion(self):
47384752
xml = textwrap.dedent("""\
47394753
<root xmlns:x="http://example.com/x">

Lib/xml/dom/expatbuilder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
from xml.dom import xmlbuilder, minidom, Node
3131
from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE
3232
from xml.parsers import expat
33-
from xml.dom.minidom import _append_child, _set_attribute_node
33+
from xml.dom.minidom import (_append_child, _set_attribute_node,
34+
_XML_WHITESPACE)
3435
from xml.dom.NodeFilter import NodeFilter
3536

3637
TEXT_NODE = Node.TEXT_NODE
@@ -412,7 +413,8 @@ def _handle_white_text_nodes(self, node, info):
412413
# whitespace.
413414
L = []
414415
for child in node.childNodes:
415-
if child.nodeType == TEXT_NODE and not child.data.strip():
416+
if (child.nodeType == TEXT_NODE
417+
and not child.data.strip(_XML_WHITESPACE)):
416418
L.append(child)
417419

418420
# Remove ignorable whitespace from the tree.

Lib/xml/dom/minidom.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
_nodeTypes_with_children = (xml.dom.Node.ELEMENT_NODE,
3131
xml.dom.Node.ENTITY_REFERENCE_NODE)
3232

33+
# The white space characters of the XML specification (see XML 1.0, 2.3).
34+
_XML_WHITESPACE = " \t\r\n"
35+
3336

3437
class Node(xml.dom.Node):
3538
namespaceURI = None # this is non-null only for elements and attributes
@@ -1174,7 +1177,7 @@ def replaceWholeText(self, content):
11741177
return None
11751178

11761179
def _get_isWhitespaceInElementContent(self):
1177-
if self.data.strip():
1180+
if self.data.strip(_XML_WHITESPACE):
11781181
return False
11791182
elem = _get_containing_element(self)
11801183
if elem is None:

Lib/xml/etree/ElementTree.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@
101101
from . import ElementPath
102102

103103

104+
# The white space characters of the XML specification (see XML 1.0, 2.3).
105+
_XML_WHITESPACE = " \t\r\n"
106+
104107
class ParseError(SyntaxError):
105108
"""An error when parsing an XML document.
106109
@@ -1197,17 +1200,17 @@ def _indent_children(elem, level):
11971200
child_indentation = indentations[level] + space
11981201
indentations.append(child_indentation)
11991202

1200-
if not elem.text or not elem.text.strip():
1203+
if not elem.text or not elem.text.strip(_XML_WHITESPACE):
12011204
elem.text = child_indentation
12021205

12031206
for child in elem:
12041207
if len(child):
12051208
_indent_children(child, child_level)
1206-
if not child.tail or not child.tail.strip():
1209+
if not child.tail or not child.tail.strip(_XML_WHITESPACE):
12071210
child.tail = child_indentation
12081211

12091212
# Dedent after the last child by overwriting the previous indentation.
1210-
if not child.tail.strip():
1213+
if not child.tail.strip(_XML_WHITESPACE):
12111214
child.tail = indentations[level]
12121215

12131216
_indent_children(tree, 0)
@@ -1712,7 +1715,7 @@ def _default(self, text):
17121715
if prefix == ">":
17131716
self._doctype = None
17141717
return
1715-
text = text.strip()
1718+
text = text.strip(_XML_WHITESPACE)
17161719
if not text:
17171720
return
17181721
self._doctype.append(text)
@@ -1928,7 +1931,7 @@ def _flush(self, _join_text=''.join):
19281931
data = _join_text(self._data)
19291932
del self._data[:]
19301933
if self._strip_text and not self._preserve_space[-1]:
1931-
data = data.strip()
1934+
data = data.strip(_XML_WHITESPACE)
19321935
if self._pending_start is not None:
19331936
args, self._pending_start = self._pending_start, None
19341937
qname_text = data if data and _looks_like_prefix_name(data) else None
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:mod:`xml.dom` and :mod:`xml.etree.ElementTree` no longer treat characters
2+
which are not white space in XML (such as U+00A0) as white space. Previously
3+
they could be lost in :func:`~xml.etree.ElementTree.indent`,
4+
:func:`~xml.etree.ElementTree.canonicalize` with ``strip_text=True``, and when
5+
parsing with the ``whitespace-in-element-content`` feature turned off.

0 commit comments

Comments
 (0)