Skip to content

Commit e7c93b7

Browse files
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.
1 parent c700121 commit e7c93b7

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
@@ -642,6 +642,30 @@ def test_toprettyxml_preserves_content_of_text_node(self):
642642
dom.getElementsByTagName('B')[0].childNodes[0].toxml(),
643643
dom2.getElementsByTagName('B')[0].childNodes[0].toxml())
644644

645+
def test_isWhitespaceInElementContent(self):
646+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
647+
dom = parseString('<!DOCTYPE a [<!ELEMENT a (b)*><!ELEMENT b (#PCDATA)>]>'
648+
'<a> <b>x</b>\xa0</a>')
649+
children = dom.documentElement.childNodes
650+
self.assertTrue(children[0].isWhitespaceInElementContent)
651+
self.assertFalse(children[2].isWhitespaceInElementContent)
652+
dom.unlink()
653+
654+
def test_remove_whitespace_in_element_content(self):
655+
from xml.dom.xmlbuilder import DOMBuilder, DOMInputSource
656+
builder = DOMBuilder()
657+
builder.setFeature("whitespace-in-element-content", False)
658+
source = DOMInputSource()
659+
source.byteStream = io.BytesIO(
660+
b'<!DOCTYPE a [<!ELEMENT a (b)*><!ELEMENT b (#PCDATA)>]>'
661+
b'<a> <b>x</b>\xc2\xa0</a>')
662+
dom = builder.parse(source)
663+
children = dom.documentElement.childNodes
664+
# ignorable whitespace is removed, other characters are not
665+
self.assertEqual([node.nodeName for node in children], ['b', '#text'])
666+
self.assertEqual(children[1].data, '\xa0')
667+
dom.unlink()
668+
645669
def testProcessingInstruction(self):
646670
dom = parseString('<e><?mypi \t\n data \t\n ?></e>')
647671
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):
@@ -4900,6 +4909,11 @@ def test_simple_roundtrip(self):
49004909
xml = '<X xmlns="http://nps/a"><Y xmlns:b="http://nsp/b" b:targets="abc,xyz"></Y></X>'
49014910
self.assertEqual(c14n_roundtrip(xml), xml)
49024911

4912+
def test_c14n_strip_non_xml_whitespace(self):
4913+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
4914+
self.assertEqual(c14n_roundtrip("<a> \xa0x\xa0 </a>", strip_text=True),
4915+
"<a>\xa0x\xa0</a>")
4916+
49034917
def test_c14n_exclusion(self):
49044918
xml = textwrap.dedent("""\
49054919
<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
@@ -413,7 +414,8 @@ def _handle_white_text_nodes(self, node, info):
413414
# whitespace.
414415
L = []
415416
for child in node.childNodes:
416-
if child.nodeType == TEXT_NODE and not child.data.strip():
417+
if (child.nodeType == TEXT_NODE
418+
and not child.data.strip(_XML_WHITESPACE)):
417419
L.append(child)
418420

419421
# 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
@@ -31,6 +31,9 @@
3131
_nodeTypes_with_children = (xml.dom.Node.ELEMENT_NODE,
3232
xml.dom.Node.ENTITY_REFERENCE_NODE)
3333

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

3538
class Node(xml.dom.Node):
3639
namespaceURI = None # this is non-null only for elements and attributes
@@ -1209,7 +1212,7 @@ def replaceWholeText(self, content):
12091212
return None
12101213

12111214
def _get_isWhitespaceInElementContent(self):
1212-
if self.data.strip():
1215+
if self.data.strip(_XML_WHITESPACE):
12131216
return False
12141217
elem = _get_containing_element(self)
12151218
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
@@ -1209,17 +1212,17 @@ def _indent_children(elem, level):
12091212
child_indentation = indentations[level] + space
12101213
indentations.append(child_indentation)
12111214

1212-
if not elem.text or not elem.text.strip():
1215+
if not elem.text or not elem.text.strip(_XML_WHITESPACE):
12131216
elem.text = child_indentation
12141217

12151218
for child in elem:
12161219
if len(child):
12171220
_indent_children(child, child_level)
1218-
if not child.tail or not child.tail.strip():
1221+
if not child.tail or not child.tail.strip(_XML_WHITESPACE):
12191222
child.tail = child_indentation
12201223

12211224
# Dedent after the last child by overwriting the previous indentation.
1222-
if not child.tail.strip():
1225+
if not child.tail.strip(_XML_WHITESPACE):
12231226
child.tail = indentations[level]
12241227

12251228
_indent_children(tree, 0)
@@ -1724,7 +1727,7 @@ def _default(self, text):
17241727
if prefix == ">":
17251728
self._doctype = None
17261729
return
1727-
text = text.strip()
1730+
text = text.strip(_XML_WHITESPACE)
17281731
if not text:
17291732
return
17301733
self._doctype.append(text)
@@ -1940,7 +1943,7 @@ def _flush(self, _join_text=''.join):
19401943
data = _join_text(self._data)
19411944
del self._data[:]
19421945
if self._strip_text and not self._preserve_space[-1]:
1943-
data = data.strip()
1946+
data = data.strip(_XML_WHITESPACE)
19441947
if self._pending_start is not None:
19451948
args, self._pending_start = self._pending_start, None
19461949
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)