From 0351f62a3b0c9bae88c80db811b968e52becd8a9 Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:10:03 -0500 Subject: [PATCH 1/3] Fix markdown syntax leaking into header ids (#192) --- CHANGES.md | 1 + lib/markdown2.py | 17 +++++++++++++---- test/tm-cases/header_ids_footnote_ref.html | 16 ++++++++++++++++ test/tm-cases/header_ids_footnote_ref.opts | 1 + test/tm-cases/header_ids_footnote_ref.tags | 1 + test/tm-cases/header_ids_footnote_ref.text | 6 ++++++ test/tm-cases/header_ids_issue192.html | 13 +++++++++++++ test/tm-cases/header_ids_issue192.opts | 1 + test/tm-cases/header_ids_issue192.tags | 1 + test/tm-cases/header_ids_issue192.text | 15 +++++++++++++++ 10 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 test/tm-cases/header_ids_footnote_ref.html create mode 100644 test/tm-cases/header_ids_footnote_ref.opts create mode 100644 test/tm-cases/header_ids_footnote_ref.tags create mode 100644 test/tm-cases/header_ids_footnote_ref.text create mode 100644 test/tm-cases/header_ids_issue192.html create mode 100644 test/tm-cases/header_ids_issue192.opts create mode 100644 test/tm-cases/header_ids_issue192.tags create mode 100644 test/tm-cases/header_ids_issue192.text diff --git a/CHANGES.md b/CHANGES.md index 90f815ec..0f738863 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ - [pull #734] Fix empty list items being parsed as nested lists (#167) - [pull #735] Fix `smarty-pants` extra converting quotes inside inline HTML tags and comments (#150) - [pull #737] Fix XSS from code spans that the span gamut doesn't treat as code spans in safe mode (#726) +- [pull #XXX] Fix `header-ids` extra putting link URLs, image paths, inline HTML tags and emphasis markers into header ids (#192) ## python-markdown2 2.5.5 diff --git a/lib/markdown2.py b/lib/markdown2.py index 66d49a69..e1d0ff7f 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1661,6 +1661,8 @@ def _do_links(self, text: str) -> str: text = link_processor.run(text) return text + _html_tag_re = re.compile(r'<[^>]*>') + def header_id_from_text(self, text: str, prefix: str, @@ -1680,7 +1682,7 @@ def header_id_from_text(self, None to not have an id attribute and to exclude this header from the TOC (if the "toc" extra is specified). """ - header_id = _slugify(html.unescape(text)) + header_id = _slugify(html.unescape(self._html_tag_re.sub('', text))) if prefix and isinstance(prefix, str): header_id = prefix + '-' + header_id @@ -1745,16 +1747,23 @@ def _h_sub(self, match: re.Match[str]) -> str: if demote_headers: n = min(n + demote_headers, 6) header_id_attr = "" + html = self._run_span_gamut(header_group) if "header-ids" in self.extras: - # in safe mode, inline HTML is hashed by now. Unhash it so the + # build the id from the rendered header so that markdown syntax + # (link URLs, image paths, emphasis markers) doesn't end up in it. + # In safe mode, inline HTML is hashed by now. Unhash it so the # sanitized HTML doesn't get swapped back into the id attribute - header_text = self._unhash_html_spans(header_group) + header_text = self._unhash_html_spans(html) + if "footnotes" in self.extras: + # footnote refs are numbered later on. Drop the placeholder + # so it doesn't end up in the id + marker = self._footnote_marker + '-' + header_text = header_text.replace(marker, '') header_id = self.header_id_from_text( self._unescape_special_chars(header_text), self.extras["header-ids"].get('prefix'), n) if header_id: header_id_attr = ' id="%s"' % header_id - html = self._run_span_gamut(header_group) if "toc" in self.extras and header_id: self._toc_add_entry(n, header_id, html) return "%s\n\n" % (n, header_id_attr, html, n) diff --git a/test/tm-cases/header_ids_footnote_ref.html b/test/tm-cases/header_ids_footnote_ref.html new file mode 100644 index 00000000..1489d32f --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref.html @@ -0,0 +1,16 @@ +

Header with a footnote1

+ +

Linked header2

+ +
+
+
    +
  1. +

    The first note. ↩

    +
  2. + +
  3. +

    The second note. ↩

    +
  4. +
+
diff --git a/test/tm-cases/header_ids_footnote_ref.opts b/test/tm-cases/header_ids_footnote_ref.opts new file mode 100644 index 00000000..00b84fb2 --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref.opts @@ -0,0 +1 @@ +{"extras": ["header-ids", "footnotes"]} diff --git a/test/tm-cases/header_ids_footnote_ref.tags b/test/tm-cases/header_ids_footnote_ref.tags new file mode 100644 index 00000000..51fe5df8 --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref.tags @@ -0,0 +1 @@ +extra header-ids footnotes issue192 diff --git a/test/tm-cases/header_ids_footnote_ref.text b/test/tm-cases/header_ids_footnote_ref.text new file mode 100644 index 00000000..39a50b8c --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref.text @@ -0,0 +1,6 @@ +# Header with a footnote[^1] + +# [Linked](http://example.com) header[^note] + +[^1]: The first note. +[^note]: The second note. diff --git a/test/tm-cases/header_ids_issue192.html b/test/tm-cases/header_ids_issue192.html new file mode 100644 index 00000000..bec35661 --- /dev/null +++ b/test/tm-cases/header_ids_issue192.html @@ -0,0 +1,13 @@ +

Inline link

+ +

Reference link

+ +

logo Image

+ +

Bold link

+ +

Inline HTML

+ +

Strong and em

+ +

code_span stays

diff --git a/test/tm-cases/header_ids_issue192.opts b/test/tm-cases/header_ids_issue192.opts new file mode 100644 index 00000000..38b8bfe8 --- /dev/null +++ b/test/tm-cases/header_ids_issue192.opts @@ -0,0 +1 @@ +{"extras": ["header-ids"]} diff --git a/test/tm-cases/header_ids_issue192.tags b/test/tm-cases/header_ids_issue192.tags new file mode 100644 index 00000000..d01b19bc --- /dev/null +++ b/test/tm-cases/header_ids_issue192.tags @@ -0,0 +1 @@ +extra header-ids issue192 diff --git a/test/tm-cases/header_ids_issue192.text b/test/tm-cases/header_ids_issue192.text new file mode 100644 index 00000000..4ab848ae --- /dev/null +++ b/test/tm-cases/header_ids_issue192.text @@ -0,0 +1,15 @@ +# [Inline link](https://github.com) + +# [Reference link][ref] + +# ![logo](logo.png) Image + +# **[Bold link](http://example.com "Title")** + +# Inline HTML + +# __Strong__ and _em_ + +# `code_span` stays + +[ref]: http://example.com From 6610d3e9d16866a3bfcdbd9117693bd1f09374ef Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:50:58 -0500 Subject: [PATCH 2/3] Add PR number to CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0f738863..665930a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ - [pull #734] Fix empty list items being parsed as nested lists (#167) - [pull #735] Fix `smarty-pants` extra converting quotes inside inline HTML tags and comments (#150) - [pull #737] Fix XSS from code spans that the span gamut doesn't treat as code spans in safe mode (#726) -- [pull #XXX] Fix `header-ids` extra putting link URLs, image paths, inline HTML tags and emphasis markers into header ids (#192) +- [pull #743] Fix `header-ids` extra putting link URLs, image paths, inline HTML tags and emphasis markers into header ids (#192) ## python-markdown2 2.5.5 From e698958f7a4f4c86904b51c8491c92280c1a46c0 Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:44:09 -0500 Subject: [PATCH 3/3] Put the rendered-text header ids behind a strip-syntax option Changing every header id by default would break existing links to them, so header-ids keeps building ids from the markdown source unless {"strip-syntax": True} is set. The issue #192 cases now set the option, and *_default copies of them pin the unchanged default ids. --- CHANGES.md | 2 +- lib/markdown2.py | 27 +++++++++++-------- test/tm-cases/header_ids_footnote_ref.opts | 2 +- .../header_ids_footnote_ref_default.html | 16 +++++++++++ .../header_ids_footnote_ref_default.opts | 1 + .../header_ids_footnote_ref_default.tags | 1 + .../header_ids_footnote_ref_default.text | 6 +++++ test/tm-cases/header_ids_issue192.opts | 2 +- .../tm-cases/header_ids_issue192_default.html | 13 +++++++++ .../tm-cases/header_ids_issue192_default.opts | 1 + .../tm-cases/header_ids_issue192_default.tags | 1 + .../tm-cases/header_ids_issue192_default.text | 15 +++++++++++ 12 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 test/tm-cases/header_ids_footnote_ref_default.html create mode 100644 test/tm-cases/header_ids_footnote_ref_default.opts create mode 100644 test/tm-cases/header_ids_footnote_ref_default.tags create mode 100644 test/tm-cases/header_ids_footnote_ref_default.text create mode 100644 test/tm-cases/header_ids_issue192_default.html create mode 100644 test/tm-cases/header_ids_issue192_default.opts create mode 100644 test/tm-cases/header_ids_issue192_default.tags create mode 100644 test/tm-cases/header_ids_issue192_default.text diff --git a/CHANGES.md b/CHANGES.md index 665930a5..49c1cc35 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,7 @@ - [pull #734] Fix empty list items being parsed as nested lists (#167) - [pull #735] Fix `smarty-pants` extra converting quotes inside inline HTML tags and comments (#150) - [pull #737] Fix XSS from code spans that the span gamut doesn't treat as code spans in safe mode (#726) -- [pull #743] Fix `header-ids` extra putting link URLs, image paths, inline HTML tags and emphasis markers into header ids (#192) +- [pull #743] Add a `strip-syntax` option to the `header-ids` extra that leaves link URLs, image paths, inline HTML tags and emphasis markers out of header ids (#192) ## python-markdown2 2.5.5 diff --git a/lib/markdown2.py b/lib/markdown2.py index e1d0ff7f..a494b286 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -56,7 +56,9 @@ * footnotes: Support footnotes as in use on daringfireball.net and implemented in other Markdown processors (tho not in Markdown.pl v1.0.1). * header-ids: Adds "id" attributes to headers. The id value is a slug of - the header text. + the header text. With `{"strip-syntax": True}`, the slug is made from the + rendered header, so link URLs, image paths, inline HTML tags and emphasis + markers are left out of it. * highlightjs-lang: Allows specifying the language which used for syntax highlighting when using fenced-code-blocks and highlightjs. * html-classes: Takes a dict mapping html tag names (lowercase) to a @@ -1682,7 +1684,7 @@ def header_id_from_text(self, None to not have an id attribute and to exclude this header from the TOC (if the "toc" extra is specified). """ - header_id = _slugify(html.unescape(self._html_tag_re.sub('', text))) + header_id = _slugify(html.unescape(text)) if prefix and isinstance(prefix, str): header_id = prefix + '-' + header_id @@ -1749,16 +1751,19 @@ def _h_sub(self, match: re.Match[str]) -> str: header_id_attr = "" html = self._run_span_gamut(header_group) if "header-ids" in self.extras: - # build the id from the rendered header so that markdown syntax - # (link URLs, image paths, emphasis markers) doesn't end up in it. - # In safe mode, inline HTML is hashed by now. Unhash it so the + # in safe mode, inline HTML is hashed by now. Unhash it so the # sanitized HTML doesn't get swapped back into the id attribute - header_text = self._unhash_html_spans(html) - if "footnotes" in self.extras: - # footnote refs are numbered later on. Drop the placeholder - # so it doesn't end up in the id - marker = self._footnote_marker + '-' - header_text = header_text.replace(marker, '') + if self.extras["header-ids"].get('strip-syntax'): + # build the id from the rendered header so that markdown syntax + # (link URLs, image paths, emphasis markers) doesn't end up in it. + header_text = self._html_tag_re.sub('', self._unhash_html_spans(html)) + if "footnotes" in self.extras: + # footnote refs are numbered later on. Drop the placeholder + # so it doesn't end up in the id + marker = self._footnote_marker + '-' + header_text = header_text.replace(marker, '') + else: + header_text = self._unhash_html_spans(header_group) header_id = self.header_id_from_text( self._unescape_special_chars(header_text), self.extras["header-ids"].get('prefix'), n) diff --git a/test/tm-cases/header_ids_footnote_ref.opts b/test/tm-cases/header_ids_footnote_ref.opts index 00b84fb2..fa2d42bf 100644 --- a/test/tm-cases/header_ids_footnote_ref.opts +++ b/test/tm-cases/header_ids_footnote_ref.opts @@ -1 +1 @@ -{"extras": ["header-ids", "footnotes"]} +{"extras": {"header-ids": {"strip-syntax": True}, "footnotes": None}} diff --git a/test/tm-cases/header_ids_footnote_ref_default.html b/test/tm-cases/header_ids_footnote_ref_default.html new file mode 100644 index 00000000..7c15fb22 --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref_default.html @@ -0,0 +1,16 @@ +

Header with a footnote1

+ +

Linked header2

+ +
+
+
    +
  1. +

    The first note. ↩

    +
  2. + +
  3. +

    The second note. ↩

    +
  4. +
+
diff --git a/test/tm-cases/header_ids_footnote_ref_default.opts b/test/tm-cases/header_ids_footnote_ref_default.opts new file mode 100644 index 00000000..00b84fb2 --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref_default.opts @@ -0,0 +1 @@ +{"extras": ["header-ids", "footnotes"]} diff --git a/test/tm-cases/header_ids_footnote_ref_default.tags b/test/tm-cases/header_ids_footnote_ref_default.tags new file mode 100644 index 00000000..51fe5df8 --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref_default.tags @@ -0,0 +1 @@ +extra header-ids footnotes issue192 diff --git a/test/tm-cases/header_ids_footnote_ref_default.text b/test/tm-cases/header_ids_footnote_ref_default.text new file mode 100644 index 00000000..39a50b8c --- /dev/null +++ b/test/tm-cases/header_ids_footnote_ref_default.text @@ -0,0 +1,6 @@ +# Header with a footnote[^1] + +# [Linked](http://example.com) header[^note] + +[^1]: The first note. +[^note]: The second note. diff --git a/test/tm-cases/header_ids_issue192.opts b/test/tm-cases/header_ids_issue192.opts index 38b8bfe8..b71e6519 100644 --- a/test/tm-cases/header_ids_issue192.opts +++ b/test/tm-cases/header_ids_issue192.opts @@ -1 +1 @@ -{"extras": ["header-ids"]} +{"extras": {"header-ids": {"strip-syntax": True}}} diff --git a/test/tm-cases/header_ids_issue192_default.html b/test/tm-cases/header_ids_issue192_default.html new file mode 100644 index 00000000..20c66497 --- /dev/null +++ b/test/tm-cases/header_ids_issue192_default.html @@ -0,0 +1,13 @@ +

Inline link

+ +

Reference link

+ +

logo Image

+ +

Bold link

+ +

Inline HTML

+ +

Strong and em

+ +

code_span stays

diff --git a/test/tm-cases/header_ids_issue192_default.opts b/test/tm-cases/header_ids_issue192_default.opts new file mode 100644 index 00000000..38b8bfe8 --- /dev/null +++ b/test/tm-cases/header_ids_issue192_default.opts @@ -0,0 +1 @@ +{"extras": ["header-ids"]} diff --git a/test/tm-cases/header_ids_issue192_default.tags b/test/tm-cases/header_ids_issue192_default.tags new file mode 100644 index 00000000..d01b19bc --- /dev/null +++ b/test/tm-cases/header_ids_issue192_default.tags @@ -0,0 +1 @@ +extra header-ids issue192 diff --git a/test/tm-cases/header_ids_issue192_default.text b/test/tm-cases/header_ids_issue192_default.text new file mode 100644 index 00000000..4ab848ae --- /dev/null +++ b/test/tm-cases/header_ids_issue192_default.text @@ -0,0 +1,15 @@ +# [Inline link](https://github.com) + +# [Reference link][ref] + +# ![logo](logo.png) Image + +# **[Bold link](http://example.com "Title")** + +# Inline HTML + +# __Strong__ and _em_ + +# `code_span` stays + +[ref]: http://example.com