diff --git a/Doc/library/xml.dom.minidom.rst b/Doc/library/xml.dom.minidom.rst index efc81f31e36a5be..eb4984e2b53d929 100644 --- a/Doc/library/xml.dom.minidom.rst +++ b/Doc/library/xml.dom.minidom.rst @@ -187,6 +187,12 @@ module documentation. This section lists the differences between the API and The *standalone* argument behaves exactly as in :meth:`writexml`. + No indentation is 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, + because this would change its content. + .. versionchanged:: 3.8 The :meth:`toprettyxml` method now preserves the attribute order specified by the user. @@ -194,6 +200,10 @@ module documentation. This section lists the differences between the API and .. versionchanged:: 3.9 The *standalone* parameter was added. + .. versionchanged:: next + Whitespace is no longer added inside an element with mixed content + or marked with ``xml:space="preserve"``. + .. _dom-example: DOM Example diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst index 1869d9b47780a9b..5c4f7b3c561633e 100644 --- a/Doc/library/xml.etree.elementtree.rst +++ b/Doc/library/xml.etree.elementtree.rst @@ -631,8 +631,16 @@ Functions characters by default. For indenting partial subtrees inside of an already indented tree, pass the initial indentation level as *level*. + No whitespace is added inside an element + which is marked with ``xml:space="preserve"`` + or which contains text, because this would change its content. + .. versionadded:: 3.9 + .. versionchanged:: next + Whitespace is no longer added inside an element with mixed content + or marked with ``xml:space="preserve"``. + .. function:: iselement(element) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 1098b152e51eb41..88d7face8949e2a 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -702,6 +702,14 @@ xml and :meth:`!Document.createEntityReference`. (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) +* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` + and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree` + no longer add whitespace inside an element + which is marked with ``xml:space="preserve"`` or which contains text. + :meth:`!toprettyxml` also takes into account + the content model declared in the DTD. + (Contributed by Serhiy Storchaka in :gh:`81623`.) + * Add :meth:`!GetSpecifiedAttributeCount` method to the :mod:`XML parser ` objects. It tells how many of the reported attributes were given in the start tag @@ -940,6 +948,15 @@ that may require changes to your code. Attributes defaulted in the DTD are no longer omitted when parsing. (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) +* :meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` + and :func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree` + no longer add whitespace inside an element + which is marked with ``xml:space="preserve"`` or which contains text, + because this changed the content of the element. + :meth:`!toprettyxml` also takes into account + the content model declared in the DTD. + (Contributed by Serhiy Storchaka in :gh:`81623`.) + * On Windows, seeking a pipe now fails instead of silently appearing to succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`, and :meth:`~io.IOBase.seekable` returns ``False``. As a consequence, diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 2a7c3293174a249..837a54a64379f74 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -617,32 +617,76 @@ def testAltNewline(self): self.assertEqual(domstr, str.replace("\n", "\r\n")) def test_toprettyxml_with_text_nodes(self): - # see issue #4147, text nodes are not indented + # see gh-48397 and gh-81623, + # the content of an element with text is not changed decl = '\n' self.assertEqual(parseString('A').toprettyxml(), decl + 'A\n') self.assertEqual(parseString('AA').toprettyxml(), - decl + '\n\tA\n\tA\n\n') + decl + 'AA\n') self.assertEqual(parseString('AA').toprettyxml(), - decl + '\n\tA\n\tA\n\n') + decl + 'AA\n') self.assertEqual(parseString('AA').toprettyxml(), decl + '\n\tA\n\tA\n\n') self.assertEqual(parseString('AAA').toprettyxml(), - decl + '\n\tA\n\tA\n\tA\n\n') + decl + 'AAA\n') + # toprettyxml treats whitespace between elements as insignificant + self.assertEqual(parseString(' A ').toprettyxml(), + decl + '\n\t \n\tA\n\t \n\n') def test_toprettyxml_with_adjacent_text_nodes(self): - # see issue #4147, adjacent text nodes are indented normally + # see gh-81623, adjacent text nodes are not separated dom = Document() elem = dom.createElement('elem') elem.appendChild(dom.createTextNode('TEXT')) elem.appendChild(dom.createTextNode('TEXT')) dom.appendChild(elem) decl = '\n' - self.assertEqual(dom.toprettyxml(), - decl + '\n\tTEXT\n\tTEXT\n\n') + self.assertEqual(dom.toprettyxml(), decl + 'TEXTTEXT\n') + + def test_toprettyxml_preserve(self): + decl = '\n' + # xml:space="preserve" applies to the whole subtree + self.assertEqual( + parseString('AA' + ).toprettyxml(), + decl + 'AA\n') + self.assertEqual( + parseString('' + ).toprettyxml(), + decl + '\n') + # other values do not preserve whitespace + self.assertEqual( + parseString('A').toprettyxml(), + decl + '\n\tA\n\n') + + def test_toprettyxml_with_non_xml_whitespace(self): + # only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3) + decl = '\n' + self.assertEqual(parseString('\xa0A').toprettyxml(), + decl + '\xa0A\n') + + def test_toprettyxml_with_dtd(self): + decl = '\n' + # only whitespace in element content is ignorable + doctype = ('' + ']>') + self.assertEqual( + parseString(doctype + 'AA').toprettyxml(), + decl + doctype + '\nAA\n') + doctype = ']>' + self.assertEqual( + parseString(doctype + 'AA').toprettyxml(), + decl + doctype + '\n\n\tA\n\tA\n\n') + + def test_toprettyxml_with_cdata_section(self): + decl = '\n' + self.assertEqual( + parseString('A').toprettyxml(), + decl + 'A\n') def test_toprettyxml_preserves_content_of_text_node(self): - # see issue #4147 + # see gh-48397 for str in ('A', 'C'): dom = parseString(str) dom2 = parseString(dom.toprettyxml()) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 90e556ec95308bc..7f7d445f832b5fb 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -773,9 +773,10 @@ def test_indent(self): ET.indent(elem) self.assertEqual(ET.tostring(elem), b'\n text\n') + # an element with mixed content is not indented elem = ET.XML("texttail") ET.indent(elem) - self.assertEqual(ET.tostring(elem), b'\n texttail') + self.assertEqual(ET.tostring(elem), b'texttail') elem = ET.XML("

par

\n

text

\t


") ET.indent(elem) @@ -851,9 +852,39 @@ def test_indent_non_xml_whitespace(self): ET.indent(elem) self.assertEqual( ET.tostring(elem), - b' \n

text

 \n' + b' 

text

 ' ) + def test_indent_preserve(self): + # xml:space="preserve" applies to the whole subtree + elem = ET.XML('

text

') + ET.indent(elem) + self.assertEqual( + ET.tostring(elem), + b'

text

' + ) + # other values do not preserve whitespace + elem = ET.XML('

text

') + ET.indent(elem) + self.assertEqual( + ET.tostring(elem), + b'\n' + b' \n' + b'

text

\n' + b' \n' + b'' + ) + + def test_indent_mixed_content(self): + # whitespace in an element which contains text is significant + elem = ET.XML('

hello x y

') + ET.indent(elem) + self.assertEqual(ET.tostring(elem), b'

hello x y

') + # the subtree of such element is not indented either + elem = ET.XML('

hello y

') + ET.indent(elem) + self.assertEqual(ET.tostring(elem), b'

hello y

') + def test_indent_level(self): elem = ET.XML("

pre
post

text

") with self.assertRaises(ValueError): diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 7639fa14c5050fb..93c2e0638493e33 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -940,6 +940,10 @@ def writexml(self, writer, indent="", addindent="", newl=""): self.childNodes[0].nodeType in ( Node.TEXT_NODE, Node.CDATA_SECTION_NODE)): self.childNodes[0].writexml(writer, '', '', '') + elif self._preserves_whitespace(): + # Adding whitespace here would change the content. + for node in self.childNodes: + node.writexml(writer, '', '', '') else: writer.write(newl) for node in self.childNodes: @@ -949,6 +953,25 @@ def writexml(self, writer, indent="", addindent="", newl=""): else: writer.write("/>%s"%(newl)) + def _preserves_whitespace(self): + """Returns true iff whitespace in the content is significant. + + This is the case if the element is marked with xml:space="preserve", + if the DTD declares that its content model is not element content, + or, in absence of such declaration, if it contains text. + """ + if self.getAttribute("xml:space") == "preserve": + return True + doc = self.ownerDocument + info = doc and doc._get_elem_info(self) + if info is not None: + # Only whitespace in element content is ignorable + # (see XML 1.0, 3.2.1). + return not info.isElementContent() + return any(node.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE) + and node.data.strip(_XML_WHITESPACE) + for node in self.childNodes) + def _get_attributes(self): self._ensure_attributes() return NamedNodeMap(self._attrs, self._attrsNS, self) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index ce98e4dc24a0d3a..8ff4a3612699f02 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -104,6 +104,9 @@ # The white space characters of the XML specification (see XML 1.0, 2.3). _XML_WHITESPACE = " \t\r\n" +# The xml:space attribute (see XML 1.0, 2.10). +_XML_SPACE = "{http://www.w3.org/XML/1998/namespace}space" + class ParseError(SyntaxError): """An error when parsing an XML document. @@ -1196,7 +1199,20 @@ def indent(tree, space=" ", level=0): # Reduce the memory consumption by reusing indentation strings. indentations = ["\n" + level * space] + def _preserves_whitespace(elem): + # True iff whitespace in the content of the element is significant. + if elem.get(_XML_SPACE) == "preserve": + return True + if elem.text and elem.text.strip(_XML_WHITESPACE): + return True + return any(child.tail and child.tail.strip(_XML_WHITESPACE) + for child in elem) + def _indent_children(elem, level): + if _preserves_whitespace(elem): + # Adding whitespace here would change the content. + return + # Start a new indentation level for the first child. child_level = level + 1 try: @@ -1205,18 +1221,15 @@ def _indent_children(elem, level): child_indentation = indentations[level] + space indentations.append(child_indentation) - if not elem.text or not elem.text.strip(_XML_WHITESPACE): - elem.text = child_indentation + elem.text = child_indentation for child in elem: if len(child): _indent_children(child, child_level) - if not child.tail or not child.tail.strip(_XML_WHITESPACE): - child.tail = child_indentation + child.tail = child_indentation # Dedent after the last child by overwriting the previous indentation. - if not child.tail.strip(_XML_WHITESPACE): - child.tail = indentations[level] + child.tail = indentations[level] _indent_children(tree, 0) diff --git a/Misc/NEWS.d/next/Library/2026-08-30-18-00-00.gh-issue-81623.Vh2Kt6.rst b/Misc/NEWS.d/next/Library/2026-08-30-18-00-00.gh-issue-81623.Vh2Kt6.rst new file mode 100644 index 000000000000000..bdc02a4c55ee03b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-30-18-00-00.gh-issue-81623.Vh2Kt6.rst @@ -0,0 +1,6 @@ +:meth:`~xml.dom.minidom.Node.toprettyxml` in :mod:`xml.dom.minidom` and +:func:`~xml.etree.ElementTree.indent` in :mod:`xml.etree.ElementTree` no longer +add whitespace inside an element which is marked with ``xml:space="preserve"`` +or which contains text (:meth:`!toprettyxml` also takes into account the +content model declared in the DTD). Previously such indentation changed the +content of the element.