Skip to content

Commit 7319f4d

Browse files
[3.14] gh-156658: Only XML white space characters are treated as white space (GH-156659) (GH-156826)
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 f6842dd commit 7319f4d

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
@@ -811,6 +811,15 @@ def test_indent_space_caching(self):
811811
len({id(el.tail) for el in elem.iter()}),
812812
)
813813

814+
def test_indent_non_xml_whitespace(self):
815+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
816+
elem = ET.XML('<html>\xa0<body><p>text</p>\xa0</body></html>')
817+
ET.indent(elem)
818+
self.assertEqual(
819+
ET.tostring(elem),
820+
b'<html>&#160;<body>\n <p>text</p>&#160;</body>\n</html>'
821+
)
822+
814823
def test_indent_level(self):
815824
elem = ET.XML("<html><body><p>pre<br/>post</p><p>text</p></body></html>")
816825
with self.assertRaises(ValueError):
@@ -4684,6 +4693,11 @@ def test_simple_roundtrip(self):
46844693
xml = '<X xmlns="http://nps/a"><Y xmlns:b="http://nsp/b" b:targets="abc,xyz"></Y></X>'
46854694
self.assertEqual(c14n_roundtrip(xml), xml)
46864695

4696+
def test_c14n_strip_non_xml_whitespace(self):
4697+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
4698+
self.assertEqual(c14n_roundtrip("<a> \xa0x\xa0 </a>", strip_text=True),
4699+
"<a>\xa0x\xa0</a>")
4700+
46874701
def test_c14n_exclusion(self):
46884702
xml = textwrap.dedent("""\
46894703
<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
@@ -104,6 +104,9 @@
104104
from . import ElementPath
105105

106106

107+
# The white space characters of the XML specification (see XML 1.0, 2.3).
108+
_XML_WHITESPACE = " \t\r\n"
109+
107110
class ParseError(SyntaxError):
108111
"""An error when parsing an XML document.
109112
@@ -1194,17 +1197,17 @@ def _indent_children(elem, level):
11941197
child_indentation = indentations[level] + space
11951198
indentations.append(child_indentation)
11961199

1197-
if not elem.text or not elem.text.strip():
1200+
if not elem.text or not elem.text.strip(_XML_WHITESPACE):
11981201
elem.text = child_indentation
11991202

12001203
for child in elem:
12011204
if len(child):
12021205
_indent_children(child, child_level)
1203-
if not child.tail or not child.tail.strip():
1206+
if not child.tail or not child.tail.strip(_XML_WHITESPACE):
12041207
child.tail = child_indentation
12051208

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

12101213
_indent_children(tree, 0)
@@ -1705,7 +1708,7 @@ def _default(self, text):
17051708
if prefix == ">":
17061709
self._doctype = None
17071710
return
1708-
text = text.strip()
1711+
text = text.strip(_XML_WHITESPACE)
17091712
if not text:
17101713
return
17111714
self._doctype.append(text)
@@ -1921,7 +1924,7 @@ def _flush(self, _join_text=''.join):
19211924
data = _join_text(self._data)
19221925
del self._data[:]
19231926
if self._strip_text and not self._preserve_space[-1]:
1924-
data = data.strip()
1927+
data = data.strip(_XML_WHITESPACE)
19251928
if self._pending_start is not None:
19261929
args, self._pending_start = self._pending_start, None
19271930
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)