Browse Source

added mentions feature to message parser

Rafał Pitoń 8 years ago
parent
commit
dadd8b0814
3 changed files with 151 additions and 4 deletions
  1. 65 0
      misago/markup/mentions.py
  2. 7 4
      misago/markup/parser.py
  3. 79 0
      misago/markup/tests/test_mentions.py

+ 65 - 0
misago/markup/mentions.py

@@ -0,0 +1,65 @@
+import re
+
+from bs4 import BeautifulSoup, NavigableString
+from django.contrib.auth import get_user_model
+from django.utils import six
+
+
+SUPPORTED_TAGS = ('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p')
+USERNAME_RE = re.compile(r'@[0-9a-z]+', re.IGNORECASE)
+MENTIONS_LIMIT = 24
+
+
+def add_mentions(request, result):
+    if '@' not in result['parsed_text']:
+        return
+
+    mentions_dict = {}
+
+    soup = BeautifulSoup(result['parsed_text'], 'html5lib')
+
+    for tagname in SUPPORTED_TAGS:
+        if tagname in result['parsed_text']:
+            for element in soup.find_all(tagname):
+                add_mentions_to_element(request, element, mentions_dict)
+
+    result['parsed_text'] = six.text_type(soup.body)[6:-7].strip()
+    result['mentions'] = filter(bool, mentions_dict.values())
+
+
+def add_mentions_to_element(request, element, mentions_dict):
+    for item in element.contents:
+        if item.name:
+            if item.name != 'a':
+                add_mentions_to_element(request, item, mentions_dict)
+        elif '@' in item.string:
+            parse_string(request, item, mentions_dict)
+
+
+def parse_string(request, element, mentions_dict):
+    def replace_mentions(matchobj):
+        if len(mentions_dict) >= MENTIONS_LIMIT:
+            return matchobj.group(0)
+
+        username = matchobj.group(0)[1:].strip().lower()
+
+        if username not in mentions_dict:
+            if username == request.user.slug:
+                mentions_dict[username] = request.user
+            else:
+                User = get_user_model()
+
+                try:
+                    mentions_dict[username] = User.objects.get(slug=username)
+                except User.DoesNotExist:
+                    mentions_dict[username] = None
+
+        if mentions_dict[username]:
+            user = mentions_dict[username]
+            return u'<a href="{}">@{}</a>'.format(user.get_absolute_url(), user.username)
+        else:
+            # we've failed to resolve user for username
+            return matchobj.group(0)
+
+    replaced_string = USERNAME_RE.sub(replace_mentions, element.string)
+    element.replace_with(BeautifulSoup(replaced_string, 'html.parser'))

+ 7 - 4
misago/markup/parser.py

@@ -6,6 +6,7 @@ from htmlmin.minify import html_minify
 
 
 from .bbcode import blocks, inline
 from .bbcode import blocks, inline
 from .md.shortimgs import ShortImagesExtension
 from .md.shortimgs import ShortImagesExtension
+from .mentions import add_mentions
 from .pipeline import pipeline
 from .pipeline import pipeline
 
 
 
 
@@ -51,8 +52,11 @@ def parse(text, request, poster, allow_mentions=True, allow_links=True,
 
 
     parsing_result = pipeline.process_result(parsing_result)
     parsing_result = pipeline.process_result(parsing_result)
 
 
+    if allow_mentions:
+        add_mentions(request, parsing_result)
+
     if allow_links or allow_images:
     if allow_links or allow_images:
-        clean_links(parsing_result, request)
+        clean_links(request, parsing_result)
 
 
     if minify:
     if minify:
         minify_result(parsing_result)
         minify_result(parsing_result)
@@ -111,11 +115,10 @@ def md_factory(allow_links=True, allow_images=True, allow_blocks=True):
 
 
 
 
 def linkify_paragraphs(result):
 def linkify_paragraphs(result):
-    result['parsed_text'] = bleach.linkify(
-        result['parsed_text'], skip_pre=True, parse_email=True)
+    result['parsed_text'] = bleach.linkify(result['parsed_text'], skip_pre=True, parse_email=True)
 
 
 
 
-def clean_links(result, request):
+def clean_links(request, result):
     site_address = '%s://%s' % (request.scheme, request.get_host())
     site_address = '%s://%s' % (request.scheme, request.get_host())
 
 
     soup = BeautifulSoup(result['parsed_text'], 'html5lib')
     soup = BeautifulSoup(result['parsed_text'], 'html5lib')

+ 79 - 0
misago/markup/tests/test_mentions.py

@@ -0,0 +1,79 @@
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from ..mentions import add_mentions
+
+
+class MockRequest(object):
+    def __init__(self, user):
+        self.user = user
+
+
+class MentionsTests(AuthenticatedUserTestCase):
+    def test_single_mention(self):
+        """markup extension parses single mention"""
+        TEST_CASES = (
+            (
+                '<p>Hello, @{}!</p>',
+                '<p>Hello, <a href="{}">@{}</a>!</p>'
+            ),
+            (
+                '<h1>Hello, @{}!</h1>',
+                '<h1>Hello, <a href="{}">@{}</a>!</h1>'
+            ),
+            (
+                '<h1>Hello, <strong>@{}!</strong></h1>',
+                '<h1>Hello, <strong><a href="{}">@{}</a>!</strong></h1>'
+            ),
+            (
+                '<h1>Hello, <strong>@{}</strong>!</h1>',
+                '<h1>Hello, <strong><a href="{}">@{}</a></strong>!</h1>'
+            ),
+        )
+
+        for before, after in TEST_CASES:
+            result = {
+                'parsed_text': before.format(self.user.username),
+                'mentions': []
+            }
+
+            add_mentions(MockRequest(self.user), result)
+
+            outcome = after.format(self.user.get_absolute_url(), self.user.username)
+            self.assertEqual(result['parsed_text'], outcome)
+            self.assertEqual(result['mentions'], [self.user])
+
+    def test_invalid_mentions(self):
+        """markup extension leaves invalid mentions alone"""
+        TEST_CASES = (
+            '<p>Hello, Bob!</p>',
+            '<p>Hello, @Bob!</p>',
+            '<p>Hello, <a href="/">@{}</a>!</p>'.format(self.user.username),
+            '<p>Hello, <a href="/"><b>@{}</b></a>!</p>'.format(self.user.username),
+        )
+
+        for markup in TEST_CASES:
+            result = {
+                'parsed_text': markup,
+                'mentions': []
+            }
+
+            add_mentions(MockRequest(self.user), result)
+
+            self.assertEqual(result['parsed_text'], markup)
+            self.assertFalse(result['mentions'])
+
+    def test_multiple_mentions(self):
+        """markup extension handles multiple mentions"""
+        before = '<p>Hello, @{0}, how you feel now, @{0}?</p>'.format(self.user.username)
+
+        formats = (self.user.get_absolute_url(), self.user.username)
+        after = '<p>Hello, <a href="{0}">@{1}</a>, how you feel now, <a href="{0}">@{1}</a>?</p>'.format(*formats)
+
+        result = {
+            'parsed_text': before,
+            'mentions': []
+        }
+
+        add_mentions(MockRequest(self.user), result)
+        self.assertEqual(result['parsed_text'], after)
+        self.assertEqual(result['mentions'], [self.user])