From 2159deb7474a04eb725cb53cbc9fb7c9630f44da Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Thu, 9 Jul 2026 19:21:15 +0530 Subject: [PATCH 1/2] Fix header-ids extra generating duplicate ids (#661) The `header-ids` extra de-duplicated ids using only a per-base-slug counter. Appending that counter to the base slug could produce an id that collides with a differently-named header. For example, two `# Chapter` headers yield `chapter` and `chapter-2`, but a following `# Chapter 2` also slugifies to the base `chapter-2` and was emitted verbatim, duplicating the id. Track the set of ids already emitted and, after building the candidate id, keep bumping the counter until the id is genuinely unique. So the third header now becomes `chapter-2-2` instead of a duplicate `chapter-2`. Fixes #661. --- CHANGES.md | 1 + lib/markdown2.py | 16 +++++++++++++--- test/tm-cases/header_ids_duplicate.html | 11 +++++++++++ test/tm-cases/header_ids_duplicate.opts | 1 + test/tm-cases/header_ids_duplicate.tags | 1 + test/tm-cases/header_ids_duplicate.text | 11 +++++++++++ 6 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 test/tm-cases/header_ids_duplicate.html create mode 100644 test/tm-cases/header_ids_duplicate.opts create mode 100644 test/tm-cases/header_ids_duplicate.tags create mode 100644 test/tm-cases/header_ids_duplicate.text diff --git a/CHANGES.md b/CHANGES.md index 157e8341..97a54dfb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ - [pull #701] Allow boolean attribute syntax in `markdown-in-html` extra - [pull #704] Fix XSS from smuggling spans into image attributes (#702, #703) - [pull #710] Add emoji support (#709) +- Fix `header-ids` extra generating duplicate ids when a suffixed id collides with another header (#661) ## python-markdown2 2.5.5 diff --git a/lib/markdown2.py b/lib/markdown2.py index 8dd1c48e..f2177919 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -412,6 +412,8 @@ def _setup_extras(self): if "header-ids" in self.extras: if not hasattr(self, '_count_from_header_id') or self.extras['header-ids'].get('reset-count', False): self._count_from_header_id = defaultdict(int) + if not hasattr(self, '_header_ids_seen') or self.extras['header-ids'].get('reset-count', False): + self._header_ids_seen = set() if "metadata" in self.extras: self.metadata: dict[str, Any] = {} @@ -1582,9 +1584,17 @@ def header_id_from_text(self, if prefix and isinstance(prefix, str): header_id = prefix + '-' + header_id - self._count_from_header_id[header_id] += 1 - if 0 == len(header_id) or self._count_from_header_id[header_id] > 1: - header_id += '-%s' % self._count_from_header_id[header_id] + base_id = header_id + self._count_from_header_id[base_id] += 1 + if 0 == len(base_id) or self._count_from_header_id[base_id] > 1: + header_id = '%s-%s' % (base_id, self._count_from_header_id[base_id]) + # A suffixed id may still collide with a differently-named header + # (e.g. "# Chapter" twice yields "chapter-2", which clashes with + # "# Chapter 2"). Keep bumping until the id is genuinely unique. + while header_id in self._header_ids_seen: + self._count_from_header_id[base_id] += 1 + header_id = '%s-%s' % (base_id, self._count_from_header_id[base_id]) + self._header_ids_seen.add(header_id) return header_id diff --git a/test/tm-cases/header_ids_duplicate.html b/test/tm-cases/header_ids_duplicate.html new file mode 100644 index 00000000..e78f25eb --- /dev/null +++ b/test/tm-cases/header_ids_duplicate.html @@ -0,0 +1,11 @@ +

Chapter

+ +

Test

+ +

Chapter

+ +

Test

+ +

Chapter 2

+ +

Test

diff --git a/test/tm-cases/header_ids_duplicate.opts b/test/tm-cases/header_ids_duplicate.opts new file mode 100644 index 00000000..38b8bfe8 --- /dev/null +++ b/test/tm-cases/header_ids_duplicate.opts @@ -0,0 +1 @@ +{"extras": ["header-ids"]} diff --git a/test/tm-cases/header_ids_duplicate.tags b/test/tm-cases/header_ids_duplicate.tags new file mode 100644 index 00000000..cb381121 --- /dev/null +++ b/test/tm-cases/header_ids_duplicate.tags @@ -0,0 +1 @@ +extra header-ids issue661 diff --git a/test/tm-cases/header_ids_duplicate.text b/test/tm-cases/header_ids_duplicate.text new file mode 100644 index 00000000..fc3a5245 --- /dev/null +++ b/test/tm-cases/header_ids_duplicate.text @@ -0,0 +1,11 @@ +# Chapter + +Test + +# Chapter + +Test + +# Chapter 2 + +Test From 43b9f1f5499b5b7f52df5f2dff22017a6af63a6c Mon Sep 17 00:00:00 2001 From: Apoorv Darshan Date: Mon, 13 Jul 2026 15:59:23 +0530 Subject: [PATCH 2/2] Match changelog entry format --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 97a54dfb..5b2b9528 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,7 +9,7 @@ - [pull #701] Allow boolean attribute syntax in `markdown-in-html` extra - [pull #704] Fix XSS from smuggling spans into image attributes (#702, #703) - [pull #710] Add emoji support (#709) -- Fix `header-ids` extra generating duplicate ids when a suffixed id collides with another header (#661) +- [pull #713] Fix `header-ids` extra generating duplicate ids when a suffixed id collides with another header (#661) ## python-markdown2 2.5.5