Skip to content

Commit 37616fa

Browse files
gh-81623: Do not add whitespace to significant content in toprettyxml()
Whitespace is no longer added inside an element which is marked with xml:space="preserve", which is declared in the DTD as not having element content, or, in absence of such declaration, which contains text. Previously such indentation changed the content of the element.
1 parent b944534 commit 37616fa

5 files changed

Lines changed: 105 additions & 8 deletions

File tree

Doc/library/xml.dom.minidom.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,23 @@ module documentation. This section lists the differences between the API and
187187

188188
The *standalone* argument behaves exactly as in :meth:`writexml`.
189189

190+
No indentation is added inside an element
191+
which is marked with ``xml:space="preserve"``,
192+
which is declared in the DTD as not having element content,
193+
or, in absence of such declaration, which contains text,
194+
because this would change its content.
195+
190196
.. versionchanged:: 3.8
191197
The :meth:`toprettyxml` method now preserves the attribute order specified
192198
by the user.
193199

194200
.. versionchanged:: 3.9
195201
The *standalone* parameter was added.
196202

203+
.. versionchanged:: next
204+
Whitespace is no longer added inside an element with mixed content
205+
or marked with ``xml:space="preserve"``.
206+
197207
.. _dom-example:
198208

199209
DOM Example

Doc/whatsnew/3.16.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,13 @@ xml
642642
and :meth:`!Document.createEntityReference`.
643643
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
644644

645+
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
646+
no longer adds whitespace inside an element
647+
which is marked with ``xml:space="preserve"``,
648+
which is declared in the DTD as not having element content,
649+
or, in absence of such declaration, which contains text.
650+
(Contributed by Serhiy Storchaka in :gh:`81623`.)
651+
645652
* Add :meth:`!GetSpecifiedAttributeCount` method
646653
to the :mod:`XML parser <xml.parsers.expat>` objects.
647654
It tells how many of the reported attributes were given in the start tag
@@ -881,6 +888,14 @@ that may require changes to your code.
881888
Attributes defaulted in the DTD are no longer omitted when parsing.
882889
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
883890

891+
* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom`
892+
no longer adds whitespace inside an element
893+
which is marked with ``xml:space="preserve"``,
894+
which is declared in the DTD as not having element content,
895+
or, in absence of such declaration, which contains text,
896+
because this changed the content of the element.
897+
(Contributed by Serhiy Storchaka in :gh:`81623`.)
898+
884899
* On Windows, seeking a pipe now fails instead of silently appearing to
885900
succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
886901
and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence,

Lib/test/test_minidom.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -609,32 +609,76 @@ def testAltNewline(self):
609609
self.assertEqual(domstr, str.replace("\n", "\r\n"))
610610

611611
def test_toprettyxml_with_text_nodes(self):
612-
# see issue #4147, text nodes are not indented
612+
# see gh-48397 and gh-81623,
613+
# the content of an element with text is not changed
613614
decl = '<?xml version="1.0" ?>\n'
614615
self.assertEqual(parseString('<B>A</B>').toprettyxml(),
615616
decl + '<B>A</B>\n')
616617
self.assertEqual(parseString('<C>A<B>A</B></C>').toprettyxml(),
617-
decl + '<C>\n\tA\n\t<B>A</B>\n</C>\n')
618+
decl + '<C>A<B>A</B></C>\n')
618619
self.assertEqual(parseString('<C><B>A</B>A</C>').toprettyxml(),
619-
decl + '<C>\n\t<B>A</B>\n\tA\n</C>\n')
620+
decl + '<C><B>A</B>A</C>\n')
620621
self.assertEqual(parseString('<C><B>A</B><B>A</B></C>').toprettyxml(),
621622
decl + '<C>\n\t<B>A</B>\n\t<B>A</B>\n</C>\n')
622623
self.assertEqual(parseString('<C><B>A</B>A<B>A</B></C>').toprettyxml(),
623-
decl + '<C>\n\t<B>A</B>\n\tA\n\t<B>A</B>\n</C>\n')
624+
decl + '<C><B>A</B>A<B>A</B></C>\n')
625+
# toprettyxml treats whitespace between elements as insignificant
626+
self.assertEqual(parseString('<C> <B>A</B> </C>').toprettyxml(),
627+
decl + '<C>\n\t \n\t<B>A</B>\n\t \n</C>\n')
624628

625629
def test_toprettyxml_with_adjacent_text_nodes(self):
626-
# see issue #4147, adjacent text nodes are indented normally
630+
# see gh-81623, adjacent text nodes are not separated
627631
dom = Document()
628632
elem = dom.createElement('elem')
629633
elem.appendChild(dom.createTextNode('TEXT'))
630634
elem.appendChild(dom.createTextNode('TEXT'))
631635
dom.appendChild(elem)
632636
decl = '<?xml version="1.0" ?>\n'
633-
self.assertEqual(dom.toprettyxml(),
634-
decl + '<elem>\n\tTEXT\n\tTEXT\n</elem>\n')
637+
self.assertEqual(dom.toprettyxml(), decl + '<elem>TEXTTEXT</elem>\n')
638+
639+
def test_toprettyxml_preserve(self):
640+
decl = '<?xml version="1.0" ?>\n'
641+
# xml:space="preserve" applies to the whole subtree
642+
self.assertEqual(
643+
parseString('<C xml:space="preserve"><B>A</B><B>A</B></C>'
644+
).toprettyxml(),
645+
decl + '<C xml:space="preserve"><B>A</B><B>A</B></C>\n')
646+
self.assertEqual(
647+
parseString('<C xml:space="preserve"><B><D/></B></C>'
648+
).toprettyxml(),
649+
decl + '<C xml:space="preserve"><B><D/></B></C>\n')
650+
# other values do not preserve whitespace
651+
self.assertEqual(
652+
parseString('<C xml:space="default"><B>A</B></C>').toprettyxml(),
653+
decl + '<C xml:space="default">\n\t<B>A</B>\n</C>\n')
654+
655+
def test_toprettyxml_with_non_xml_whitespace(self):
656+
# only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
657+
decl = '<?xml version="1.0" ?>\n'
658+
self.assertEqual(parseString('<C>\xa0<B>A</B></C>').toprettyxml(),
659+
decl + '<C>\xa0<B>A</B></C>\n')
660+
661+
def test_toprettyxml_with_dtd(self):
662+
decl = '<?xml version="1.0" ?>\n'
663+
# only whitespace in element content is ignorable
664+
doctype = ('<!DOCTYPE C [<!ELEMENT C (#PCDATA|B)*>'
665+
'<!ELEMENT B (#PCDATA)>]>')
666+
self.assertEqual(
667+
parseString(doctype + '<C><B>A</B><B>A</B></C>').toprettyxml(),
668+
decl + doctype + '\n<C><B>A</B><B>A</B></C>\n')
669+
doctype = '<!DOCTYPE C [<!ELEMENT C (B)*><!ELEMENT B (#PCDATA)>]>'
670+
self.assertEqual(
671+
parseString(doctype + '<C><B>A</B><B>A</B></C>').toprettyxml(),
672+
decl + doctype + '\n<C>\n\t<B>A</B>\n\t<B>A</B>\n</C>\n')
673+
674+
def test_toprettyxml_with_cdata_section(self):
675+
decl = '<?xml version="1.0" ?>\n'
676+
self.assertEqual(
677+
parseString('<C><![CDATA[A]]><B>A</B></C>').toprettyxml(),
678+
decl + '<C><![CDATA[A]]><B>A</B></C>\n')
635679

636680
def test_toprettyxml_preserves_content_of_text_node(self):
637-
# see issue #4147
681+
# see gh-48397
638682
for str in ('<B>A</B>', '<A><B>C</B></A>'):
639683
dom = parseString(str)
640684
dom2 = parseString(dom.toprettyxml())

Lib/xml/dom/minidom.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,10 @@ def writexml(self, writer, indent="", addindent="", newl=""):
940940
self.childNodes[0].nodeType in (
941941
Node.TEXT_NODE, Node.CDATA_SECTION_NODE)):
942942
self.childNodes[0].writexml(writer, '', '', '')
943+
elif self._preserves_whitespace():
944+
# Adding whitespace here would change the content.
945+
for node in self.childNodes:
946+
node.writexml(writer, '', '', '')
943947
else:
944948
writer.write(newl)
945949
for node in self.childNodes:
@@ -949,6 +953,25 @@ def writexml(self, writer, indent="", addindent="", newl=""):
949953
else:
950954
writer.write("/>%s"%(newl))
951955

956+
def _preserves_whitespace(self):
957+
"""Returns true iff whitespace in the content is significant.
958+
959+
This is the case if the element is marked with xml:space="preserve",
960+
if the DTD declares that its content model is not element content,
961+
or, in absence of such declaration, if it contains text.
962+
"""
963+
if self.getAttribute("xml:space") == "preserve":
964+
return True
965+
doc = self.ownerDocument
966+
info = doc and doc._get_elem_info(self)
967+
if info is not None:
968+
# Only whitespace in element content is ignorable
969+
# (see XML 1.0, 3.2.1).
970+
return not info.isElementContent()
971+
return any(node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE)
972+
and node.data.strip(_XML_WHITESPACE)
973+
for node in self.childNodes)
974+
952975
def _get_attributes(self):
953976
self._ensure_attributes()
954977
return NamedNodeMap(self._attrs, self._attrsNS, self)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` no longer
2+
adds whitespace inside an element which is marked with
3+
``xml:space="preserve"``, which is declared in the DTD as not having element
4+
content, or, in absence of such declaration, which contains text. Previously
5+
such indentation changed the content of the element.

0 commit comments

Comments
 (0)