Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-mistletoe for openSUSE:Factory checked in at 2026-10-14 18:33:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-mistletoe (Old) and /work/SRC/openSUSE:Factory/.python-mistletoe.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-mistletoe" Wed Oct 14 18:33:22 2026 rev:7 rq:1384372 version:1.6.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-mistletoe/python-mistletoe.changes 2026-03-17 19:04:25.708499961 +0100 +++ /work/SRC/openSUSE:Factory/.python-mistletoe.new.3698736/python-mistletoe.changes 2026-10-14 18:33:37.100058093 +0200 @@ -1,0 +2,18 @@ +Mon Oct 5 16:25:52 UTC 2026 - Dirk Müller <[email protected]> + +- update to 1.6.0: + * Incorrect escaping of `$` and other reserved chars in links + (#248). + * Fix `` being rendered as `...` (#260). + * `HtmlRenderer`: Escape characters in image src. + * `MarkdownRenderer`: Fix type annotations (#265, #267). + * `JiraRenderer`: Escape pipes in table cells (#270). + * `MarkdownRenderer`: Escape pipes in table cells (#274). + * `MarkdownRenderer`: Preserve line break before/after HTML + span during word-wrap (#275). + * Avoid crash on overlapping emphasis delimiters (#271). 🎉 + * `GithubWiki`: Reduce backtracking in wiki links (#276). + * Guard `remove_token()` against duplicate removal (#281). + * `HtmlRenderer`: Escape mailto autolink hrefs (#283). + +------------------------------------------------------------------- Old: ---- mistletoe-1.5.1.tar.gz New: ---- mistletoe-1.6.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-mistletoe.spec ++++++ --- /var/tmp/diff_new_pack.nveh8A/_old 2026-10-14 18:33:37.852089938 +0200 +++ /var/tmp/diff_new_pack.nveh8A/_new 2026-10-14 18:33:37.854090022 +0200 @@ -23,7 +23,7 @@ %endif %{?sle15_python_module_pythons} Name: python-mistletoe -Version: 1.5.1 +Version: 1.6.0 Release: 0 Summary: A Markdown parser in pure Python License: MIT ++++++ mistletoe-1.5.1.tar.gz -> mistletoe-1.6.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/.github/workflows/python-package.yml new/mistletoe-1.6.0/.github/workflows/python-package.yml --- old/mistletoe-1.5.1/.github/workflows/python-package.yml 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/.github/workflows/python-package.yml 2026-07-11 16:09:13.000000000 +0200 @@ -18,12 +18,13 @@ # As long as we want to support Python 3.6-, we need to stick to an older version of Ubuntu, # see <https://github.com/actions/setup-python/issues/544>. # - # Update: ubuntu-20.04 is no longer available, see <https://github.com/actions/runner-images/issues/11101>. + # UPDATE: ubuntu-20.04 is no longer available, see <https://github.com/actions/runner-images/issues/11101>. + # So we also need to remove versions 3.5 and 3.6 from the list to avoid this CI error: "The version 'X.Y' with architecture 'x64' was not found for Ubuntu 22.04." runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/dev-guide.md new/mistletoe-1.6.0/dev-guide.md --- old/mistletoe-1.5.1/dev-guide.md 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/dev-guide.md 2026-07-11 16:09:13.000000000 +0200 @@ -307,7 +307,7 @@ if isinstance(token, RawText): token.content = token.content.replace("mistletoe", "The Amazing mistletoe") - if not isinstance(token, InlineCode) and hasattr(token, "children"): + if not isinstance(token, InlineCode) and token.children is not None: for child in token.children: update_text(child) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/__init__.py new/mistletoe-1.6.0/mistletoe/__init__.py --- old/mistletoe-1.5.1/mistletoe/__init__.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/__init__.py 2026-07-11 16:09:13.000000000 +0200 @@ -2,7 +2,7 @@ Make mistletoe easier to import. """ -__version__ = "1.5.1" +__version__ = "1.6.0" __all__ = ['html_renderer', 'ast_renderer', 'block_token', 'block_tokenizer', 'span_token', 'span_tokenizer'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/base_renderer.py new/mistletoe-1.6.0/mistletoe/base_renderer.py --- old/mistletoe-1.5.1/mistletoe/base_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/base_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -203,3 +203,9 @@ def render_document(self, token: block_token.Document) -> str: return self.render_inner(token) + + +# Safe characters for URL quoting. These are basically all the URI +# reserved characters as per [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-2.2). +# Plus we add the percent character (%) to avoid double-escaping. +URI_SAFE_CHARACTERS = ":/?#[]@!$&'()*+,;=%" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/block_token.py new/mistletoe-1.6.0/mistletoe/block_token.py --- old/mistletoe-1.5.1/mistletoe/block_token.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/block_token.py 2026-07-11 16:09:13.000000000 +0200 @@ -59,7 +59,8 @@ Arguments: token_cls (BlockToken): token to be removed from the parsing process. """ - _token_types.remove(token_cls) + if token_cls in _token_types: + _token_types.remove(token_cls) def reset_tokens(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/contrib/github_wiki.py new/mistletoe-1.6.0/mistletoe/contrib/github_wiki.py --- old/mistletoe-1.5.1/mistletoe/contrib/github_wiki.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/contrib/github_wiki.py 2026-07-11 16:09:13.000000000 +0200 @@ -11,7 +11,8 @@ class GithubWiki(SpanToken): - pattern = re.compile(r"\[\[ *(.+?) *\| *(.+?) *\]\]") + # In order to avoid inefficient backtracking, we use positive lookahead here. + pattern = re.compile(r"\[\[(?=[^\n|]*\|[^\n]*\]\]) *([^\n|]+?) *\| *([^\n]+?) *\]\]") def __init__(self, match): self.target = match.group(2) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/contrib/jira_renderer.py new/mistletoe-1.6.0/mistletoe/contrib/jira_renderer.py --- old/mistletoe-1.5.1/mistletoe/contrib/jira_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/contrib/jira_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -23,7 +23,7 @@ from itertools import chain from mistletoe import block_token, span_token -from mistletoe.base_renderer import BaseRenderer +from mistletoe.base_renderer import BaseRenderer, URI_SAFE_CHARACTERS import re @@ -68,7 +68,7 @@ inner = self.render_inner(token) target = escape_url(token.target) if token.title: - title = '|{}'.format(token.title) + title = '|{}'.format(escape_link_chars(token.title)) else: title = '' @@ -191,7 +191,7 @@ else: template = '|{inner}' - inner = self.render_inner(token) + inner = self.render_inner(token).replace("|", "\\|") if inner == '': inner = ' ' return template.format(inner=inner) @@ -231,10 +231,21 @@ def escape_url(raw): """ - Escape urls to prevent code injection craziness. (Hopefully.) + Escapes the URL part of a Jira link. """ from urllib.parse import quote - return quote(raw, safe='/#:()*?=%@+,&;') + return escape_link_chars(quote(raw, safe=URI_SAFE_CHARACTERS)) + + +def escape_link_chars(s: str) -> str: + """ + Escapes special characters that are used in the Jira link syntax. + """ + s = s.replace("[", "\\[") + s = s.replace("]", "\\]") + # Note: Escaping pipe doesn't seem to have an effect in the tested Jira version. + s = s.replace("|", "\\|") + return s JIRARenderer = JiraRenderer diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/contrib/xwiki20_renderer.py new/mistletoe-1.6.0/mistletoe/contrib/xwiki20_renderer.py --- old/mistletoe-1.5.1/mistletoe/contrib/xwiki20_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/contrib/xwiki20_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -1,6 +1,6 @@ from itertools import chain from mistletoe import block_token, span_token -from mistletoe.base_renderer import BaseRenderer +from mistletoe.base_renderer import BaseRenderer, URI_SAFE_CHARACTERS class XWiki20Renderer(BaseRenderer): @@ -234,4 +234,4 @@ Escape urls to prevent code injection craziness. (Hopefully.) """ from urllib.parse import quote - return quote(raw, safe='/#:()*?=%@+,&;') + return quote(raw, safe=URI_SAFE_CHARACTERS) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/core_tokens.py new/mistletoe-1.6.0/mistletoe/core_tokens.py --- old/mistletoe-1.5.1/mistletoe/core_tokens.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/core_tokens.py 2026-07-11 16:09:13.000000000 +0200 @@ -439,9 +439,10 @@ self.number = self.end - self.start self.type = self.type[n:] return True + # Trim closing markers from the right so type matches start/end. self.end = self.end - n self.number = self.end - self.start - self.type = self.type[:n] + self.type = self.type[:-n] return True def closed_by(self, other): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/html_renderer.py new/mistletoe-1.6.0/mistletoe/html_renderer.py --- old/mistletoe-1.5.1/mistletoe/html_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/html_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -10,6 +10,7 @@ from mistletoe.block_token import HtmlBlock from mistletoe.span_token import HtmlSpan from mistletoe.base_renderer import BaseRenderer +from mistletoe.base_renderer import URI_SAFE_CHARACTERS class HtmlRenderer(BaseRenderer): @@ -73,11 +74,12 @@ def render_image(self, token: span_token.Image) -> str: template = '<img src="{}" alt="{}"{} />' + src = self.escape_url(token.src) if token.title: title = ' title="{}"'.format(html.escape(token.title)) else: title = '' - return template.format(token.src, self.render_to_plain(token), title) + return template.format(src, self.render_to_plain(token), title) def render_link(self, token: span_token.Link) -> str: template = '<a href="{target}"{title}>{inner}</a>' @@ -92,7 +94,7 @@ def render_auto_link(self, token: span_token.AutoLink) -> str: template = '<a href="{target}">{inner}</a>' if token.mailto: - target = 'mailto:{}'.format(token.target) + target = self.escape_url('mailto:{}'.format(token.target)) else: target = self.escape_url(token.target) inner = self.render_inner(token) @@ -235,7 +237,7 @@ """ Escape urls to prevent code injection craziness. (Hopefully.) """ - return html.escape(quote(raw, safe='/#:()*?=%@+,&;')) + return html.escape(quote(raw, safe=URI_SAFE_CHARACTERS)) HTMLRenderer = HtmlRenderer diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/latex_renderer.py new/mistletoe-1.6.0/mistletoe/latex_renderer.py --- old/mistletoe-1.5.1/mistletoe/latex_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/latex_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -6,7 +6,7 @@ from itertools import chain from urllib.parse import quote import mistletoe.latex_token as latex_token -from mistletoe.base_renderer import BaseRenderer +from mistletoe.base_renderer import BaseRenderer, URI_SAFE_CHARACTERS # (customizable) delimiters for inline code verb_delimiters = string.punctuation + string.digits @@ -184,16 +184,12 @@ """ Quote unsafe chars in urls & escape as needed for LaTeX's hyperref. - %-escapes all characters that are neither in the unreserved chars - ("always safe" as per RFC 2396 or RFC 3986) nor in the chars set - '/#:()*?=%@+,&;' - - Subsequently, LaTeX-escapes '%' and '#' for hyperref's \\url{} to also + LaTeX-escapes '%' and '#' for hyperref's \\url{} to also work if used within macros like \\multicolumn. if \\url{} with urls containing '%' or '#' is used outside of multicolumn-macros, they work regardless of whether these characters are escaped, and the result remains the same (at least for pdflatex from TeX Live 2019). """ - quoted_url = quote(raw, safe='/#:()*?=%@+,&;') + quoted_url = quote(raw, safe=URI_SAFE_CHARACTERS) return quoted_url.replace('%', '\\%') \ .replace('#', '\\#') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/markdown_renderer.py new/mistletoe-1.6.0/mistletoe/markdown_renderer.py --- old/mistletoe-1.5.1/mistletoe/markdown_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/markdown_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -4,7 +4,7 @@ import re from itertools import chain -from typing import Iterable, Sequence +from typing import Iterable, Optional, Sequence from mistletoe import block_token, span_token, token from mistletoe.base_renderer import BaseRenderer @@ -95,8 +95,8 @@ def __init__( self, *extras, - max_line_length: int = None, - normalize_whitespace=False + max_line_length: Optional[int] = None, + normalize_whitespace: bool = False, ): """ Args: @@ -254,12 +254,12 @@ # rendered into sequences of lines (strings), to be joined by newlines. def render_document( - self, token: block_token.Document, max_line_length: int + self, token: block_token.Document, max_line_length: Optional[int] ) -> Iterable[str]: return self.blocks_to_lines(token.children, max_line_length=max_line_length) def render_heading( - self, token: block_token.Heading, max_line_length: int + self, token: block_token.Heading, max_line_length: Optional[int] ) -> Iterable[str]: # note: no word wrapping, because atx headings always fit on a single line. line = "#" * token.level @@ -271,13 +271,13 @@ return [line] def render_setext_heading( - self, token: block_token.SetextHeading, max_line_length: int + self, token: block_token.SetextHeading, max_line_length: Optional[int] ) -> Iterable[str]: yield from self.span_to_lines(token.children, max_line_length=max_line_length) yield token.underline def render_quote( - self, token: block_token.Quote, max_line_length: int + self, token: block_token.Quote, max_line_length: Optional[int] ) -> Iterable[str]: max_child_line_length = max_line_length - 2 if max_line_length else None lines = self.blocks_to_lines( @@ -286,18 +286,18 @@ return self.prefix_lines(lines or [""], "> ") def render_paragraph( - self, token: block_token.Paragraph, max_line_length: int + self, token: block_token.Paragraph, max_line_length: Optional[int] ) -> Iterable[str]: return self.span_to_lines(token.children, max_line_length=max_line_length) def render_block_code( - self, token: block_token.BlockCode, max_line_length: int + self, token: block_token.BlockCode, max_line_length: Optional[int] ) -> Iterable[str]: lines = token.content[:-1].split("\n") return self.prefix_lines(lines, " ") def render_fenced_code_block( - self, token: block_token.BlockCode, max_line_length: int + self, token: block_token.CodeFence, max_line_length: Optional[int] ) -> Iterable[str]: indentation = " " * token.indentation yield indentation + token.delimiter + token.info_string @@ -307,12 +307,12 @@ yield indentation + token.delimiter def render_list( - self, token: block_token.List, max_line_length: int + self, token: block_token.List, max_line_length: Optional[int] ) -> Iterable[str]: return self.blocks_to_lines(token.children, max_line_length=max_line_length) def render_list_item( - self, token: block_token.ListItem, max_line_length: int + self, token: block_token.ListItem, max_line_length: Optional[int] ) -> Iterable[str]: if self.normalize_whitespace: prepend = len(token.leader) + 1 @@ -333,7 +333,7 @@ ) def render_table( - self, token: block_token.Table, max_line_length: int + self, token: block_token.Table, max_line_length: Optional[int] ) -> Iterable[str]: # note: column widths are not preserved; they are automatically adjusted to fit the contents. content = [self.table_row_to_text(token.header), []] @@ -346,24 +346,24 @@ ] def render_thematic_break( - self, token: block_token.ThematicBreak, max_line_length: int + self, token: block_token.ThematicBreak, max_line_length: Optional[int] ) -> Iterable[str]: return [token.line] def render_html_block( - self, token: block_token.HtmlBlock, max_line_length: int + self, token: block_token.HtmlBlock, max_line_length: Optional[int] ) -> Iterable[str]: return token.content.split("\n") def render_link_reference_definition_block( - self, token: LinkReferenceDefinitionBlock, max_line_length: int + self, token: LinkReferenceDefinitionBlock, max_line_length: Optional[int] ) -> Iterable[str]: # each link reference definition starts on a new line for child in token.children: yield from self.span_to_lines([child], max_line_length=max_line_length) def render_blank_line( - self, token: BlankLine, max_line_length: int + self, token: BlankLine, max_line_length: Optional[int] ) -> Iterable[str]: return [""] @@ -373,7 +373,7 @@ self, leader: Fragment, tokens: Iterable[span_token.SpanToken], - trailer: Fragment = None, + trailer: Optional[Fragment] = None, ) -> Iterable[Fragment]: """ Makes fragments from `tokens` and embeds within a leader and a trailer. @@ -384,7 +384,7 @@ yield trailer or leader def blocks_to_lines( - self, tokens: Iterable[block_token.BlockToken], max_line_length: int + self, tokens: Iterable[block_token.BlockToken], max_line_length: Optional[int] ) -> Iterable[str]: """ Renders a sequence of block tokens into a sequence of lines. @@ -395,7 +395,7 @@ ) def span_to_lines( - self, tokens: Iterable[span_token.SpanToken], max_line_length: int + self, tokens: Iterable[span_token.SpanToken], max_line_length: Optional[int] ) -> Iterable[str]: """ Renders a sequence of span (inline) tokens into a sequence of lines. @@ -408,13 +408,24 @@ """ Renders a sequence of span (inline) tokens into a sequence of Fragments. """ - return chain.from_iterable( - [self.render_map[token.__class__.__name__](token) for token in tokens] - ) + token_list = list(tokens) + for i, token in enumerate(token_list): + if ( + isinstance(token, span_token.LineBreak) + and token.soft + and ( + (i + 1 < len(token_list) and isinstance(token_list[i + 1], span_token.HtmlSpan)) + or (i > 0 and isinstance(token_list[i - 1], span_token.HtmlSpan)) + ) + ): + yield Fragment("\n", hard_line_break=True) + else: + yield from self.render_map[token.__class__.__name__](token) + @classmethod def fragments_to_lines( - cls, fragments: Iterable[Fragment], max_line_length: int = None + cls, fragments: Iterable[Fragment], max_line_length: Optional[int] = None ) -> Iterable[str]: """ Renders a sequence of Fragments into lines. @@ -492,7 +503,7 @@ cls, lines: Iterable[str], first_line_prefix: str, - following_line_prefix: str = None, + following_line_prefix: Optional[str] = None, ) -> Iterable[str]: """ Prepends a prefix string to a sequence of lines. The first line may @@ -512,7 +523,19 @@ """ Renders each table cell on a table row to text. No word wrapping. """ - return [next(self.span_to_lines(col.children, max_line_length=None), "") for col in row.children] + return [ + self.escape_table_cell_text( + next(self.span_to_lines(col.children, max_line_length=None), "") + ) + for col in row.children + ] + + @staticmethod + def escape_table_cell_text(text: str) -> str: + """ + Escapes text that would otherwise be parsed as a table delimiter. + """ + return text.replace("|", r"\|") @classmethod def calculate_table_column_widths(cls, col_text) -> Sequence[int]: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/mistletoe/span_token.py new/mistletoe-1.6.0/mistletoe/span_token.py --- old/mistletoe-1.5.1/mistletoe/span_token.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/mistletoe/span_token.py 2026-07-11 16:09:13.000000000 +0200 @@ -50,7 +50,8 @@ Arguments: token_cls (SpanToken): token to be removed from the parsing process. """ - _token_types.remove(token_cls) + if token_cls in _token_types: + _token_types.remove(token_cls) def reset_tokens(): @@ -192,7 +193,7 @@ class AutoLink(SpanToken): """ - Autolink token. ("<http://www.google.com>") + Autolink token. ("<http://www.google.com>", or "<[email protected]>") This is an inline token with a single child of type RawText. Attributes: @@ -201,14 +202,15 @@ mailto (bool): true iff the target looks like an email address, but does not have the "mailto:" prefix. """ repr_attributes = ("target", "mailto") - pattern = re.compile(r"(?<!\\)(?:\\\\)*<([A-Za-z][A-Za-z0-9+.-]{1,31}:[^ <>]*?|[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*)>") + pattern = re.compile(r"(?<!\\)(?:\\\\)*<(([A-Za-z][A-Za-z0-9+.-]{1,31}):[^ <>]*?|[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*)>") parse_inner = False def __init__(self, match): content = match.group(self.parse_group) + scheme = match.group(2) self.children = (RawText(content),) self.target = content - self.mailto = '@' in self.target and 'mailto' not in self.target.casefold() + self.mailto = not scheme class EscapeSequence(SpanToken): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/setup.py new/mistletoe-1.6.0/setup.py --- old/mistletoe-1.5.1/setup.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/setup.py 2026-07-11 16:09:13.000000000 +0200 @@ -29,6 +29,9 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries :: Python Modules', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/specification/commonmark.py new/mistletoe-1.6.0/test/specification/commonmark.py --- old/mistletoe-1.5.1/test/specification/commonmark.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/specification/commonmark.py 2026-07-11 16:09:13.000000000 +0200 @@ -49,7 +49,15 @@ def load_tests(specfile): with open(specfile, 'r', encoding='utf-8') as fin: - return json.load(fin) + tests = json.load(fin) + + # alter the tests for our needs + for test in tests: + # CommonMark states that "Renderers may make different decisions about how to escape or normalize URLs in the output.". + # So we unescape square brackets in URLs to match mistletoe's behavior. + test['html'] = test['html'].replace('%5B', '[').replace('%5D', ']') + + return tests def locate_section(section, tests): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/test_contrib/test_github_wiki.py new/mistletoe-1.6.0/test/test_contrib/test_github_wiki.py --- old/mistletoe-1.5.1/test/test_contrib/test_github_wiki.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/test_contrib/test_github_wiki.py 2026-07-11 16:09:13.000000000 +0200 @@ -1,4 +1,6 @@ +import time from unittest import TestCase, mock +from parameterized import parameterized from mistletoe import span_token, Document, token from mistletoe.span_token import tokenize_inner from mistletoe.contrib.github_wiki import GithubWiki, GithubWikiRenderer @@ -28,3 +30,25 @@ token = next(iter(tokenize_inner('[[wiki|target]]'))) output = '<a href="target">wiki</a>' self.assertEqual(self.renderer.render(token), output) + + @parameterized.expand([ + ('[[wiki | target]]', 'wiki', 'target'), + ('[[ wiki | target ]]', 'wiki', 'target'), + ('[[a | b | c]]', 'a', 'b | c'), + ]) + def test_parse_variant_spacing_and_pipes(self, source, label, target): + token = next(iter(tokenize_inner(source))) + self.assertIsInstance(token, GithubWiki) + self.assertEqual(token.target, target) + self.assertEqual(self.renderer.render_inner(token), label) + + @parameterized.expand([ + ('missing_closing_marker', '[[' + ' ' * 200 + '|' + ' ' * 200 + ']'), + ('missing_pipe', '[[' + ' ' * 6000 + ']]'), + ]) + def test_malformed_input_is_rejected_quickly(self, _name, source): + start = time.perf_counter() + match = GithubWiki.pattern.match(source) + elapsed = time.perf_counter() - start + self.assertLess(elapsed, 1.0) + self.assertIsNone(match) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/test_contrib/test_jira_renderer.py new/mistletoe-1.6.0/test/test_contrib/test_jira_renderer.py --- old/mistletoe-1.5.1/test/test_contrib/test_jira_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/test_contrib/test_jira_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -113,6 +113,12 @@ expected = '[{body}|{url}|{title}]'.format(url=url, body=body, title=title) self.assertEqual(output, expected) + def test_render_link_with_escapes(self): + token = next(iter(tokenize_inner('[A link with \\[brackets\\]](http://wwww.example.com/some[complex]url "[A title]")'))) + output = self.renderer.render(token) + expected = '[A link with \\[brackets\\]|http://wwww.example.com/some\\[complex\\]url|\\[A title\\]]' + self.assertEqual(output, expected) + def test_render_footnote_link(self): pass @@ -211,6 +217,20 @@ """ self.markdownResultTest(markdown, expected) + + def test_table_pipe_in_cell(self): + """ Escape pipe in cells """ + markdown = """\ +| A | B | +|-----------| +| 1\\|1 | 2 | +""" + expected = """\ +||A||B|| +|1\\|1|2| + +""" + self.markdownResultTest(markdown, expected) @filesBasedTest def test_render__basic_blocks(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/test_core_tokens.py new/mistletoe-1.6.0/test/test_core_tokens.py --- old/mistletoe-1.5.1/test/test_core_tokens.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/test_core_tokens.py 2026-07-11 16:09:13.000000000 +0200 @@ -30,6 +30,7 @@ self.assertEqual(delimiter.number, 1) self.assertEqual(delimiter.start, 5) self.assertEqual(delimiter.end, 6) + self.assertEqual(delimiter.type, '*') def test_delimiter_remove_right(self): delimiter = Delimiter(4, 6, 'abcd**') @@ -37,6 +38,15 @@ self.assertEqual(delimiter.number, 1) self.assertEqual(delimiter.start, 4) self.assertEqual(delimiter.end, 5) + self.assertEqual(delimiter.type, '*') + + def test_delimiter_remove_right_from_longer_run(self): + delimiter = Delimiter(4, 8, 'abcd****') + self.assertTrue(delimiter.remove(1, left=False)) + self.assertEqual(delimiter.number, 3) + self.assertEqual(delimiter.start, 4) + self.assertEqual(delimiter.end, 7) + self.assertEqual(delimiter.type, '***') def test_delimiter_remove_empty(self): delimiter = Delimiter(4, 6, 'abcd**') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/test_html_renderer.py new/mistletoe-1.6.0/test/test_html_renderer.py --- old/mistletoe-1.5.1/test/test_html_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/test_html_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -40,8 +40,8 @@ self._test_token('Strikethrough', '<del>inner</del>') def test_image(self): - expected = '<img src="src" alt="" title="title" />' - self._test_token('Image', expected, src='src', title='title') + expected = '<img src="s&r%3Ec" alt="" title="title" />' + self._test_token('Image', expected, src='s&r>c', title='title') def test_link(self): expected = '<a href="target" title="title">inner</a>' @@ -51,6 +51,11 @@ expected = '<a href="link">inner</a>' self._test_token('AutoLink', expected, target='link', mailto=False) + def test_email_autolink_escapes_href(self): + expected = '<a href="mailto:a&[email protected]">inner</a>' + self._test_token('AutoLink', expected, target='a&[email protected]', + mailto=True) + def test_escape_sequence(self): self._test_token('EscapeSequence', 'inner') @@ -158,6 +163,12 @@ expected = '<p><div><br> as plain text</div></p>\n' self.assertEqual(renderer.render(token), expected) + def test_email_autolink_escapes_href(self): + with HtmlRenderer() as renderer: + output = renderer.render(Document('<a&[email protected]>')) + expected = '<p><a href="mailto:a&[email protected]">a&[email protected]</a></p>\n' + self.assertEqual(output, expected) + class TestHtmlRendererFootnotes(TestCase): def setUp(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/test_markdown_renderer.py new/mistletoe-1.6.0/test/test_markdown_renderer.py --- old/mistletoe-1.5.1/test/test_markdown_renderer.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/test_markdown_renderer.py 2026-07-11 16:09:13.000000000 +0200 @@ -338,6 +338,15 @@ output = self.roundtrip(input) self.assertEqual(output, "".join(input)) + def test_table_escapes_pipes_in_cells(self): + input = [ + "| Left column | Right column |\n", + "| ----------- | ----------------------------- |\n", + "| left baz | right foo \\| between pipes \\| |\n", + ] + output = self.roundtrip(input) + self.assertEqual(output, "".join(input)) + def test_table_with_varying_column_counts(self): input = [ " | header | x | \n", @@ -638,6 +647,28 @@ "> > hills of hay\n" ) + def test_wordwrap_paragraph_with_html_span_on_own_line(self): + with MarkdownRenderer() as renderer: + paragraph = block_token.Paragraph( + [ + "Some long sentence that will be reflowed by the word wrapper.\n", + "</template>\n", + "More text after the tag.\n", + ] + ) + + renderer.max_line_length = 50 + lines = renderer.render(paragraph) + + # then the closing tag must remain on its own line — not collapsed + # onto the end of the previous line, and not merged with the next line + assert lines == ( + "Some long sentence that will be reflowed by the\n" + "word wrapper.\n" + "</template>\n" + "More text after the tag.\n" + ) + def test_wordwrap_tables(self): with MarkdownRenderer(max_line_length=30) as renderer: # given a markdown table diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/test_repr.py new/mistletoe-1.6.0/test/test_repr.py --- old/mistletoe-1.5.1/test/test_repr.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/test_repr.py 2026-07-11 16:09:13.000000000 +0200 @@ -103,6 +103,12 @@ doc = Document("Foo <http://www.example.org/>\n") self._check_repr_matches(doc.children[0].children[1], "span_token.AutoLink with 1 child target='http://www.example.org/' mailto=False") + doc = Document("Foo <[email protected]>\n") + self._check_repr_matches(doc.children[0].children[1], "span_token.AutoLink with 1 child target='[email protected]' mailto=True") + + doc = Document("Foo <http://example.org/@user/1234>\n") + self._check_repr_matches(doc.children[0].children[1], "span_token.AutoLink with 1 child target='http://example.org/@user/1234' mailto=False") + def test_escapesequence(self): doc = Document("\\*\n") self._check_repr_matches(doc.children[0].children[0], "span_token.EscapeSequence with 1 child") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mistletoe-1.5.1/test/test_span_token.py new/mistletoe-1.6.0/test/test_span_token.py --- old/mistletoe-1.5.1/test/test_span_token.py 2025-12-07 17:12:47.000000000 +0100 +++ new/mistletoe-1.6.0/test/test_span_token.py 2026-07-11 16:09:13.000000000 +0200 @@ -35,6 +35,24 @@ self._test_token(next(tokens), 'bar', children=True) self._test_token(next(tokens), '***baz', children=False) + def test_overlapping_delimiter_runs_do_not_crash(self): + tokens = list(span_token.tokenize_inner('**a****a*')) + self.assertEqual(len(tokens), 3) + self.assertIsInstance(tokens[0], span_token.Strong) + self.assertIsInstance(tokens[2], span_token.Emphasis) + self.mock.assert_any_call('*') + + def test_issue_261_asterisk_sequences_do_not_crash(self): + cases = [ + '**IKXZXPTB****AJFHV *Pbjovp*VSN*', + '**IKXZXPTB****AJFHV*Pbjovp*', + 'Ch******her R**a, Bu*****ly In****nt, j**n ko***ob, ' + 'ji*yu, M**s, Ch****us, M*', + ] + for case in cases: + with self.subTest(case=case): + self.assertTrue(list(span_token.tokenize_inner(case))) + class TestEmphasis(TestBranchToken): def test_parse(self):
