Some refactoring on mdrender
parent
3c4696962f
commit
c7ee910647
|
@ -3,6 +3,7 @@
|
|||
# Tested on Markdown 2.3.1
|
||||
#
|
||||
# Copyright (c) 2014, Esteban Castro Borsani
|
||||
# Copyright (c) 2014, Jesús Espino García
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
@ -171,7 +172,7 @@ class EmojifyPreprocessor(Preprocessor):
|
|||
|
||||
url = emojis_path + emoji + u'.png'
|
||||
|
||||
return u's)' % {'emoji': emoji, 'url': url}
|
||||
return ''.format(emoji=emoji, url=url)
|
||||
|
||||
for line in lines:
|
||||
if line.strip():
|
|
@ -28,41 +28,44 @@ import re
|
|||
import os
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.preprocessors import Preprocessor
|
||||
from markdown.inlinepatterns import Pattern
|
||||
from markdown.util import etree
|
||||
|
||||
from taiga.users.models import User
|
||||
|
||||
|
||||
class MentionsExtension(Extension):
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
md.registerExtension(self)
|
||||
md.preprocessors.add('emojify',
|
||||
MentionsPreprocessor(md),
|
||||
'_end')
|
||||
MENTION_RE = r'(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-]+)'
|
||||
mentionsPattern = MentionsPattern(MENTION_RE)
|
||||
mentionsPattern.md = md
|
||||
md.inlinePatterns.add('mentions',
|
||||
mentionsPattern,
|
||||
'_begin')
|
||||
|
||||
|
||||
class MentionsPreprocessor(Preprocessor):
|
||||
class MentionsPattern(Pattern):
|
||||
def handleMatch(self, m):
|
||||
if m.group(2).strip():
|
||||
username = m.group(2)
|
||||
|
||||
def run(self, lines):
|
||||
new_lines = []
|
||||
pattern = re.compile('(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9]+)')
|
||||
try:
|
||||
user = User.objects.get(username=username)
|
||||
except User.DoesNotExist:
|
||||
return "@{}".format(username)
|
||||
|
||||
def make_mention_link(m):
|
||||
name = m.group(1)
|
||||
url = "/#/profile/{}".format(username)
|
||||
|
||||
if not User.objects.filter(username=name):
|
||||
return "@{name}".format(name=name)
|
||||
|
||||
tpl = ('[@{name}](/#/profile/{name} "@{name}")')
|
||||
return tpl.format(name=name)
|
||||
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
line = pattern.sub(make_mention_link, line)
|
||||
|
||||
new_lines.append(line)
|
||||
|
||||
return new_lines
|
||||
link_text = "@{}".format(username)
|
||||
|
||||
a = etree.Element('a')
|
||||
a.text = link_text
|
||||
a.set('href', url)
|
||||
a.set('alt', user.get_full_name())
|
||||
a.set('title', user.get_full_name())
|
||||
a.set('class', "mention")
|
||||
return a
|
||||
return ''
|
||||
|
||||
def makeExtension(configs=None):
|
||||
return MentionsExtension(configs=configs)
|
|
@ -0,0 +1,96 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
# Tested on Markdown 2.3.1
|
||||
#
|
||||
# Copyright (c) 2014, Esteban Castro Borsani
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
|
||||
import re
|
||||
import os
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.inlinepatterns import Pattern
|
||||
from markdown.util import etree
|
||||
|
||||
from taiga.projects.references.services import get_instance_by_ref
|
||||
|
||||
|
||||
class TaigaReferencesExtension(Extension):
|
||||
def __init__(self, project, *args, **kwargs):
|
||||
self.project = project
|
||||
return super().__init__(*args, **kwargs)
|
||||
|
||||
def extendMarkdown(self, md, md_globals):
|
||||
TAIGA_REFERENCE_RE = r'(?<=^|(?<=[^a-zA-Z0-9-\[]))#(\d+)'
|
||||
referencesPattern = TaigaReferencesPattern(TAIGA_REFERENCE_RE, self.project)
|
||||
referencesPattern.md = md
|
||||
md.inlinePatterns.add('taiga-references',
|
||||
referencesPattern,
|
||||
'_begin')
|
||||
|
||||
class TaigaReferencesPattern(Pattern):
|
||||
def __init__(self, pattern, project):
|
||||
self.project = project
|
||||
super().__init__(pattern)
|
||||
|
||||
def handleMatch(self, m):
|
||||
if m.group(2).strip():
|
||||
obj_ref = m.group(2)
|
||||
|
||||
instance = get_instance_by_ref(self.project.id, obj_ref)
|
||||
if instance is None:
|
||||
return "#{}".format(obj_ref)
|
||||
|
||||
subject = instance.content_object.subject
|
||||
|
||||
if instance.content_type.model == "userstory":
|
||||
obj_section = "user-story"
|
||||
html_classes = "reference user-story"
|
||||
elif instance.content_type.model == "task":
|
||||
obj_section = "tasks"
|
||||
html_classes = "reference task"
|
||||
elif instance.content_type.model == "issue":
|
||||
obj_section = "issues"
|
||||
html_classes = "reference issue"
|
||||
else:
|
||||
return "#{}".format(obj_ref)
|
||||
|
||||
|
||||
url = "/#/project/{}/{}/{}".format(
|
||||
self.project.slug,
|
||||
obj_section,
|
||||
obj_ref
|
||||
)
|
||||
link_text = "#{}".format(obj_ref)
|
||||
|
||||
a = etree.Element('a')
|
||||
a.text = link_text
|
||||
a.set('href', url)
|
||||
a.set('alt', subject)
|
||||
a.set('title', subject)
|
||||
a.set('class', html_classes)
|
||||
return a
|
||||
return ''
|
||||
|
||||
|
||||
def makeExtension(configs=None):
|
||||
return TaigaReferencesExtension(configs=configs)
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
# for details. All rights reserved. Use of this source code is governed by a
|
||||
# BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
from . import autolink
|
||||
from . import automail
|
||||
from . import hidden_hilite
|
||||
from . import semi_sane_lists
|
||||
from . import spaced_link
|
||||
from . import strikethrough
|
||||
from . import wikilinks
|
||||
from . import emojify
|
||||
from . import mentions
|
||||
|
||||
AutolinkExtension = autolink.AutolinkExtension
|
||||
AutomailExtension = automail.AutomailExtension
|
||||
HiddenHiliteExtension = hidden_hilite.HiddenHiliteExtension
|
||||
SemiSaneListExtension = semi_sane_lists.SemiSaneListExtension
|
||||
SpacedLinkExtension = spaced_link.SpacedLinkExtension
|
||||
StrikethroughExtension = strikethrough.StrikethroughExtension
|
||||
WikiLinkExtension = wikilinks.WikiLinkExtension
|
||||
EmojifyExtension = emojify.EmojifyExtension
|
||||
MentionsExtension = mentions.MentionsExtension
|
|
@ -1,74 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2012, lepture.com
|
||||
# Copyright (c) 2014, taiga.io
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the name of the author nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import re
|
||||
|
||||
from taiga.projects.userstories.models import UserStory
|
||||
from taiga.projects.issues.models import Issue
|
||||
from taiga.projects.tasks.models import Task
|
||||
|
||||
|
||||
def references(project, text):
|
||||
pattern = re.compile('(?<=^|(?<=[^a-zA-Z0-9-]))#(us|issue|task)(\d+)')
|
||||
|
||||
def make_reference_link(m):
|
||||
obj_type = m.group(1)
|
||||
obj_ref = m.group(2)
|
||||
|
||||
if obj_type == "us":
|
||||
model = UserStory
|
||||
obj_section = "user-story"
|
||||
elif obj_type == "issue":
|
||||
model = Issue
|
||||
obj_section = "issues"
|
||||
elif obj_type == "task":
|
||||
model = Task
|
||||
obj_section = "tasks"
|
||||
|
||||
instances = model.objects.filter(project_id=project.id, ref=obj_ref)
|
||||
if not instances:
|
||||
return "#{type}{ref}".format(type=obj_type, ref=obj_ref)
|
||||
|
||||
subject = instances[0].subject
|
||||
|
||||
return '[#{type}{ref}](/#/project/{project_slug}/{section}/{ref} "{subject}")'.format(
|
||||
type=obj_type,
|
||||
section=obj_section,
|
||||
ref=obj_ref,
|
||||
project_slug=project.slug,
|
||||
subject=subject
|
||||
)
|
||||
|
||||
text = pattern.sub(make_reference_link, text)
|
||||
return text
|
||||
|
||||
__all__ = ['references']
|
|
@ -7,20 +7,19 @@ from django.utils.encoding import force_bytes
|
|||
from markdown import markdown
|
||||
from fn import F
|
||||
|
||||
from .gfm import AutolinkExtension
|
||||
from .gfm import AutomailExtension
|
||||
from .gfm import HiddenHiliteExtension
|
||||
from .gfm import SemiSaneListExtension
|
||||
from .gfm import SpacedLinkExtension
|
||||
from .gfm import StrikethroughExtension
|
||||
from .gfm import WikiLinkExtension
|
||||
from .gfm import EmojifyExtension
|
||||
from .gfm import MentionsExtension
|
||||
|
||||
from .processors.references import references
|
||||
from .extensions.autolink import AutolinkExtension
|
||||
from .extensions.automail import AutomailExtension
|
||||
from .extensions.hidden_hilite import HiddenHiliteExtension
|
||||
from .extensions.semi_sane_lists import SemiSaneListExtension
|
||||
from .extensions.spaced_link import SpacedLinkExtension
|
||||
from .extensions.strikethrough import StrikethroughExtension
|
||||
from .extensions.wikilinks import WikiLinkExtension
|
||||
from .extensions.emojify import EmojifyExtension
|
||||
from .extensions.mentions import MentionsExtension
|
||||
from .extensions.references import TaigaReferencesExtension
|
||||
|
||||
|
||||
def _make_extensions_list(wikilinks_config=None):
|
||||
def _make_extensions_list(wikilinks_config=None, project=None):
|
||||
return [AutolinkExtension(),
|
||||
AutomailExtension(),
|
||||
SemiSaneListExtension(),
|
||||
|
@ -29,6 +28,7 @@ def _make_extensions_list(wikilinks_config=None):
|
|||
WikiLinkExtension(wikilinks_config),
|
||||
EmojifyExtension(),
|
||||
MentionsExtension(),
|
||||
TaigaReferencesExtension(project),
|
||||
"extra",
|
||||
"codehilite"]
|
||||
|
||||
|
@ -54,22 +54,13 @@ def cache_by_sha(func):
|
|||
return _decorator
|
||||
|
||||
|
||||
def _render_markdown(project, text):
|
||||
wikilinks_config = {"base_url": "#/project/{}/wiki/".format(project.slug),
|
||||
"end_url": ""}
|
||||
extensions = _make_extensions_list(wikilinks_config=wikilinks_config)
|
||||
return markdown(text, extensions=extensions)
|
||||
|
||||
|
||||
def _preprocessors(project, text):
|
||||
pre = F() >> F(references, project)
|
||||
return pre(text)
|
||||
|
||||
|
||||
#@cache_by_sha
|
||||
def render(project, text):
|
||||
renderer = F() >> F(_preprocessors, project) >> F(_render_markdown, project)
|
||||
return renderer(text)
|
||||
wikilinks_config = {"base_url": "#/project/{}/wiki/".format(project.slug),
|
||||
"end_url": ""}
|
||||
extensions = _make_extensions_list(wikilinks_config=wikilinks_config, project=project)
|
||||
return markdown(text, extensions=extensions)
|
||||
|
||||
|
||||
class DiffMatchPatch(diff_match_patch.diff_match_patch):
|
||||
def diff_pretty_html(self, diffs):
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
from .models import Reference
|
||||
|
||||
def get_instance_by_ref(project_id, obj_ref):
|
||||
try:
|
||||
instance = Reference.objects.get(project_id=project_id, ref=obj_ref)
|
||||
except Reference.DoesNotExist:
|
||||
instance = None
|
||||
|
||||
return instance
|
|
@ -1,17 +1,17 @@
|
|||
from unittest import mock
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import taiga.base
|
||||
from taiga.mdrender.gfm import mentions
|
||||
from taiga.mdrender.gfm import emojify
|
||||
from taiga.mdrender.processors import references
|
||||
from taiga.mdrender.extensions import mentions
|
||||
from taiga.mdrender.extensions import emojify
|
||||
from taiga.mdrender.service import render
|
||||
|
||||
class DummyClass:
|
||||
pass
|
||||
from taiga.projects.references import services
|
||||
|
||||
dummy_project = DummyClass()
|
||||
from taiga.users.models import User
|
||||
|
||||
dummy_project = MagicMock()
|
||||
dummy_project.id = 1
|
||||
dummy_project.slug = "test"
|
||||
|
||||
|
@ -24,119 +24,52 @@ def test_proccessor_invalid_emoji():
|
|||
assert result == ["**:notvalidemoji:**"]
|
||||
|
||||
def test_proccessor_valid_user_mention():
|
||||
DummyModel = DummyClass()
|
||||
DummyModel.objects = DummyClass()
|
||||
DummyModel.objects.filter = lambda username: ["test"]
|
||||
|
||||
mentions.User = DummyModel
|
||||
|
||||
result = mentions.MentionsPreprocessor().run(["**@user1**"])
|
||||
assert result == ["**[@user1](/#/profile/user1 \"@user1\")**"]
|
||||
with patch("taiga.mdrender.extensions.mentions.User") as mock:
|
||||
instance = mock.objects.get.return_value
|
||||
instance.get_full_name.return_value = "test name"
|
||||
result = render(dummy_project, "**@user1**")
|
||||
expected_result = "<p><strong><a alt=\"test name\" class=\"mention\" href=\"/#/profile/user1\" title=\"test name\">@user1</a></strong></p>"
|
||||
assert result == expected_result
|
||||
|
||||
def test_proccessor_invalid_user_mention():
|
||||
DummyModel = DummyClass()
|
||||
DummyModel.objects = DummyClass()
|
||||
DummyModel.objects.filter = lambda username: []
|
||||
|
||||
mentions.User = DummyModel
|
||||
|
||||
result = mentions.MentionsPreprocessor().run(["**@notvaliduser**"])
|
||||
assert result == ['**@notvaliduser**']
|
||||
|
||||
with patch("taiga.mdrender.extensions.mentions.User") as mock:
|
||||
mock.DoesNotExist = User.DoesNotExist
|
||||
mock.objects.get.side_effect = User.DoesNotExist
|
||||
result = render(dummy_project, "**@notvaliduser**")
|
||||
assert result == '<p><strong>@notvaliduser</strong></p>'
|
||||
|
||||
def test_proccessor_valid_us_reference():
|
||||
class MockModelWithInstance:
|
||||
class objects:
|
||||
def filter(*args, **kwargs):
|
||||
dummy_instance = DummyClass()
|
||||
dummy_instance.subject = "test-subject"
|
||||
return [dummy_instance]
|
||||
UserStoryBack = references.UserStory
|
||||
references.UserStory = MockModelWithInstance
|
||||
|
||||
result = references.references(dummy_project, "**#us1**")
|
||||
assert result == '**[#us1](/#/project/test/user-story/1 "test-subject")**'
|
||||
|
||||
references.UserStory = UserStoryBack
|
||||
|
||||
|
||||
def test_proccessor_invalid_us_reference():
|
||||
class MockModelEmpty:
|
||||
class objects:
|
||||
def filter(*args, **kwargs):
|
||||
return []
|
||||
|
||||
UserStoryBack = references.UserStory
|
||||
references.UserStory = MockModelEmpty
|
||||
|
||||
result = references.references(dummy_project, "**#us1**")
|
||||
assert result == "**#us1**"
|
||||
|
||||
references.UserStory = UserStoryBack
|
||||
with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
|
||||
instance = mock.return_value
|
||||
instance.content_type.model = "userstory"
|
||||
instance.content_object.subject = "test"
|
||||
result = render(dummy_project, "**#1**")
|
||||
expected_result = '<p><strong><a alt="test" class="reference user-story" href="/#/project/test/user-story/1" title="test">#1</a></strong></p>'
|
||||
assert result == expected_result
|
||||
|
||||
def test_proccessor_valid_issue_reference():
|
||||
class MockModelWithInstance:
|
||||
class objects:
|
||||
def filter(*args, **kwargs):
|
||||
dummy_instance = DummyClass()
|
||||
dummy_instance.subject = "test-subject"
|
||||
return [dummy_instance]
|
||||
IssueBack = references.Issue
|
||||
references.Issue = MockModelWithInstance
|
||||
|
||||
result = references.references(dummy_project, "**#issue1**")
|
||||
assert result == '**[#issue1](/#/project/test/issues/1 "test-subject")**'
|
||||
|
||||
references.Issue = IssueBack
|
||||
|
||||
|
||||
def test_proccessor_invalid_issue_reference():
|
||||
class MockModelEmpty:
|
||||
class objects:
|
||||
def filter(*args, **kwargs):
|
||||
return []
|
||||
|
||||
IssueBack = references.Issue
|
||||
references.Issue = MockModelEmpty
|
||||
|
||||
result = references.references(dummy_project, "**#issue1**")
|
||||
assert result == "**#issue1**"
|
||||
|
||||
references.Issue = IssueBack
|
||||
with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
|
||||
instance = mock.return_value
|
||||
instance.content_type.model = "issue"
|
||||
instance.content_object.subject = "test"
|
||||
result = render(dummy_project, "**#1**")
|
||||
expected_result = '<p><strong><a alt="test" class="reference issue" href="/#/project/test/issues/1" title="test">#1</a></strong></p>'
|
||||
assert result == expected_result
|
||||
|
||||
def test_proccessor_valid_task_reference():
|
||||
class MockModelWithInstance:
|
||||
class objects:
|
||||
def filter(*args, **kwargs):
|
||||
dummy_instance = DummyClass()
|
||||
dummy_instance.subject = "test-subject"
|
||||
return [dummy_instance]
|
||||
TaskBack = references.Task
|
||||
references.Task = MockModelWithInstance
|
||||
with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
|
||||
instance = mock.return_value
|
||||
instance.content_type.model = "task"
|
||||
instance.content_object.subject = "test"
|
||||
result = render(dummy_project, "**#1**")
|
||||
expected_result = '<p><strong><a alt="test" class="reference task" href="/#/project/test/tasks/1" title="test">#1</a></strong></p>'
|
||||
assert result == expected_result
|
||||
|
||||
result = references.references(dummy_project, "**#task1**")
|
||||
assert result == '**[#task1](/#/project/test/tasks/1 "test-subject")**'
|
||||
|
||||
references.Task = TaskBack
|
||||
|
||||
|
||||
def test_proccessor_invalid_task_reference():
|
||||
class MockModelEmpty:
|
||||
class objects:
|
||||
def filter(*args, **kwargs):
|
||||
return []
|
||||
|
||||
TaskBack = references.Task
|
||||
references.Task = MockModelEmpty
|
||||
|
||||
result = references.references(dummy_project, "**#task1**")
|
||||
assert result == "**#task1**"
|
||||
|
||||
references.Task = TaskBack
|
||||
|
||||
def test_proccessor_invalid_type_reference():
|
||||
result = references.references(dummy_project, "**#invalid1**")
|
||||
assert result == "**#invalid1**"
|
||||
def test_proccessor_invalid_reference():
|
||||
with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
|
||||
mock.return_value = None
|
||||
result = render(dummy_project, "**#1**")
|
||||
assert result == "<p><strong>#1</strong></p>"
|
||||
|
||||
def test_render_wiki_strong():
|
||||
assert render(dummy_project, "**test**") == "<p><strong>test</strong></p>"
|
||||
|
|
Loading…
Reference in New Issue