You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2026/02/20 17:44:18 UTC
(allura) 03/05: [#8597] Fix perf by using markdown's own regexes and inheriting from existing InlineProcessors which process more incrementally with simple regexes.
This is an automated email from the ASF dual-hosted git repository.
brondsem pushed a commit to branch db/8597
in repository https://gitbox.apache.org/repos/asf/allura.git
commit a92176381a73fdca7d7f2101d27f974cc6a48605
Author: Dave Brondsema <da...@brondsema.net>
AuthorDate: Thu Feb 19 18:02:31 2026 -0500
[#8597] Fix perf by using markdown's own regexes and inheriting from existing InlineProcessors
which process more incrementally with simple regexes.
In the test, the slowness factor is now a constant 4x and the absolute time is fast enough
---
Allura/allura/lib/markdown_extensions.py | 163 +++++++++++++++----------------
Allura/allura/tests/test_globals.py | 8 +-
2 files changed, 82 insertions(+), 89 deletions(-)
diff --git a/Allura/allura/lib/markdown_extensions.py b/Allura/allura/lib/markdown_extensions.py
index 70949a520..39cae8ef9 100644
--- a/Allura/allura/lib/markdown_extensions.py
+++ b/Allura/allura/lib/markdown_extensions.py
@@ -31,6 +31,8 @@
import html5lib.serializer
import html5lib.filters.alphabeticalattributes
import markdown
+import markdown.inlinepatterns
+import markdown.util
import emoji
from markupsafe import Markup
@@ -44,21 +46,6 @@
MACRO_PATTERN = r'\[\[([^\]\[]+)\]\]'
-# SHORT_REF_RE copied from markdown pre 3.0
-SHORT_REF_RE = markdown.inlinepatterns.NOIMG + r'\[([^\]]+)\]'
-
-# FORGE_LINK_RE copied from markdown pre 3.0's LINK_RE
-# TODO: further simplify now that InlineProcessor is used, see ForgeLinkPattern? or not needed since possessive quantifiers are used
-NOBRACKET = r'[^\]\[]*+'
-BRK = (
- r'\[(' +
- (NOBRACKET + r'(\[')*6 +
- (NOBRACKET + r'\])*+')*6 +
- NOBRACKET + r')\]'
-)
-FORGE_LINK_RE = markdown.inlinepatterns.NOIMG + BRK + \
- r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\11\s*)?\)'''
-
def clear_markdown_registry(reg: markdown.util.Registry, keep: list[str] = []):
keep_items = {}
@@ -112,7 +99,7 @@ def extendMarkdown(self, md):
# remove all inlinepattern processors except short refs and links
clear_markdown_registry(md.inlinePatterns, keep=['link'])
- md.inlinePatterns.register(ForgeLinkPattern(SHORT_REF_RE, md, ext=self), 'short_reference', 0)
+ md.inlinePatterns.register(ForgeShortRefPattern(markdown.inlinepatterns.REFERENCE_RE, md, ext=self), 'short_reference', 0)
# remove all default block processors except for paragraph
clear_markdown_registry(md.parser.blockprocessors, keep=['paragraph'])
@@ -290,9 +277,10 @@ def extendMarkdown(self, md):
md.inlinePatterns.register(AutolinkPattern(r'(?:(?<=\s)|^)(http(?:s?)://[a-zA-Z0-9./\-\\_%?&=+#;~:!@]+)', md),
'autolink_without_brackets',
185) # was '<escape' and 'escape' is priority 180; great num runs first, so: 185
- # replace the link pattern with our extended version
- md.inlinePatterns.register(ForgeLinkPattern(FORGE_LINK_RE, md, ext=self), 'link', 160)
- md.inlinePatterns.register(ForgeLinkPattern(SHORT_REF_RE, md, ext=self), 'short_reference', 130)
+ # replace the 2 link processors with our extended versions
+ md.inlinePatterns.register(ForgeLinkPattern(markdown.inlinepatterns.LINK_RE, md, ext=self), 'link', 160) # [WikiPage](foobar) or [WikiPage](foobar "title")
+ md.inlinePatterns.register(ForgeShortRefPattern(markdown.inlinepatterns.REFERENCE_RE, md, ext=self), 'short_reference', 130) # [WikiPage] no parens
+
# macro must be processed before links
md.inlinePatterns.register(ForgeMacroPattern(MACRO_PATTERN, md, ext=self), 'macro', 165) # similar to above
@@ -352,80 +340,85 @@ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None
return result, m.start(0), m.end(0)
-class ForgeLinkPattern(markdown.inlinepatterns.InlineProcessor):
- # TODO: consider further changes to align with InlineProcessor patterns
- # but maybe isn't needed since NOBRACKET and BRK are fast now (see their comments)
- # https://github.com/Python-Markdown/markdown/commit/d18c3d0acab0e7469c3284c897afcb61f9dd1fea
- # https://github.com/brondsem/allura/pull/1 maybe a starting point
+artifact_re = re.compile(r'((.*?):)?((.*?):)?(.+)')
+
- artifact_re = re.compile(r'((.*?):)?((.*?):)?(.+)')
+class ForgeShortRefPattern(markdown.inlinepatterns.ShortReferenceInlineProcessor):
def __init__(self, *args, **kwargs):
self.ext = kwargs.pop('ext')
super().__init__(*args, **kwargs)
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
- el = etree.Element('a')
- el.text = m.group(1)
- is_link_with_brackets = False
- try:
- href = m.group(8)
- except IndexError:
- href = m.group(1)
- is_link_with_brackets = True
- if el.text == 'x' or el.text == ' ': # skip [ ] and [x] for markdown checklist
- return None, None, None
- try:
- title = m.group(12)
- except IndexError:
- title = None
-
- classes = ''
- if href:
- if href == 'TOC':
- return None, None, None
- if self.artifact_re.match(href) and c.project:
- href, classes = self._expand_alink(href, is_link_with_brackets)
- el.set('href', self.unescape(href.strip()))
- el.set('class', classes)
+ text, index, handled = self.getText(data, m.end(0))
+ if not handled:
+ return None, None, None
+ if text == 'TOC':
+ return None, None, None
+
+ if artifact_re.match(text) and c.project:
+ href, classes = _expand_alink(self.ext, text, is_link_with_brackets=True)
+ if 'notfound' in classes and not self.ext._use_wiki:
+ el = etree.Element('span')
+ el.text = '[%s]' % text
+ else:
+ el = self.makeTag(href, title='', text=text)
+ el.set('class', classes)
+ end = index
+ return el, m.start(0), end
else:
- el.set('href', '')
-
- if title:
- title = markdown.inlinepatterns.dequote(self.unescape(title))
- el.set('title', title)
-
- if 'notfound' in classes and not self.ext._use_wiki:
- text = el.text
- el = etree.Element('span')
- el.text = '[%s]' % text
- return el, m.start(0), m.end(0)
-
- def _expand_alink(self, link, is_link_with_brackets):
- '''Return (href, classes) for an artifact link'''
- classes = ''
- if is_link_with_brackets:
- classes = 'alink'
- href = link
- shortlink = M.Shortlink.lookup(link)
- if shortlink and shortlink.ref and not getattr(shortlink.ref.artifact, 'deleted', False):
- href = shortlink.url
- if getattr(shortlink.ref.artifact, 'is_closed', False):
- classes += ' strikethrough'
- self.ext.forge_link_tree_processor.alinks.append(shortlink)
- elif is_link_with_brackets:
- href = h.urlquote(link)
- classes += ' notfound'
- attach_link = link.split('/attachment/')
- if len(attach_link) == 2 and self.ext._use_wiki:
- shortlink = M.Shortlink.lookup(attach_link[0])
- if shortlink:
- attach_status = ' notfound'
- for attach in shortlink.ref.artifact.attachments:
- if attach.filename == attach_link[1]:
- attach_status = ''
- classes += attach_status
- return href, classes
+ return None, None, None
+
+
+class ForgeLinkPattern(markdown.inlinepatterns.LinkInlineProcessor):
+
+ def __init__(self, *args, **kwargs):
+ self.ext = kwargs.pop('ext')
+ self.extra_allura_classes = ''
+ super().__init__(*args, **kwargs)
+
+ def getLink(self, data, index) -> tuple[str, str | None, int, bool]:
+ href, title, index, handled = super().getLink(data, index)
+
+ if artifact_re.match(href) and c.project:
+ href, self.extra_allura_classes = _expand_alink(self.ext, href, is_link_with_brackets=False)
+ # TODO: some thread-local var instead of self.extra_allura_classes for thread safety?
+
+ return href, title, index, handled
+
+ def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
+ el, start, end = super().handleMatch(m, data)
+ if el is not None:
+ el.set('class', self.extra_allura_classes)
+ self.extra_allura_classes = '' # reset for next link
+ return el, start, end
+
+
+def _expand_alink(ext: markdown.Extension, link: str, is_link_with_brackets: bool) -> tuple[str, str]:
+ '''Return (href, classes) for an artifact link'''
+ classes = ''
+ if is_link_with_brackets:
+ classes = 'alink'
+ href = link
+ shortlink = M.Shortlink.lookup(link)
+ if shortlink and shortlink.ref and not getattr(shortlink.ref.artifact, 'deleted', False):
+ href = shortlink.url
+ if getattr(shortlink.ref.artifact, 'is_closed', False):
+ classes += ' strikethrough'
+ ext.forge_link_tree_processor.alinks.append(shortlink)
+ elif is_link_with_brackets:
+ href = h.urlquote(link)
+ classes += ' notfound'
+ attach_link = link.split('/attachment/')
+ if len(attach_link) == 2 and ext._use_wiki:
+ shortlink = M.Shortlink.lookup(attach_link[0])
+ if shortlink:
+ attach_status = ' notfound'
+ for attach in shortlink.ref.artifact.attachments:
+ if attach.filename == attach_link[1]:
+ attach_status = ''
+ classes += attach_status
+ return href, classes
class ForgeMacroPattern(markdown.inlinepatterns.InlineProcessor):
diff --git a/Allura/allura/tests/test_globals.py b/Allura/allura/tests/test_globals.py
index ffc4db2f2..5946733ed 100644
--- a/Allura/allura/tests/test_globals.py
+++ b/Allura/allura/tests/test_globals.py
@@ -606,14 +606,14 @@ def test_markdown_invalid_tagslash(self):
def test_markdown_invalid_script_in_link(self):
r = g.markdown.convert('[xss](http://"><a onmouseover=prompt(document.domain)>xss</a>)')
assert ('<div class="markdown_content"><p><a class="" '
- '''href='http://"><a%20onmouseover=prompt(document.domain)>xss</a>' '''
- 'rel="nofollow">xss</a></p></div>' == r)
+ '''href='http://"><a%20onmouseover=prompt(document.domain)>xss</a' '''
+ 'rel="nofollow">xss</a>)</p></div>' == r)
def test_markdown_invalid_script_in_link2(self):
r = g.markdown.convert('[xss](http://"><img src=x onerror=alert(document.cookie)>)')
assert ('<div class="markdown_content"><p><a class="" '
- '''href='http://"><img%20src=x%20onerror=alert(document.cookie)>' '''
- 'rel="nofollow">xss</a></p></div>' == r)
+ '''href='http://"><img%20src=x%20onerror=alert(document.cookie)' '''
+ 'rel="nofollow">xss</a>)</p></div>' == r)
def test_markdown_invalid_classes_ids(self):
r = g.markdown.convert("# Test message")