Skip to content

Commit 3df1482

Browse files
jonbaldieclaude
andcommitted
gh-158860: Make minidom Node.normalize() linear in adjacent text nodes
Merging each text node with `node.data + child.data` copies the growing string once per absorbed node. Collect the pieces and join each run once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9112dae commit 3df1482

3 files changed

Lines changed: 29 additions & 1 deletion

File tree

‎Lib/test/test_minidom.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,21 @@ def testNormalizeDeleteAndCombine(self):
16051605
, "testNormalizeDeleteAndCombine -- result")
16061606
doc.unlink()
16071607

1608+
def testNormalizeManyTextNodes(self):
1609+
# Ensure that normalize() is fast with many adjacent text nodes.
1610+
N = 100_000
1611+
doc = parseString("<doc/>")
1612+
root = doc.documentElement
1613+
for i in range(N):
1614+
root.appendChild(doc.createTextNode("x" * 16))
1615+
if i % 3 == 0:
1616+
root.appendChild(doc.createTextNode(""))
1617+
doc.normalize()
1618+
self.assertEqual(len(root.childNodes), 1)
1619+
self.assertEqual(root.firstChild.data, "x" * 16 * N)
1620+
self.assertIsNone(root.firstChild.nextSibling)
1621+
doc.unlink()
1622+
16081623
def testNormalizeRecursion(self):
16091624
doc = parseString("<doc>"
16101625
"<o>"

‎Lib/xml/dom/minidom.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ def removeChild(self, oldChild):
196196

197197
def normalize(self):
198198
L = []
199+
# (text node, [data, ...]) for each text node that absorbs others.
200+
# Join each run once at the end; concatenating as we go is quadratic.
201+
runs = None
199202
for child in self.childNodes:
200203
if child.nodeType == Node.TEXT_NODE:
201204
if not child.data:
@@ -208,7 +211,12 @@ def normalize(self):
208211
elif L and L[-1].nodeType == child.nodeType:
209212
# collapse text node
210213
node = L[-1]
211-
node.data = node.data + child.data
214+
if runs is None:
215+
runs = []
216+
if runs and runs[-1][0] is node:
217+
runs[-1][1].append(child.data)
218+
else:
219+
runs.append((node, [node.data, child.data]))
212220
node.nextSibling = child.nextSibling
213221
if child.nextSibling:
214222
child.nextSibling.previousSibling = node
@@ -219,6 +227,9 @@ def normalize(self):
219227
L.append(child)
220228
if child.nodeType == Node.ELEMENT_NODE:
221229
child.normalize()
230+
if runs is not None:
231+
for node, data in runs:
232+
node.data = ''.join(data)
222233
self.childNodes[:] = L
223234

224235
def cloneNode(self, deep):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:meth:`xml.dom.minidom.Node.normalize` now merges runs of adjacent text
2+
nodes in linear time instead of quadratic time.

0 commit comments

Comments
 (0)