diff --git a/docs/changelog.md b/docs/changelog.md index 913732761..b29385e2d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -20,6 +20,7 @@ See the [Contributing Guide](contributing.md) for details. performance for repeated inline patterns (#1619). * Officially support Python 3.15 and drop support for Python 3.10 * Walk backtick runs in `BacktickInlineProcessor` without a regex (#1620). +* Complete rework of emphasis handling to imporove nested emphasis handling better (#1632). ### Fixed diff --git a/docs/extensions/api.md b/docs/extensions/api.md index 6952bcd19..4822bbc2b 100644 --- a/docs/extensions/api.md +++ b/docs/extensions/api.md @@ -349,7 +349,7 @@ Here are some convenience functions and other examples: | Class | Kind | Description | | -------------------------------------------------------------------------------------|-----------|---------------------------------------------------------------| -| [`AsteriskProcessor`][markdown.inlinepatterns.AsteriskProcessor] | built-in | Emphasis processor for handling strong and em matches inside asterisks | +| [`DelimiterProcessor`][markdown.inlinepatterns.DelimiterProcessor] | built-in | Emphasis processor for handling strong and em matches | | [`WikiLinksInlineProcessor`][markdown.extensions.wikilinks.WikiLinksInlineProcessor] | extension | Link `[[article names]]` to wiki given in metadata | | [`FootnoteInlineProcessor`][markdown.extensions.footnotes.FootnoteInlineProcessor] | extension | Replaces footnote in text with link to footnote div at bottom | diff --git a/markdown/core.py b/markdown/core.py index 370cb7ec5..a62b31160 100644 --- a/markdown/core.py +++ b/markdown/core.py @@ -28,7 +28,7 @@ from .preprocessors import build_preprocessors from .blockprocessors import build_block_parser from .treeprocessors import build_treeprocessors -from .inlinepatterns import build_inlinepatterns +from .inlinepatterns import build_inlinepatterns, DelimiterProcessor from .postprocessors import build_postprocessors from .extensions import Extension from .serializers import to_html_string, to_xhtml_string @@ -106,6 +106,7 @@ def __init__(self, **kwargs): """ + self.last_run: float = 0.0 self.tab_length: int = kwargs.get('tab_length', 4) self.ESCAPED_CHARS: list[str] = [ @@ -118,6 +119,7 @@ def __init__(self, **kwargs): self.registeredExtensions: list[Extension] = [] self.docType = "" # TODO: Maybe delete this. It does not appear to be used anymore. self.stripTopLevelTags: bool = True + self.delimiters: DelimiterProcessor | None = None self.build_parser() @@ -270,6 +272,11 @@ def reset(self) -> Markdown: self.htmlStash.reset() self.references.clear() + if self.delimiters is not None: + self.delimiters.reset() + if self.delimiters not in self.inlinePatterns: + self.delimiters = None + for extension in self.registeredExtensions: if hasattr(extension, 'reset'): extension.reset() diff --git a/markdown/extensions/legacy_em.py b/markdown/extensions/legacy_em.py index 39efe9a73..4ce47176e 100644 --- a/markdown/extensions/legacy_em.py +++ b/markdown/extensions/legacy_em.py @@ -14,29 +14,7 @@ from __future__ import annotations from . import Extension -from ..inlinepatterns import UnderscoreProcessor, EmStrongItem, EM_STRONG2_RE, STRONG_EM2_RE -import re - -# _emphasis_ -EMPHASIS_RE = r'(_)([^_]+)\1' - -# __strong__ -STRONG_RE = r'(_{2})(.+?)\1' - -# __strong_em___ -STRONG_EM_RE = r'(_)\1(?!\1)([^_]+?)\1(?!\1)(.+?)\1{3}' - - -class LegacyUnderscoreProcessor(UnderscoreProcessor): - """Emphasis processor for handling strong and em matches inside underscores.""" - - PATTERNS = [ - EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'), - EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'), - EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'), - EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), - EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') - ] +from ..inlinepatterns import DelimiterProcessor class LegacyEmExtension(Extension): @@ -45,14 +23,17 @@ class LegacyEmExtension(Extension): def extendMarkdown(self, md): """ Register the processor. - | Class Instance | Registry | Name | Priority | - | ------------------------------------------------------------- | ---------------------------------------------------------------- | ------ | :------: | - | [`LegacyUnderscoreProcessor`][markdown.extensions.legacy_em.LegacyUnderscoreProcessor] | [`inlinepatterns`][markdown.inlinepatterns.build_inlinepatterns] | `em_strong2` | `50` | + | Class Instance | Registry | Name | Priority | + | ------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------ | :------: | + | [`DelimiterProcessor`][markdown.inlinepatterns.DelimiterProcessor] | [`inlinepatterns`][markdown.inlinepatterns.build_inlinepatterns] | `em_strong` | `60` | """ - # flake8: noqa: E501 48-50 - md.inlinePatterns.register(LegacyUnderscoreProcessor(r'_'), 'em_strong2', 50) + # flake8: noqa: E501 27-29 + if md.delimiters is not None: + md.delimiters.add('_', 'strong,em') + else: + md.inlinePatterns.register(DelimiterProcessor('_', 'strong,em', md), 'em_strong', 60) def makeExtension(**kwargs): # pragma: no cover """ Return an instance of the `LegacyEmExtension` """ diff --git a/markdown/inlinepatterns.py b/markdown/inlinepatterns.py index 0f3533b2e..c38c0ef3a 100644 --- a/markdown/inlinepatterns.py +++ b/markdown/inlinepatterns.py @@ -41,7 +41,8 @@ from __future__ import annotations from . import util -from typing import TYPE_CHECKING, Any, Collection, NamedTuple +from typing import TYPE_CHECKING, Any, Collection, NamedTuple, cast +from collections import deque import re import xml.etree.ElementTree as etree from html import entities @@ -89,9 +90,8 @@ def build_inlinepatterns(md: Markdown, **kwargs: Any) -> util.Registry[InlinePro inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100) inlinePatterns.register(HtmlInlineProcessor(HTML_RE, md), 'html', 90) inlinePatterns.register(HtmlInlineProcessor(ENTITY_RE, md), 'entity', 80) - inlinePatterns.register(SimpleTextInlineProcessor(NOT_STRONG_RE), 'not_strong', 70) - inlinePatterns.register(AsteriskProcessor(r'\*'), 'em_strong', 60) - inlinePatterns.register(UnderscoreProcessor(r'_'), 'em_strong2', 50) + inlinePatterns.register(DelimiterProcessor('*', 'strong,em', md), 'em_strong', 60) + md.delimiters.add('_', 'strong,em', smart=True) return inlinePatterns @@ -107,36 +107,6 @@ def build_inlinepatterns(md: Markdown, **kwargs: Any) -> util.Registry[InlinePro ESCAPE_RE = r'\\(.)' """ Match a backslash escaped character (`\\<` or `\\*`). """ -EMPHASIS_RE = r'(\*)([^\*]+)\1' -""" Match emphasis with an asterisk (`*emphasis*`). """ - -STRONG_RE = r'(\*{2})(.+?)\1' -""" Match strong with an asterisk (`**strong**`). """ - -SMART_STRONG_RE = r'(?)` or `[text](url "title")`). """ @@ -149,9 +119,6 @@ def build_inlinepatterns(md: Markdown, **kwargs: Any) -> util.Registry[InlinePro IMAGE_REFERENCE_RE = IMAGE_LINK_RE """ Match start of image reference (`![alt text][2]`). """ -NOT_STRONG_RE = r'((^|(?<=\s))(\*{1,3}|_{1,3})(?=\s|$))' -""" Match a stand-alone `*` or `_`. """ - AUTOLINK_RE = r'<((?:[Ff]|[Hh][Tt])[Tt][Pp][Ss]?://[^<>]*)>' """ Match an automatic link (``). """ @@ -592,151 +559,550 @@ def _unescape(m: re.Match[str]) -> str: return RE.sub(_unescape, text) -class AsteriskProcessor(InlineProcessor): - """Emphasis processor for handling strong and em matches inside asterisks.""" +PUNCT = ( + b'!-/:-@\\[-`{-\\~\xc2\xa1-\xc2\xa9\xc2\xab-\xc2\xac\xc2\xae-\xc2\xb1\xc2\xb4\xc2\xb6-\xc2\xb8\xc2\xbb\xc2\xbf\xc3' + b'\x97\xc3\xb7\xcb\x82-\xcb\x85\xcb\x92-\xcb\x9f\xcb\xa5-\xcb\xab\xcb\xad\xcb\xaf-\xcb\xbf\xcd\xb5\xcd\xbe\xce\x84' + b'-\xce\x85\xce\x87\xcf\xb6\xd2\x82\xd5\x9a-\xd5\x9f\xd6\x89-\xd6\x8a\xd6\x8d-\xd6\x8f\xd6\xbe\xd7\x80\xd7\x83\xd7' + b'\x86\xd7\xb3-\xd7\xb4\xd8\x86-\xd8\x8f\xd8\x9b\xd8\x9d-\xd8\x9f\xd9\xaa-\xd9\xad\xdb\x94\xdb\x9e\xdb\xa9\xdb\xbd' + b'-\xdb\xbe\xdc\x80-\xdc\x8d\xdf\xb6-\xdf\xb9\xdf\xbe-\xdf\xbf\xe0\xa0\xb0-\xe0\xa0\xbe\xe0\xa1\x9e\xe0\xa2\x88' + b'\xe0\xa5\xa4-\xe0\xa5\xa5\xe0\xa5\xb0\xe0\xa7\xb2-\xe0\xa7\xb3\xe0\xa7\xba-\xe0\xa7\xbb\xe0\xa7\xbd\xe0\xa9\xb6' + b'\xe0\xab\xb0-\xe0\xab\xb1\xe0\xad\xb0\xe0\xaf\xb3-\xe0\xaf\xba\xe0\xb1\xb7\xe0\xb1\xbf\xe0\xb2\x84\xe0\xb5\x8f' + b'\xe0\xb5\xb9\xe0\xb7\xb4\xe0\xb8\xbf\xe0\xb9\x8f\xe0\xb9\x9a-\xe0\xb9\x9b\xe0\xbc\x81-\xe0\xbc\x97\xe0\xbc\x9a-' + b'\xe0\xbc\x9f\xe0\xbc\xb4\xe0\xbc\xb6\xe0\xbc\xb8\xe0\xbc\xba-\xe0\xbc\xbd\xe0\xbe\x85\xe0\xbe\xbe-\xe0\xbf\x85' + b'\xe0\xbf\x87-\xe0\xbf\x8c\xe0\xbf\x8e-\xe0\xbf\x9a\xe1\x81\x8a-\xe1\x81\x8f\xe1\x82\x9e-\xe1\x82\x9f\xe1\x83\xbb' + b'\xe1\x8d\xa0-\xe1\x8d\xa8\xe1\x8e\x90-\xe1\x8e\x99\xe1\x90\x80\xe1\x99\xad-\xe1\x99\xae\xe1\x9a\x9b-\xe1\x9a\x9c' + b'\xe1\x9b\xab-\xe1\x9b\xad\xe1\x9c\xb5-\xe1\x9c\xb6\xe1\x9f\x94-\xe1\x9f\x96\xe1\x9f\x98-\xe1\x9f\x9b\xe1\xa0\x80' + b'-\xe1\xa0\x8a\xe1\xa5\x80\xe1\xa5\x84-\xe1\xa5\x85\xe1\xa7\x9e-\xe1\xa7\xbf\xe1\xa8\x9e-\xe1\xa8\x9f\xe1\xaa\xa0' + b'-\xe1\xaa\xa6\xe1\xaa\xa8-\xe1\xaa\xad\xe1\xad\x8e-\xe1\xad\x8f\xe1\xad\x9a-\xe1\xad\xaa\xe1\xad\xb4-\xe1\xad' + b'\xbf\xe1\xaf\xbc-\xe1\xaf\xbf\xe1\xb0\xbb-\xe1\xb0\xbf\xe1\xb1\xbe-\xe1\xb1\xbf\xe1\xb3\x80-\xe1\xb3\x87\xe1\xb3' + b'\x93\xe1\xbe\xbd\xe1\xbe\xbf-\xe1\xbf\x81\xe1\xbf\x8d-\xe1\xbf\x8f\xe1\xbf\x9d-\xe1\xbf\x9f\xe1\xbf\xad-\xe1\xbf' + b'\xaf\xe1\xbf\xbd-\xe1\xbf\xbe\xe2\x80\x90-\xe2\x80\xa7\xe2\x80\xb0-\xe2\x81\x9e\xe2\x81\xba-\xe2\x81\xbe\xe2\x82' + b'\x8a-\xe2\x82\x8e\xe2\x82\xa0-\xe2\x83\x80\xe2\x84\x80-\xe2\x84\x81\xe2\x84\x83-\xe2\x84\x86\xe2\x84\x88-\xe2' + b'\x84\x89\xe2\x84\x94\xe2\x84\x96-\xe2\x84\x98\xe2\x84\x9e-\xe2\x84\xa3\xe2\x84\xa5\xe2\x84\xa7\xe2\x84\xa9\xe2' + b'\x84\xae\xe2\x84\xba-\xe2\x84\xbb\xe2\x85\x80-\xe2\x85\x84\xe2\x85\x8a-\xe2\x85\x8d\xe2\x85\x8f\xe2\x86\x8a-\xe2' + b'\x86\x8b\xe2\x86\x90-\xe2\x90\xa9\xe2\x91\x80-\xe2\x91\x8a\xe2\x92\x9c-\xe2\x93\xa9\xe2\x94\x80-\xe2\x9d\xb5\xe2' + b'\x9e\x94-\xe2\xad\xb3\xe2\xad\xb6-\xe2\xae\x95\xe2\xae\x97-\xe2\xaf\xbf\xe2\xb3\xa5-\xe2\xb3\xaa\xe2\xb3\xb9-' + b'\xe2\xb3\xbc\xe2\xb3\xbe-\xe2\xb3\xbf\xe2\xb5\xb0\xe2\xb8\x80-\xe2\xb8\xae\xe2\xb8\xb0-\xe2\xb9\x9d\xe2\xba\x80-' + b'\xe2\xba\x99\xe2\xba\x9b-\xe2\xbb\xb3\xe2\xbc\x80-\xe2\xbf\x95\xe2\xbf\xb0-\xe2\xbf\xbf\xe3\x80\x81-\xe3\x80\x84' + b'\xe3\x80\x88-\xe3\x80\xa0\xe3\x80\xb0\xe3\x80\xb6-\xe3\x80\xb7\xe3\x80\xbd-\xe3\x80\xbf\xe3\x82\x9b-\xe3\x82\x9c' + b'\xe3\x82\xa0\xe3\x83\xbb\xe3\x86\x90-\xe3\x86\x91\xe3\x86\x96-\xe3\x86\x9f\xe3\x87\x80-\xe3\x87\xa5\xe3\x87\xaf' + b'\xe3\x88\x80-\xe3\x88\x9e\xe3\x88\xaa-\xe3\x89\x87\xe3\x89\x90\xe3\x89\xa0-\xe3\x89\xbf\xe3\x8a\x8a-\xe3\x8a\xb0' + b'\xe3\x8b\x80-\xe3\x8f\xbf\xe4\xb7\x80-\xe4\xb7\xbf\xea\x92\x90-\xea\x93\x86\xea\x93\xbe-\xea\x93\xbf\xea\x98\x8d' + b'-\xea\x98\x8f\xea\x99\xb3\xea\x99\xbe\xea\x9b\xb2-\xea\x9b\xb7\xea\x9c\x80-\xea\x9c\x96\xea\x9c\xa0-\xea\x9c\xa1' + b'\xea\x9e\x89-\xea\x9e\x8a\xea\xa0\xa8-\xea\xa0\xab\xea\xa0\xb6-\xea\xa0\xb9\xea\xa1\xb4-\xea\xa1\xb7\xea\xa3\x8e' + b'-\xea\xa3\x8f\xea\xa3\xb8-\xea\xa3\xba\xea\xa3\xbc\xea\xa4\xae-\xea\xa4\xaf\xea\xa5\x9f\xea\xa7\x81-\xea\xa7\x8d' + b'\xea\xa7\x9e-\xea\xa7\x9f\xea\xa9\x9c-\xea\xa9\x9f\xea\xa9\xb7-\xea\xa9\xb9\xea\xab\x9e-\xea\xab\x9f\xea\xab\xb0' + b'-\xea\xab\xb1\xea\xad\x9b\xea\xad\xaa-\xea\xad\xab\xea\xaf\xab\xef\xac\xa9\xef\xae\xb2-\xef\xaf\x82\xef\xb4\xbe-' + b'\xef\xb5\x8f\xef\xb7\x8f\xef\xb7\xbc-\xef\xb7\xbf\xef\xb8\x90-\xef\xb8\x99\xef\xb8\xb0-\xef\xb9\x92\xef\xb9\x94-' + b'\xef\xb9\xa6\xef\xb9\xa8-\xef\xb9\xab\xef\xbc\x81-\xef\xbc\x8f\xef\xbc\x9a-\xef\xbc\xa0\xef\xbc\xbb-\xef\xbd\x80' + b'\xef\xbd\x9b-\xef\xbd\xa5\xef\xbf\xa0-\xef\xbf\xa6\xef\xbf\xa8-\xef\xbf\xae\xef\xbf\xbc-\xef\xbf\xbd\xf0\x90\x84' + b'\x80-\xf0\x90\x84\x82\xf0\x90\x84\xb7-\xf0\x90\x84\xbf\xf0\x90\x85\xb9-\xf0\x90\x86\x89\xf0\x90\x86\x8c-\xf0\x90' + b'\x86\x8e\xf0\x90\x86\x90-\xf0\x90\x86\x9c\xf0\x90\x86\xa0\xf0\x90\x87\x90-\xf0\x90\x87\xbc\xf0\x90\x8e\x9f\xf0' + b'\x90\x8f\x90\xf0\x90\x95\xaf\xf0\x90\xa1\x97\xf0\x90\xa1\xb7-\xf0\x90\xa1\xb8\xf0\x90\xa4\x9f\xf0\x90\xa4\xbf' + b'\xf0\x90\xa9\x90-\xf0\x90\xa9\x98\xf0\x90\xa9\xbf\xf0\x90\xab\x88\xf0\x90\xab\xb0-\xf0\x90\xab\xb6\xf0\x90\xac' + b'\xb9-\xf0\x90\xac\xbf\xf0\x90\xae\x99-\xf0\x90\xae\x9c\xf0\x90\xb5\xae\xf0\x90\xb6\x8e-\xf0\x90\xb6\x8f\xf0\x90' + b'\xba\xad\xf0\x90\xbd\x95-\xf0\x90\xbd\x99\xf0\x90\xbe\x86-\xf0\x90\xbe\x89\xf0\x91\x81\x87-\xf0\x91\x81\x8d\xf0' + b'\x91\x82\xbb-\xf0\x91\x82\xbc\xf0\x91\x82\xbe-\xf0\x91\x83\x81\xf0\x91\x85\x80-\xf0\x91\x85\x83\xf0\x91\x85\xb4-' + b'\xf0\x91\x85\xb5\xf0\x91\x87\x85-\xf0\x91\x87\x88\xf0\x91\x87\x8d\xf0\x91\x87\x9b\xf0\x91\x87\x9d-\xf0\x91\x87' + b'\x9f\xf0\x91\x88\xb8-\xf0\x91\x88\xbd\xf0\x91\x8a\xa9\xf0\x91\x8f\x94-\xf0\x91\x8f\x95\xf0\x91\x8f\x97-\xf0\x91' + b'\x8f\x98\xf0\x91\x91\x8b-\xf0\x91\x91\x8f\xf0\x91\x91\x9a-\xf0\x91\x91\x9b\xf0\x91\x91\x9d\xf0\x91\x93\x86\xf0' + b'\x91\x97\x81-\xf0\x91\x97\x97\xf0\x91\x99\x81-\xf0\x91\x99\x83\xf0\x91\x99\xa0-\xf0\x91\x99\xac\xf0\x91\x9a\xb9' + b'\xf0\x91\x9c\xbc-\xf0\x91\x9c\xbf\xf0\x91\xa0\xbb\xf0\x91\xa5\x84-\xf0\x91\xa5\x86\xf0\x91\xa7\xa2\xf0\x91\xa8' + b'\xbf-\xf0\x91\xa9\x86\xf0\x91\xaa\x9a-\xf0\x91\xaa\x9c\xf0\x91\xaa\x9e-\xf0\x91\xaa\xa2\xf0\x91\xac\x80-\xf0\x91' + b'\xac\x89\xf0\x91\xaf\xa1\xf0\x91\xb1\x81-\xf0\x91\xb1\x85\xf0\x91\xb1\xb0-\xf0\x91\xb1\xb1\xf0\x91\xbb\xb7-\xf0' + b'\x91\xbb\xb8\xf0\x91\xbd\x83-\xf0\x91\xbd\x8f\xf0\x91\xbf\x95-\xf0\x91\xbf\xb1\xf0\x91\xbf\xbf\xf0\x92\x91\xb0-' + b'\xf0\x92\x91\xb4\xf0\x92\xbf\xb1-\xf0\x92\xbf\xb2\xf0\x96\xa9\xae-\xf0\x96\xa9\xaf\xf0\x96\xab\xb5\xf0\x96\xac' + b'\xb7-\xf0\x96\xac\xbf\xf0\x96\xad\x84-\xf0\x96\xad\x85\xf0\x96\xb5\xad-\xf0\x96\xb5\xaf\xf0\x96\xba\x97-\xf0\x96' + b'\xba\x9a\xf0\x96\xbf\xa2\xf0\x9b\xb2\x9c\xf0\x9b\xb2\x9f\xf0\x9c\xb0\x80-\xf0\x9c\xb3\xaf\xf0\x9c\xb4\x80-\xf0' + b'\x9c\xba\xb3\xf0\x9c\xbd\x90-\xf0\x9c\xbf\x83\xf0\x9d\x80\x80-\xf0\x9d\x83\xb5\xf0\x9d\x84\x80-\xf0\x9d\x84\xa6' + b'\xf0\x9d\x84\xa9-\xf0\x9d\x85\xa4\xf0\x9d\x85\xaa-\xf0\x9d\x85\xac\xf0\x9d\x86\x83-\xf0\x9d\x86\x84\xf0\x9d\x86' + b'\x8c-\xf0\x9d\x86\xa9\xf0\x9d\x86\xae-\xf0\x9d\x87\xaa\xf0\x9d\x88\x80-\xf0\x9d\x89\x81\xf0\x9d\x89\x85\xf0\x9d' + b'\x8c\x80-\xf0\x9d\x8d\x96\xf0\x9d\x9b\x81\xf0\x9d\x9b\x9b\xf0\x9d\x9b\xbb\xf0\x9d\x9c\x95\xf0\x9d\x9c\xb5\xf0' + b'\x9d\x9d\x8f\xf0\x9d\x9d\xaf\xf0\x9d\x9e\x89\xf0\x9d\x9e\xa9\xf0\x9d\x9f\x83\xf0\x9d\xa0\x80-\xf0\x9d\xa7\xbf' + b'\xf0\x9d\xa8\xb7-\xf0\x9d\xa8\xba\xf0\x9d\xa9\xad-\xf0\x9d\xa9\xb4\xf0\x9d\xa9\xb6-\xf0\x9d\xaa\x83\xf0\x9d\xaa' + b'\x85-\xf0\x9d\xaa\x8b\xf0\x9e\x85\x8f\xf0\x9e\x8b\xbf\xf0\x9e\x97\xbf\xf0\x9e\xa5\x9e-\xf0\x9e\xa5\x9f\xf0\x9e' + b'\xb2\xac\xf0\x9e\xb2\xb0\xf0\x9e\xb4\xae\xf0\x9e\xbb\xb0-\xf0\x9e\xbb\xb1\xf0\x9f\x80\x80-\xf0\x9f\x80\xab\xf0' + b'\x9f\x80\xb0-\xf0\x9f\x82\x93\xf0\x9f\x82\xa0-\xf0\x9f\x82\xae\xf0\x9f\x82\xb1-\xf0\x9f\x82\xbf\xf0\x9f\x83\x81-' + b'\xf0\x9f\x83\x8f\xf0\x9f\x83\x91-\xf0\x9f\x83\xb5\xf0\x9f\x84\x8d-\xf0\x9f\x86\xad\xf0\x9f\x87\xa6-\xf0\x9f\x88' + b'\x82\xf0\x9f\x88\x90-\xf0\x9f\x88\xbb\xf0\x9f\x89\x80-\xf0\x9f\x89\x88\xf0\x9f\x89\x90-\xf0\x9f\x89\x91\xf0\x9f' + b'\x89\xa0-\xf0\x9f\x89\xa5\xf0\x9f\x8c\x80-\xf0\x9f\x9b\x97\xf0\x9f\x9b\x9c-\xf0\x9f\x9b\xac\xf0\x9f\x9b\xb0-\xf0' + b'\x9f\x9b\xbc\xf0\x9f\x9c\x80-\xf0\x9f\x9d\xb6\xf0\x9f\x9d\xbb-\xf0\x9f\x9f\x99\xf0\x9f\x9f\xa0-\xf0\x9f\x9f\xab' + b'\xf0\x9f\x9f\xb0\xf0\x9f\xa0\x80-\xf0\x9f\xa0\x8b\xf0\x9f\xa0\x90-\xf0\x9f\xa1\x87\xf0\x9f\xa1\x90-\xf0\x9f\xa1' + b'\x99\xf0\x9f\xa1\xa0-\xf0\x9f\xa2\x87\xf0\x9f\xa2\x90-\xf0\x9f\xa2\xad\xf0\x9f\xa2\xb0-\xf0\x9f\xa2\xbb\xf0\x9f' + b'\xa3\x80-\xf0\x9f\xa3\x81\xf0\x9f\xa4\x80-\xf0\x9f\xa9\x93\xf0\x9f\xa9\xa0-\xf0\x9f\xa9\xad\xf0\x9f\xa9\xb0-\xf0' + b'\x9f\xa9\xbc\xf0\x9f\xaa\x80-\xf0\x9f\xaa\x89\xf0\x9f\xaa\x8f-\xf0\x9f\xab\x86\xf0\x9f\xab\x8e-\xf0\x9f\xab\x9c' + b'\xf0\x9f\xab\x9f-\xf0\x9f\xab\xa9\xf0\x9f\xab\xb0-\xf0\x9f\xab\xb8\xf0\x9f\xac\x80-\xf0\x9f\xae\x92\xf0\x9f\xae' + b'\x94-\xf0\x9f\xaf\xaf' +).decode('utf-8') + + +class Delimiter: + """Delimiter.""" + + def __init__(self, token: str, tags: str, smart: bool, double: bool): + """Initialize.""" + + self.stack: deque[tuple[int, int, bool, int]] = deque() + temp = tags.split(',') + self.tag_count = len(temp) + self.tags: tuple[str, str] = (temp[0], temp[1]) if self.tag_count == 2 else (temp[0], temp[0]) + self.double = len(temp) != 2 and double + self.single = len(temp) != 2 and not double + self.smart = smart + self._build_patterns(token) + + def _build_patterns(self, token: str) -> str: + """Build regular expression patterns.""" + + # Build up patterns + self.token = token + etoken = re.escape(token) + # Avoid at start and end + xstart = fr'(?:(?<=_)|(? + (? + (? + {xstart}{etoken}{{1,}}(?![\s{etoken}{PUNCT}])(?!$)| + (?:(?<=[\s{etx}{PUNCT}])|^)(? + (? + (? + {etoken}{{1,}}(?![\s{etoken}{PUNCT}])(?!$)| + (?:(?<=[\s{etx}{PUNCT}])|^)(? None: + """Reset.""" + + # Cache info + self.stack.clear() + + +class DelimiterProcessor(InlineProcessor): + """Processor for handling complex nested patterns such as strong and em matches.""" + + SPACE = re.compile(r'\s') + + def __init__( + self, + token: str, + tags: str, + md: Markdown, + smart: bool = False, + double: bool = False + ) -> None: + """Initialize.""" + + md.delimiters = self + self.regions: list[tuple[int, int, int, int, tuple[str, str], int]] = [] + self.stack: list[tuple[int, int, bool, int]] = [] + self.tokens: list[str] = [] + self.delimiters: dict[str, Delimiter] = {} + self.cache_index = 0 + self.cache_pos = 0 + self.md = md + # API for Markdown to pass `safe_mode` into instance + self.safe_mode = False + self.add(token, tags, smart, double) + + def add( + self, + token: str, + tags: str, + smart: bool = False, + double: bool = False + ) -> None: + """Add a delimiter.""" - PATTERNS = [ - EmStrongItem(re.compile(EM_STRONG_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'), - EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'), - EmStrongItem(re.compile(STRONG_EM3_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'), - EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), - EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') - ] - """ The various strong and emphasis patterns handled by this processor. """ + if token not in self.tokens: + self.tokens.append(token) + self.delimiters[token] = Delimiter(token, tags, smart, double) + self.pattern = '|'.join([re.escape(t) for t in self.tokens]) + self.compiled_re = re.compile(self.pattern, re.DOTALL | re.UNICODE) - def build_single(self, m: re.Match[str], tag: str, idx: int) -> etree.Element: - """Return single tag.""" - el1 = etree.Element(tag) - text = m.group(2) - self.parse_sub_patterns(text, el1, None, idx) - return el1 + def remove(self, token: str) -> None: + """Remove a token.""" - def build_double(self, m: re.Match[str], tags: str, idx: int) -> etree.Element: - """Return double tag.""" + try: + i = self.tokens.index(token) + del self.tokens[i] + del self.delimiters[token] + except Exception: + pass + self.pattern = '|'.join([re.escape(t) for t in self.tokens]) + self.compiled_re = re.compile(self.pattern, re.DOTALL | re.UNICODE) if self.pattern else re.compile(r'(?!)') + + def reset(self) -> None: + """Reset.""" + + # Cache info + for v in self.delimiters.values(): + v.reset() + self.regions.clear() + self.stack.clear() + self.cache_index = 0 + self.cache_pos = 0 + + def _build_element( + self, + data: str, + start: int = 0, + offset: int = 0 + ) -> tuple[etree.Element, int]: + """Element builder.""" - tag1, tag2 = tags.split(",") - el1 = etree.Element(tag1) - el2 = etree.Element(tag2) - text = m.group(2) - self.parse_sub_patterns(text, el2, None, idx) - el1.append(el2) - if len(m.groups()) == 3: - text = m.group(3) - self.parse_sub_patterns(text, el1, el2, idx) - return el1 + regions = self.regions + el: etree.Element | None = None + last: Any = None + previous: Any = None + + outer: list[etree.Element] = [] + outer_r: list[tuple[int, int, int, int, tuple[str, str], int]] = [] + + # Iterate regions creating the elements they represent + end = len(regions) + idx = 0 + for idx, i in enumerate(range(start, end), 1): + r = regions[i] + # Not contained within region + if idx and r[0] >= regions[start][3]: + idx -= 1 + break - def build_double2(self, m: re.Match[str], tags: str, idx: int) -> etree.Element: - """Return double tags (variant 2): `text text`.""" + # Get the appropriate element(s) + if r[-1] == 2: + el1 = etree.Element(r[4][0]) + else: + el1 = etree.Element(r[4][1]) - tag1, tag2 = tags.split(",") - el1 = etree.Element(tag1) - el2 = etree.Element(tag2) - text = m.group(2) - self.parse_sub_patterns(text, el1, None, idx) - text = m.group(3) - el1.append(el2) - self.parse_sub_patterns(text, el2, None, idx) - return el1 + # Populate the elements with their text + if idx > 1: + if last.text is None: + if previous[2] < r[0]: + last.text = data[previous[1]+offset:previous[2]+offset] + else: + last.text = data[previous[1]+offset:r[0]+offset] + if last is not outer[-1] and last.tail is None: + if r[0] < outer_r[-1][3]: + last.tail = data[previous[3]+offset:r[0]+offset] + else: + last.tail = data[previous[3]+offset:outer_r[-1][2]+offset] + outer[-1].tail = data[outer_r[-1][3]+offset:r[0]+offset] - def parse_sub_patterns( - self, data: str, parent: etree.Element, last: etree.Element | None, idx: int - ) -> None: - """ - Parses sub patterns. + # First element + if el is None: + el = el1 + last = el + outer.append(el) + outer_r.append(r) - `data`: text to evaluate. + # Subsequent elements + else: + # Is the current outer element no longer wrapping this one? + while len(outer_r) > 1 and r[3] > outer_r[-1][3]: + outer.pop() + outer_r.pop() + + # Non-nested + else: + outer[-1].append(el1) + + # Is this element wrapping the next? + if i + 1 < end: + if r[3] > regions[i + 1][3]: + outer.append(el1) + outer_r.append(r) - `parent`: Parent to attach text and sub elements to. + # Track the last element we parsed. + last = el1 - `last`: Last appended child to parent. Can also be None if parent has no children. + # Track the previous region. + previous = r - `idx`: Current pattern index that was used to evaluate the parent. + # Populate remaining elements with their text + while outer: + if last.text is None: + last.text = data[previous[1]+offset:previous[2]+offset] + if last.tail is None and last is not outer[-1]: + last.tail = data[previous[3]+offset:outer_r[-1][2]+offset] + last = outer.pop() + previous = outer_r.pop() + + return cast('etree.Element', el), idx + + def increment_next_position(self, start: int, count: int) -> None: """ + Increment cache position to the next location that we can initiate an insertion. - offset = 0 - pos = 0 - - length = len(data) - while pos < length: - # Find the start of potential emphasis or strong tokens - if self.compiled_re.match(data, pos): - matched = False - # See if the we can match an emphasis/strong pattern - for index, item in enumerate(self.PATTERNS): - # Only evaluate patterns that are after what was used on the parent - if index <= idx: - continue - m = item.pattern.match(data, pos) - if m: - # Append child nodes to parent - # Text nodes should be appended to the last - # child if present, and if not, it should - # be added as the parent's text node. - text = data[offset:m.start(0)] - if text: - if last is not None: - last.tail = text - else: - parent.text = text - el = self.build_element(m, item.builder, item.tags, index) - parent.append(el) - last = el - # Move our position past the matched hunk - offset = pos = m.end(0) - matched = True - if not matched: - # We matched nothing, move on to the next character - pos += 1 - else: - # Increment position as no potential emphasis start was found. - pos += 1 - - # Append any leftover text as a text node. - text = data[offset:] - if text: - if last is not None: - last.tail = text - else: - parent.text = text + Cache position should be the first match after our current replacement. + This gives us an anchor to calculate the new offset after insertion. + """ - def build_element(self, m: re.Match[str], builder: str, tags: str, index: int) -> etree.Element: - """Element builder.""" + # Determine next offset + self.cache_index += count + if self.cache_index < len(self.regions): + self.cache_pos = self.regions[self.cache_index][0] + while self.stack: + entry = self.stack.pop(0) + if start < entry[0] <= self.cache_pos: + self.cache_pos = entry[0] + break - if builder == 'double2': - return self.build_double2(m, tags, index) - elif builder == 'double': - return self.build_double(m, tags, index) + # Nothing left to process else: - return self.build_single(m, tags, index) + self.reset() + + def get_cached_result(self, pos: int, data: str) -> tuple[etree.Element, int, int]: + """Get a cached result.""" + + # Process the next region(s) in the cache + regions = self.regions + offset = pos - self.cache_pos if pos != self.cache_pos else pos - regions[self.cache_index][0] + start, end = regions[self.cache_index][0], regions[self.cache_index][3] + el, count = self._build_element(data, self.cache_index, offset) + self.increment_next_position(start, count) + return el, start + offset, end + offset + + def get_match(self, data: str, start: int) -> re.Match[str] | None: + """Get match.""" + + for d in self.delimiters.values(): + m = d.boundary.match(data, start) + if m is not None: + return m + return None - def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]: - """Parse patterns.""" - - el = None - start = None - end = None - - for index, item in enumerate(self.PATTERNS): - m1 = item.pattern.match(data, m.start(0)) - if m1: - start = m1.start(0) - end = m1.end(0) - el = self.build_element(m1, item.builder, item.tags, index) + def search(self, data: str, start: int) -> re.Match[str] | None: + """Search.""" + + for i in range(start, len(data)): + if data[i] in self.delimiters: + d = self.delimiters[data[i]] + m = d.boundary.match(data, i) + if m is not None: + if not d.stack and m.lastgroup[0] == 'e': # type: ignore[index] + continue + return m + return None + + def add_region(self, delim: Delimiter, a: int, b: int, c: int, d: int, size: int) -> None: + """Add region.""" + + self.regions.append((a, b, c, d, delim.tags, size)) + for delim in self.delimiters.values(): + while delim.stack: + p = delim.stack[-1][0] + if max(p, a) <= min(p, d - 1): + delim.stack.pop() + continue break - return el, start, end + def handleMatch( # type: ignore[override] + self, + m: re.Match[str], + data: str + ) -> tuple[etree.Element | None, int | None, int | None]: + """Parse delimiter pattern.""" + + # Do we have entries we haven't returned yet? + if self.regions: + return self.get_cached_result(m.start(0), data) + + # If token is not an opening, quit + m2 = self.get_match(data, m.start(0)) + if m2 is None or m2.lastgroup[0] == 'e': # type: ignore[index] + if m2 is not None: + m = m2 + # Advance past the full length of the delimiter found + return None, m.start(0), m.end(0) + + # Get the stack and regions + token = data[m.start(0)] + delim = self.delimiters[token] + stack = delim.stack -class UnderscoreProcessor(AsteriskProcessor): - """Emphasis processor for handling strong and em matches inside underscores.""" + start = m2.start(0) + end = m2.end(0) + length = end - start + + # Double needs at least a size of 2 + if delim.double and length < 2: + return None, m.start(0), m.end(0) + + is_ambiguous = m2.lastgroup[0] != 's' # type: ignore[index] + stack.append((start, start + length, is_ambiguous, length)) + + # Pair tokens until the stack is empty or we can no longer find tokens. + while any(d.stack for d in self.delimiters.values()): + m2 = self.search(data, end) + if m2 is None: + break + + token = data[m2.start(0)] + delim = self.delimiters[token] + stack = delim.stack + + start = m2.start(0) + end = m2.end(0) + + # Get current and last delimiter size + current = len(m2.group(0)) + + # Some delimiters may be ambiguous and look like both a start or an end + is_start = m2.lastgroup[0] != 'e' # type: ignore[index] + is_end = not is_start or m2.lastgroup[0] != 's' # type: ignore[index] + is_ambiguous = is_start and is_end + + last = stack[-1][-1] if stack else 0 + + # Find closing tokens + # Looking for: + # - `*em*` + # - `**strong**` + # - `***strong,em***` + # - `*em**` + # - `*em***` + # - `**strong***` + # + # Avoid ambiguous tokens that could be a start or an end. + # Consume starts until the end token is fully consumed. + # If we don't consume the entire end, see if next rule consumes it. + if stack and is_end and ((not is_ambiguous and current > last) or current == last or current >= 3): + is_start = False + + # Consume previous points until the delimiter is consumed + original = current + while stack and current and last <= current: + delimiter = stack.pop() + + # Build up region for pair and adjust accounting. + size = min(delimiter[-1], 1 if delim.tag_count == 2 and delimiter[-1] == 3 else delim.max_size) + self.add_region(delim, delimiter[1] - size, delimiter[1], start, start + size, size) + start += size + current -= size + new = 0 + if size < delimiter[-1] and (not delim.double or (delimiter[-1] - size) != 1): + new = delimiter[-1] - size + stack.append((delimiter[0], delimiter[1] - size, delimiter[2], new)) + + if not stack: + if any(d.stack for d in self.delimiters.values() if d is not delim): + delim.reset() + continue + is_end = False + break + + last = stack[-1][-1] + + # Should remainder be treated as a new start? + if original >= 3 and current and is_ambiguous: + delim.stack.append((m2.start(0) + (original - current), end, False, current)) + is_end = False + + # Do we still have more to consume? + else: + is_end = current and stack and last > current + + # Looking for: + # - `***em*` + # - `***strong**` + # - `**em*` + if stack and is_end and (last >= 3 or not is_ambiguous) and last > current: + delimiter = stack.pop() + + # Don't pair with an ambiguous opening + while stack and delimiter[-1] != 3 and delimiter[2]: + delimiter = stack.pop() + last = delimiter[-1] + + if delimiter[2] and delimiter[-1] != 3: + if any(d.stack for d in self.delimiters.values() if d is not delim): + delim.reset() + continue + break - PATTERNS = [ - EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'), - EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'), - EmStrongItem(re.compile(SMART_STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'), - EmStrongItem(re.compile(SMART_STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'), - EmStrongItem(re.compile(SMART_EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em') - ] - """ The various strong and emphasis patterns handled by this processor. """ + ignore = False + # Create new region if end is valid. + # If not valid, ignore the end but continue parsing. + if not ignore: + is_start = False + ds, de = delimiter[:2] + while current and (not delim.double or current != 1): + size = min(current, 1 if delim.tag_count == 2 and current == 3 else delim.max_size) + new = last - size + self.add_region(delim, ds + new, de, start, start + size, size) + start += size + current -= size + last -= size + de -= size + if not delim.double or last > 1: + stack.append((ds, de, False, last)) + + # Find opening tokens + # Looking for: + # - `*em ...*` + # - `**strong ...*` + # - `***em ...*` + if is_start and (not delim.double or current != 1): + stack.append((start, end, is_ambiguous, current)) + + # Combine the stacks and order them + for delim in self.delimiters.values(): + self.stack.extend(delim.stack) + self.stack.sort(key=lambda x: x[0]) + + # Build the HTML elements + if self.regions: + # Regions may be out of order. + self.regions.sort(key=lambda x: x[0]) + start, end = self.regions[0][0], self.regions[0][3] + el, count = self._build_element(data) + self.increment_next_position(start, count) + return el, start, end + + # We failed to pair any valid start/end delimiters, avoid the parsed range next pass. + start = m.start(0) + end = self.stack[-1][1] if self.stack else m.end(0) + self.reset() + return None, start, end class LinkInlineProcessor(InlineProcessor): diff --git a/tests/basic/strong-and-em-together.html b/tests/basic/strong-and-em-together.html deleted file mode 100644 index 7bf5163e7..000000000 --- a/tests/basic/strong-and-em-together.html +++ /dev/null @@ -1,4 +0,0 @@ -

This is strong and em.

-

So is this word.

-

This is strong and em.

-

So is this word.

\ No newline at end of file diff --git a/tests/basic/strong-and-em-together.txt b/tests/basic/strong-and-em-together.txt deleted file mode 100644 index 95ee690db..000000000 --- a/tests/basic/strong-and-em-together.txt +++ /dev/null @@ -1,7 +0,0 @@ -***This is strong and em.*** - -So is ***this*** word. - -___This is strong and em.___ - -So is ___this___ word. diff --git a/tests/misc/em_strong_complex.html b/tests/misc/em_strong_complex.html deleted file mode 100644 index 65faddfad..000000000 --- a/tests/misc/em_strong_complex.html +++ /dev/null @@ -1,14 +0,0 @@ -

test test test test

-

test test test test

-

test

-

test

-

test test_

-

test test

-

test_test test_test

-

test test test test

-

test test test test

-

*test

-

test

-

test*

-

test test

-

testtest testtest

\ No newline at end of file diff --git a/tests/misc/em_strong_complex.txt b/tests/misc/em_strong_complex.txt deleted file mode 100644 index 042597184..000000000 --- a/tests/misc/em_strong_complex.txt +++ /dev/null @@ -1,27 +0,0 @@ -___test test__ test test_ - -___test test_ test test__ - -___test___ - -__test__ - -___test_ test___ - -___test_ test__ - -_test_test test_test_ - -***test test** test test* - -***test test* test test** - -**test* - -***test*** - -**test*** - -***test* test** - -*test*test test*test* \ No newline at end of file diff --git a/tests/misc/nested-patterns.html b/tests/misc/nested-patterns.html deleted file mode 100644 index 1c7bb43c6..000000000 --- a/tests/misc/nested-patterns.html +++ /dev/null @@ -1,10 +0,0 @@ -

link -link -link -link -link -link -link

-

I am italic and bold I am just bold

-

Example bold italic on the same line bold italic.

-

Example bold italic on the same line bold italic.

\ No newline at end of file diff --git a/tests/misc/nested-patterns.txt b/tests/misc/nested-patterns.txt deleted file mode 100644 index 9032cf131..000000000 --- a/tests/misc/nested-patterns.txt +++ /dev/null @@ -1,13 +0,0 @@ -___[link](http://example.com)___ -***[link](http://example.com)*** -**[*link*](http://example.com)** -__[_link_](http://example.com)__ -__[*link*](http://example.com)__ -**[_link_](http://example.com)** -[***link***](http://example.com) - -***I am ___italic_ and__ bold* I am `just` bold** - -Example __*bold italic*__ on the same line __*bold italic*__. - -Example **_bold italic_** on the same line **_bold italic_**. diff --git a/tests/misc/underscores.html b/tests/misc/underscores.html deleted file mode 100644 index 72d51b8b5..000000000 --- a/tests/misc/underscores.html +++ /dev/null @@ -1,6 +0,0 @@ -

THIS_SHOULD_STAY_AS_IS

-

Here is some emphasis, ok?

-

Ok, at least this should work.

-

THIS__SHOULD__STAY

-

Here is some strong stuff.

-

THISSHOULDSTAY?

\ No newline at end of file diff --git a/tests/misc/underscores.txt b/tests/misc/underscores.txt deleted file mode 100644 index 3c7f4bdd9..000000000 --- a/tests/misc/underscores.txt +++ /dev/null @@ -1,11 +0,0 @@ -THIS_SHOULD_STAY_AS_IS - -Here is some _emphasis_, ok? - -Ok, at least _this_ should work. - -THIS__SHOULD__STAY - -Here is some __strong__ stuff. - -THIS___SHOULD___STAY? diff --git a/tests/test_syntax/inline/test_emphasis.py b/tests/test_syntax/inline/test_emphasis.py index 6e96ea32c..c4dbd2948 100644 --- a/tests/test_syntax/inline/test_emphasis.py +++ b/tests/test_syntax/inline/test_emphasis.py @@ -147,7 +147,7 @@ def test_complex_emphasis_smart_underscore(self): def test_complex_emphasis_smart_underscore_mid_word(self): self.assertMarkdownRenders( 'This is text __bold_italic bold___ with more text', - '

This is text __bold_italic bold___ with more text

' + '

This is text bold_italic bold_ with more text

' ) def test_nested_emphasis(self): @@ -191,3 +191,673 @@ def test_link_emphasis_inner_outer(self): '**[**text**](url)**', '

text

' ) + + def test_underscore_legacy(self): + + self.assertMarkdownRenders( + self.dedent( + """ + THIS_SHOULD_STAY_AS_IS + + Here is some _emphasis_, ok? + + Ok, at least _this_ should work. + + THIS__SHOULD__STAY + + Here is some __strong__ stuff. + + THIS___SHOULD___STAY? + """ + ), + self.dedent( + """ +

THIS_SHOULD_STAY_AS_IS

+

Here is some emphasis, ok?

+

Ok, at least this should work.

+

THIS__SHOULD__STAY

+

Here is some strong stuff.

+

THIS___SHOULD___STAY?

+ """ + ) + ) + + def test_nested_patterns(self): + + self.assertMarkdownRenders( + self.dedent( + """ + ___[link](http://example.com)___ + ***[link](http://example.com)*** + **[*link*](http://example.com)** + __[_link_](http://example.com)__ + __[*link*](http://example.com)__ + **[_link_](http://example.com)** + [***link***](http://example.com) + + ***I am ___italic_ and__ bold* I am `just` bold** + + Example __*bold italic*__ on the same line __*bold italic*__. + + Example **_bold italic_** on the same line **_bold italic_**. + """ + ), + self.dedent( + """ +

link + link + link + link + link + link + link

+

I am italic and bold I am just bold

+

Example bold italic on the same line bold italic.

+

Example bold italic on the same line bold italic.

+ """ # noqa: E501 + ) + ) + + def test_em_strong_complex(self): + + self.assertMarkdownRenders( + self.dedent( + """ + ___test test__ test test_ + + ___test test_ test test__ + + ___test___ + + __test__ + + ___test_ test___ + + ___test_ test__ + + _test_test test_test_ + + ***test test** test test* + + ***test test* test test** + + **test* + + ***test*** + + **test*** + + ***test* test** + + *test*test test*test* + """ + ), + self.dedent( + """ +

test test test test

+

test test test test

+

test

+

test

+

test test_

+

test test

+

test_test test_test

+

test test test test

+

test test test test

+

*test

+

test

+

test*

+

test test

+

testtest testtest

+ """ + ) + ) + + def test_strong_and_em_together(self): + + self.assertMarkdownRenders( + self.dedent( + """ + ***This is strong and em.*** + + So is ***this*** word. + + ___This is strong and em.___ + + So is ___this___ word. + """ + ), + self.dedent( + """ +

This is strong and em.

+

So is this word.

+

This is strong and em.

+

So is this word.

+ """ + ) + ) + + def test_advanced_nesting(self): + + self.assertMarkdownRenders( + self.dedent( + """ + **a*bc** + + *a**b**c**d**e**f* + + ***a**b*cd**e*f*** + + ***a**b*cd*e**f*** + + ***a**b*cd*e**f*g*h*** + + ***a***bc**d*e*** + + *a**b**c**d**e**f* + + *a**b***c**d***e**f* + + *a**b***c**d***e**f** + + __a _b c__ + + _a __b __c __d __e __f_ + + ___a __b _c d__ e_ f___ + + ___a __b _c d_ e__ f___ + + ___a __b _c d_ e__ f _g_ h___ + + ___a ___b c__ d_ e___ + + _a __b__ _c __d__ _e __f__ + + ___bold and italic***bold and italic**bold and italic*__ italic_ + + ***a __b** __c *d__ e* + """ + ), + self.dedent( + """ +

*abc

+

abcde**f

+

abcdef

+

abcdef

+

abcdefgh

+

abcde

+

abcde**f

+

abcde**f

+

abcd*ef

+

_a b c

+

_a __b __c __d __e _f

+

a b c d e f

+

a b c d e f

+

a b c d e f g h

+

a b c d e

+

_a b _c d _e f

+

bold and italicbold and italicbold and italic italic

+

a __b c *d e

+ """ # noqa: E501 + ) + ) + + +class TestCommonMark(TestCase): + """Test CommonMark.""" + + def test_commonmark(self): + """Test CommonMark.""" + + self.maxDiff = None + + self.assertMarkdownRenders( + self.dedent( + R""" + *foo bar* + + a * foo bar* + + a*"foo"* + + *$*alpha. + + *£*bravo. + + *€*charlie. + + + test * a * + + foo*bar* + + 5*6*78 + + _foo bar_ + + _ foo bar_ + + a_"foo"_ + + foo_bar_ + + 5_6_78 + + пристаням_стремятся_ + + aa_"bb"_cc + + foo-_(bar)_ + + _foo* + + *foo bar * + + *foo bar + * + + *(*foo) + + *(*foo*)* + + *foo*bar + + _foo bar _ + + _(_foo) + + _(_foo_)_ + + _foo_bar + + _пристаням_стремятся + + _foo_bar_baz_ + + _(bar)_. + + **foo bar** + + ** foo bar** + + a**"foo"** + + foo**bar** + + __foo bar__ + + __ foo bar__ + + __ + foo bar__ + + a__"foo"__ + + foo__bar__ + + 5__6__78 + + пристаням__стремятся__ + + __foo, __bar__, baz__ + + foo-__(bar)__ + + **foo bar ** + + **(**foo) + + *(**foo**)* + + **Gomphocarpus (*Gomphocarpus physocarpus*, syn. + *Asclepias physocarpa*)** + + **foo "*bar*" foo** + + **foo**bar + + __foo bar __ + + __(__foo) + + _(__foo__)_ + + __foo__bar + + __пристаням__стремятся + + __foo__bar__baz__ + + __(bar)__. + + *foo [bar](/url)* + + *foo + bar* + + _foo __bar__ baz_ + + _foo _bar_ baz_ + + __foo_ bar_ + + *foo *bar** + + *foo **bar** baz* + + *foo**bar**baz* + + *foo**bar* + + ***foo** bar* + + *foo **bar*** + + *foo**bar*** + + + foo***bar***baz + + foo******bar*********baz + + *foo **bar *baz* bim** bop* + + *foo [*bar*](/url)* + + ** is not an empty emphasis + + **** is not an empty strong emphasis + + **foo [bar](/url)** + + **foo + bar** + + __foo _bar_ baz__ + + __foo __bar__ baz__ + + ____foo__ bar__ + + **foo **bar**** + + **foo *bar* baz** + + **foo*bar*baz** + + ***foo* bar** + + **foo *bar*** + + **foo *bar **baz** + bim* bop** + + **foo [*bar*](/url)** + + __ is not an empty emphasis + + ____ is not an empty strong emphasis + + foo *** + + foo *\** + + foo *_* + + foo ***** + + foo **\*** + + foo **_** + + **foo* + + *foo** + + ***foo** + + ****foo* + + **foo*** + + *foo**** + + foo ___ + + foo _\__ + + foo _*_ + + foo _____ + + foo __\___ + + foo __*__ + + __foo_ + + _foo__ + + ___foo__ + + ____foo_ + + __foo___ + + _foo____ + + **foo** + + *_foo_* + + __foo__ + + _*foo*_ + + ****foo**** + + ____foo____ + + ******foo****** + + + ***foo*** + + _____foo_____ + + *foo _bar* baz_ + + *foo __bar *baz bim__ bam* + + **foo **bar baz** + + *foo *bar baz* + + *[bar*](/url) + + _foo [bar_](/url) + + * + + ** + + __ + + *a `*`* + + _a `_`_ + + **a + + __a + """ + ), + self.dedent( + """ +

foo bar

+

a * foo bar*

+

a*"foo"*

+

*$*alpha.

+

*£*bravo.

+

*€*charlie.

+ +

test * a *

+

foobar

+

5678

+

foo bar

+

_ foo bar_

+

a_"foo"_

+

foo_bar_

+

5_6_78

+

пристаням_стремятся_

+

aa_"bb"_cc

+

foo-(bar)

+

_foo*

+

*foo bar *

+

*foo bar + *

+

*(*foo)

+

(foo)

+

foobar

+

_foo bar _

+

_(_foo)

+

(foo)

+

_foo_bar

+

_пристаням_стремятся

+

foo_bar_baz

+

(bar).

+

foo bar

+

** foo bar**

+

a**"foo"**

+

foobar

+

foo bar

+

__ foo bar__

+

__ + foo bar__

+

a__"foo"__

+

foo__bar__

+

5__6__78

+

пристаням__стремятся__

+

foo, bar, baz

+

foo-(bar)

+

**foo bar **

+

**(**foo)

+

(foo)

+

Gomphocarpus (Gomphocarpus physocarpus, syn. + Asclepias physocarpa)

+

foo "bar" foo

+

foobar

+

__foo bar __

+

__(__foo)

+

(foo)

+

__foo__bar

+

__пристаням__стремятся

+

foo__bar__baz

+

(bar).

+

foo bar

+

foo + bar

+

foo bar baz

+

foo bar baz

+

foo bar

+

foo bar

+

foo bar baz

+

foobarbaz

+

foo**bar

+

foo bar

+

foo bar

+

foobar

+ +

foobarbaz

+

foobar***baz

+

foo bar baz bim bop

+

foo bar

+

** is not an empty emphasis

+

**** is not an empty strong emphasis

+

foo bar

+

foo + bar

+

foo bar baz

+

foo bar baz

+

foo bar

+

foo bar

+

foo bar baz

+

foobarbaz

+

foo bar

+

foo bar

+

foo bar baz + bim bop

+

foo bar

+

__ is not an empty emphasis

+

____ is not an empty strong emphasis

+

foo ***

+

foo *

+

foo _

+

foo *****

+

foo *

+

foo _

+

*foo

+

foo*

+

*foo

+

***foo

+

foo*

+

foo***

+

foo ___

+

foo _

+

foo *

+

foo _____

+

foo _

+

foo *

+

_foo

+

foo_

+

_foo

+

___foo

+

foo_

+

foo___

+

foo

+

foo

+

foo

+

foo

+

foo

+

foo

+

foo

+ +

foo

+

foo

+

foo _bar baz_

+

foo bar *baz bim bam

+

**foo bar baz

+

*foo bar baz

+

*bar*

+

_foo bar_

+

*

+

**

+

__

+

a *

+

a _

+

**ahttps://foo.bar/?q=**

+

__ahttps://foo.bar/?q=__

+ """ + ) + ) + + +class TestProcessorRemoval(TestCase): + + def test_remove_processor(self): + + import markdown + from markdown.inlinepatterns import DelimiterProcessor + + # Remove all delimiter processors + md = markdown.Markdown() + + self.assertTrue(md.delimiters is not None) + self.assertTrue(isinstance(md.delimiters, DelimiterProcessor)) + + md.inlinePatterns.deregister('em_strong') + + # Call reset which will cause them to remove themselves from being registered + md.reset() + + self.assertTrue(md.delimiters is None)