From aaff1817a1969ff13dcca7f8ded3cad4b5f63a10 Mon Sep 17 00:00:00 2001 From: Justin Zhang Date: Thu, 30 Jul 2026 15:50:23 -0400 Subject: [PATCH] perf: remove tags in one pass in striptags instead of rebuilding per tag The tag-removal loop did `value = f"{value[:start]}{value[end + 1:]}"` for every tag it found, which copies the whole remaining string once per tag and makes striptags quadratic in the tag count. Tags are now collected in a single left-to-right pass and joined once. That is sound because the tag start mark is a single character: removing a tag can never join two characters into a new `<`, so one pass finds exactly the same tags that repeatedly searching from the beginning did. The comment-removal loop above is left alone, since ``, newline, ampersand and letters, comparing exact output. 0 mismatches. The cases cover unclosed tags, comments interleaved with tags, `<>` pairs, nested tags, a tag containing a quoted `>`, and non-ASCII inside a tag. Suite: 79 passed, 1 skipped, rc=0, matching main. ruff check and ruff format --check both clean. --- src/markupsafe/__init__.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/markupsafe/__init__.py b/src/markupsafe/__init__.py index f8a0d58b..5bb07088 100644 --- a/src/markupsafe/__init__.py +++ b/src/markupsafe/__init__.py @@ -216,12 +216,28 @@ def striptags(self, /) -> str: value = f"{value[:start]}{value[end + 3 :]}" - # remove tags using the same method - while (start := value.find("<")) != -1: - if (end := value.find(">", start)) == -1: - break - - value = f"{value[:start]}{value[end + 1 :]}" + # Remove tags. Unlike the comment mark, the tag start mark is a single + # character, so removing a tag can never join two characters into a new + # start mark. That means one left-to-right pass finds exactly the same + # tags as repeatedly searching from the beginning would, while copying + # the remainder of the string once instead of once per tag. + if (start := value.find("<")) != -1 and (end := value.find(">", start)) != -1: + chunks = [] + pos = 0 + + while True: + chunks.append(value[pos:start]) + pos = end + 1 + + if (start := value.find("<", pos)) == -1: + break + + # an unclosed tag ends the search, keeping the rest as-is + if (end := value.find(">", start)) == -1: + break + + chunks.append(value[pos:]) + value = "".join(chunks) # collapse spaces value = " ".join(value.split())