From 75052082485781e0620d85a6a0bf6e49931717d0 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Sat, 19 Feb 2022 22:11:45 +0000 Subject: [PATCH] Parse @username links in markdown --- KerbalStuff/kerbdown.py | 48 ++++++++++++++++++++++++++++++++++++++--- templates/markdown.html | 3 +++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/KerbalStuff/kerbdown.py b/KerbalStuff/kerbdown.py index 180f04ef..d1271e45 100644 --- a/KerbalStuff/kerbdown.py +++ b/KerbalStuff/kerbdown.py @@ -1,11 +1,13 @@ import urllib.parse from urllib.parse import parse_qs, urlparse -from typing import Dict, Any, Match, Tuple - +from typing import Dict, Any, Match, Tuple, Optional +from flask import url_for from markdown import Markdown from markdown.extensions import Extension from markdown.inlinepatterns import InlineProcessor -from markdown.util import etree +from markdown.util import etree, AtomicString + +from .objects import User class EmbedInlineProcessor(InlineProcessor): @@ -62,6 +64,45 @@ def _embed_youtube(self, vid_id: str) -> etree.Element: return el +class AtUsernameProcessor(InlineProcessor): + # Don't worry about re.compiling this, markdown.inlinepatterns.Pattern.__init__ does that for us + # Same as blueprints.accounts._username_re + USER_RE = r'@(?P[A-Za-z0-9_]+)' + + def __init__(self, md: Markdown, configs: Dict[str, Any]) -> None: + super().__init__(self.USER_RE, md) + self.configs = configs + + def handleMatch(self, match: Match[str], data: str) -> Tuple[Optional[etree.Element], Optional[int], Optional[int]]: # type: ignore[override] + username = match.groupdict().get('username') + # Case insensitive lookup + user = User.query.filter(User.username.ilike(username)).first() + return ((self._profileLink(user), match.start(0), match.end(0)) + # Keep original text if user not found + if user and user.public else (None, None, None)) + + @classmethod + def _profileLink(cls, user: User) -> etree.Element: + # Make a link to the user's profile + elt = etree.Element('a', href=url_for('profile.view_profile', + username=user.username), + # Summarize user's profile in tooltip + title='\n'.join((f'{user.username}\'s profile', + f'{cls._profileModCount(user)} mods', + f'Joined {user.created.strftime("%Y-%m-%d")}'))) + # Make it bold + strong = etree.SubElement(elt, 'strong') + # AtomicString prevents Markdown from entering an infinite loop by processing the subelement's text again + strong.text = AtomicString(f'@{user.username}') + return elt + + @staticmethod + def _profileModCount(user: User) -> int: + return len([m for m in user.mods + [sa.mod for sa in user.shared_authors + if sa.accepted] + if m.published]) + + class KerbDown(Extension): def __init__(self, **kwargs: str) -> None: super().__init__(**kwargs) # type: ignore[arg-type] @@ -71,4 +112,5 @@ def __init__(self, **kwargs: str) -> None: def extendMarkdown(self, md: Markdown) -> None: # BUG: the base method signature is INVALID, it's a bug in flask-markdown md.inlinePatterns.register(EmbedInlineProcessor(md, self.config), 'embed', 200) + md.inlinePatterns.register(AtUsernameProcessor(md, self.config), 'atuser', 200) md.registerExtension(self) diff --git a/templates/markdown.html b/templates/markdown.html index c6808b72..519af1cc 100644 --- a/templates/markdown.html +++ b/templates/markdown.html @@ -52,6 +52,9 @@

My super cool mod

You can use Markdown on your mod descriptions, profile page, and changelogs. The nice thing about using it in your changelogs is that it looks nice in plaintext, which is how it shows up in your user's email inboxes.

+

User profile links

+

You can create a link to a user's profile like this:

+
@username

Embedding videos and images

You can easily embed a single image like so (this came from the Markdown spec):

![](http://example.com/image.png)