Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse @username links and colon emoji codes in markdown #441

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion KerbalStuff/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from werkzeug.exceptions import HTTPException, InternalServerError, NotFound
from flask.typing import ResponseReturnValue
from jinja2 import ChainableUndefined
from pymdownx.emoji import gemoji, to_alt

from .blueprints.accounts import accounts
from .blueprints.admin import admin
Expand Down Expand Up @@ -60,7 +61,13 @@
app.secret_key = _cfg("secret-key")
app.json_encoder = CustomJSONEncoder
app.session_interface = OnlyLoggedInSessionInterface()
Markdown(app, extensions=[KerbDown(), 'fenced_code'])
Markdown(app, extensions=[KerbDown(), 'fenced_code', 'pymdownx.emoji'],
extension_configs={'pymdownx.emoji': {
# GitHub's emojis
'emoji_index': gemoji,
# Unicode output
'emoji_generator': to_alt
}})
login_manager = LoginManager(app)

prof_dir = _cfg('profile-dir')
Expand Down
44 changes: 44 additions & 0 deletions KerbalStuff/kerbdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
from urllib.parse import parse_qs, urlparse
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 xml.etree import ElementTree
from markdown.util import AtomicString

from .objects import User


class EmbedInlineProcessor(InlineProcessor):
Expand Down Expand Up @@ -63,6 +67,45 @@ def _embed_youtube(self, vid_id: str) -> ElementTree.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<username>[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[ElementTree.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) -> ElementTree.Element:
# Make a link to the user's profile
elt = ElementTree.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 = ElementTree.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]
Expand All @@ -72,4 +115,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)
1 change: 1 addition & 0 deletions requirements-backend.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ packaging
Pillow
psycopg2-binary
PyGithub
pymdown-extensions
python-daemon
redis
requests
Expand Down
3 changes: 3 additions & 0 deletions templates/markdown.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ <h2>My super cool mod</h2>
</blockquote>
<p>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.</p>
<h2>User profile links</h2>
<p>You can create a link to a user's profile like this:</p>
<pre>@username</pre>
<h2>Embedding videos and images</h2>
<p>You can easily embed a single image like so (this came from the Markdown spec):</p>
<pre>![](http://example.com/image.png)</pre>
Expand Down