mdx_neboard.py
292 lines
| 7.7 KiB
| text/x-python
|
PythonLexer
/ boards / mdx_neboard.py
neko259
|
r699 | # coding=utf-8 | ||
neko259
|
r1986 | from xml import etree | ||
neko259
|
r699 | |||
neko259
|
r736 | import re | ||
neko259
|
r1458 | import random | ||
neko259
|
r736 | import bbcode | ||
neko259
|
r1066 | |||
from urllib.parse import unquote | ||||
neko259
|
r1061 | from django.core.exceptions import ObjectDoesNotExist | ||
neko259
|
r1986 | from django.urls import reverse | ||
neko259
|
r721 | |||
neko259
|
r98 | import boards | ||
neko259
|
r1832 | from boards import settings | ||
neko259
|
r1896 | from neboard.settings import ALLOWED_HOSTS | ||
neko259
|
r52 | |||
neko259
|
r721 | |||
neko259
|
r89 | __author__ = 'neko259' | ||
neko259
|
r52 | |||
neko259
|
r799 | REFLINK_PATTERN = re.compile(r'^\d+$') | ||
neko259
|
r1528 | GLOBAL_REFLINK_PATTERN = re.compile(r'(\w+)::([^:]+)::(\d+)') | ||
neko259
|
r754 | MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}') | ||
ONE_NEWLINE = '\n' | ||||
neko259
|
r1066 | REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?') | ||
neko259
|
r1068 | LINE_BREAK_HTML = '<div class="br"></div>' | ||
neko259
|
r1460 | SPOILER_SPACE = ' ' | ||
MAX_SPOILER_MULTIPLIER = 2 | ||||
neko259
|
r1832 | MAX_SPOILER_SPACE_COUNT = 20 | ||
neko259
|
r302 | |||
neko259
|
r398 | |||
neko259
|
r2048 | class TextFormatter: | ||
neko259
|
r438 | """ | ||
An interface for formatter that can be used in the text format panel | ||||
""" | ||||
neko259
|
r721 | def __init__(self): | ||
pass | ||||
neko259
|
r438 | name = '' | ||
# Left and right tags for the button preview | ||||
preview_left = '' | ||||
preview_right = '' | ||||
# Left and right characters for the textarea input | ||||
format_left = '' | ||||
format_right = '' | ||||
neko259
|
r2048 | class AutolinkPattern: | ||
neko259
|
r52 | def handleMatch(self, m): | ||
link_element = etree.Element('a') | ||||
href = m.group(2) | ||||
link_element.set('href', href) | ||||
link_element.text = href | ||||
return link_element | ||||
neko259
|
r736 | class QuotePattern(TextFormatter): | ||
neko259
|
r1068 | name = '>q' | ||
preview_left = '<span class="quote">' | ||||
neko259
|
r438 | preview_right = '</span>' | ||
neko259
|
r736 | format_left = '[quote]' | ||
format_right = '[/quote]' | ||||
neko259
|
r52 | |||
neko259
|
r736 | class SpoilerPattern(TextFormatter): | ||
name = 'spoiler' | ||||
neko259
|
r438 | preview_left = '<span class="spoiler">' | ||
preview_right = '</span>' | ||||
neko259
|
r736 | format_left = '[spoiler]' | ||
format_right = '[/spoiler]' | ||||
neko259
|
r438 | |||
neko259
|
r56 | |||
neko259
|
r736 | class CommentPattern(TextFormatter): | ||
neko259
|
r438 | name = '' | ||
preview_left = '<span class="comment">// ' | ||||
preview_right = '</span>' | ||||
neko259
|
r736 | format_left = '[comment]' | ||
format_right = '[/comment]' | ||||
neko259
|
r56 | |||
neko259
|
r755 | # TODO Use <s> tag here | ||
neko259
|
r736 | class StrikeThroughPattern(TextFormatter): | ||
neko259
|
r438 | name = 's' | ||
preview_left = '<span class="strikethrough">' | ||||
preview_right = '</span>' | ||||
neko259
|
r736 | format_left = '[s]' | ||
format_right = '[/s]' | ||||
neko259
|
r330 | |||
neko259
|
r438 | class ItalicPattern(TextFormatter): | ||
name = 'i' | ||||
preview_left = '<i>' | ||||
preview_right = '</i>' | ||||
neko259
|
r736 | format_left = '[i]' | ||
format_right = '[/i]' | ||||
neko259
|
r438 | |||
class BoldPattern(TextFormatter): | ||||
name = 'b' | ||||
preview_left = '<b>' | ||||
preview_right = '</b>' | ||||
neko259
|
r736 | format_left = '[b]' | ||
format_right = '[/b]' | ||||
neko259
|
r438 | |||
neko259
|
r440 | class CodePattern(TextFormatter): | ||
name = 'code' | ||||
preview_left = '<code>' | ||||
preview_right = '</code>' | ||||
neko259
|
r736 | format_left = '[code]' | ||
format_right = '[/code]' | ||||
neko259
|
r699 | |||
neko259
|
r1662 | class HintPattern(TextFormatter): | ||
name = 'hint' | ||||
preview_left = '<span class="hint">' | ||||
preview_right = '</span>' | ||||
format_left = '[hint]' | ||||
format_right = '[/hint]' | ||||
neko259
|
r736 | def render_reflink(tag_name, value, options, parent, context): | ||
neko259
|
r1061 | result = '>>%s' % value | ||
neko259
|
r100 | |||
neko259
|
r1528 | post = None | ||
neko259
|
r1061 | if REFLINK_PATTERN.match(value): | ||
post_id = int(value) | ||||
neko259
|
r100 | |||
neko259
|
r1061 | try: | ||
post = boards.models.Post.objects.get(id=post_id) | ||||
neko259
|
r52 | |||
neko259
|
r1061 | except ObjectDoesNotExist: | ||
pass | ||||
neko259
|
r1528 | elif GLOBAL_REFLINK_PATTERN.match(value): | ||
match = GLOBAL_REFLINK_PATTERN.search(value) | ||||
try: | ||||
global_id = boards.models.GlobalId.objects.get( | ||||
key_type=match.group(1), key=match.group(2), | ||||
local_id=match.group(3)) | ||||
post = global_id.post | ||||
except ObjectDoesNotExist: | ||||
pass | ||||
if post is not None: | ||||
result = post.get_link_view() | ||||
neko259
|
r1061 | |||
return result | ||||
neko259
|
r90 | |||
neko259
|
r52 | |||
neko259
|
r755 | def render_quote(tag_name, value, options, parent, context): | ||
neko259
|
r831 | source = '' | ||
neko259
|
r755 | if 'source' in options: | ||
source = options['source'] | ||||
neko259
|
r1398 | elif 'quote' in options: | ||
source = options['quote'] | ||||
neko259
|
r755 | |||
if source: | ||||
neko259
|
r831 | result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value) | ||
neko259
|
r755 | else: | ||
neko259
|
r1068 | # Insert a ">" at the start of every line | ||
result = '<span class="quote">>{}</span>'.format( | ||||
value.replace(LINE_BREAK_HTML, | ||||
'{}>'.format(LINE_BREAK_HTML))) | ||||
neko259
|
r755 | |||
return result | ||||
neko259
|
r1662 | def render_hint(tag_name, value, options, parent, context): | ||
if 'hint' in options: | ||||
hint = options['hint'] | ||||
result = '<span class="hint" title="{}">{}</span>'.format(hint, value) | ||||
else: | ||||
result = value | ||||
return result | ||||
neko259
|
r990 | def render_notification(tag_name, value, options, parent, content): | ||
neko259
|
r1008 | username = value.lower() | ||
neko259
|
r990 | return '<a href="{}" class="user-cast">@{}</a>'.format( | ||
neko259
|
r1008 | reverse('notifications', kwargs={'username': username}), username) | ||
neko259
|
r990 | |||
neko259
|
r1202 | def render_tag(tag_name, value, options, parent, context): | ||
tag_name = value.lower() | ||||
neko259
|
r2048 | tag = boards.models.Tag.objects.get_by_alias(tag_name) | ||
if tag: | ||||
url = tag.get_view() | ||||
else: | ||||
neko259
|
r1202 | url = tag_name | ||
return url | ||||
neko259
|
r1458 | def render_spoiler(tag_name, value, options, parent, context): | ||
neko259
|
r1832 | if settings.get_bool('Forms', 'AdditionalSpoilerSpaces'): | ||
text_len = len(value) | ||||
space_count = min(random.randint(0, text_len * MAX_SPOILER_MULTIPLIER), | ||||
MAX_SPOILER_SPACE_COUNT) | ||||
side_spaces = SPOILER_SPACE * (space_count // 2) | ||||
else: | ||||
side_spaces = '' | ||||
return '<span class="spoiler">{}{}{}</span>'.format(side_spaces, | ||||
value, side_spaces) | ||||
neko259
|
r1458 | |||
neko259
|
r438 | formatters = [ | ||
QuotePattern, | ||||
SpoilerPattern, | ||||
ItalicPattern, | ||||
BoldPattern, | ||||
CommentPattern, | ||||
StrikeThroughPattern, | ||||
neko259
|
r440 | CodePattern, | ||
neko259
|
r1662 | HintPattern, | ||
neko259
|
r438 | ] | ||
neko259
|
r1066 | |||
PREPARSE_PATTERNS = { | ||||
r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123" | ||||
r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text" | ||||
neko259
|
r1798 | r'^//\s?(.+)': r'[comment]\1[/comment]', # Comment "//text" | ||
neko259
|
r1066 | r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user" | ||
} | ||||
neko259
|
r1896 | for hostname in ALLOWED_HOSTS: | ||
if hostname != '*': | ||||
PREPARSE_PATTERNS[r'https?://{}/thread/\d+/#(\d+)/?'.format(hostname)] = r'[post]\1[/post]' | ||||
PREPARSE_PATTERNS[r'https?://{}/thread/(\d+)/?'.format(hostname)] = r'[post]\1[/post]' | ||||
neko259
|
r1066 | |||
class Parser: | ||||
def __init__(self): | ||||
# The newline hack is added because br's margin does not work in all | ||||
# browsers except firefox, when the div's does. | ||||
neko259
|
r1068 | self.parser = bbcode.Parser(newline=LINE_BREAK_HTML) | ||
neko259
|
r1066 | |||
self.parser.add_formatter('post', render_reflink, strip=True) | ||||
self.parser.add_formatter('quote', render_quote, strip=True) | ||||
neko259
|
r1662 | self.parser.add_formatter('hint', render_hint, strip=True) | ||
neko259
|
r1066 | self.parser.add_formatter('user', render_notification, strip=True) | ||
neko259
|
r1202 | self.parser.add_formatter('tag', render_tag, strip=True) | ||
neko259
|
r1458 | self.parser.add_formatter('spoiler', render_spoiler, strip=True) | ||
neko259
|
r1066 | self.parser.add_simple_formatter( | ||
neko259
|
r1798 | 'comment', '<span class="comment">// %(value)s</span>', strip=True) | ||
neko259
|
r1066 | self.parser.add_simple_formatter( | ||
's', '<span class="strikethrough">%(value)s</span>') | ||||
self.parser.add_simple_formatter('code', | ||||
'<pre><code>%(value)s</pre></code>', | ||||
neko259
|
r1585 | render_embedded=False, | ||
neko259
|
r1621 | escape_html=True, | ||
neko259
|
r1585 | replace_links=False, | ||
replace_cosmetic=False) | ||||
neko259
|
r1066 | |||
def preparse(self, text): | ||||
""" | ||||
Performs manual parsing before the bbcode parser is used. | ||||
Preparsed text is saved as raw and the text before preparsing is lost. | ||||
""" | ||||
new_text = MULTI_NEWLINES_PATTERN.sub(ONE_NEWLINE, text) | ||||
for key, value in PREPARSE_PATTERNS.items(): | ||||
new_text = re.sub(key, value, new_text, flags=re.MULTILINE) | ||||
for link in REGEX_URL.findall(text): | ||||
new_text = new_text.replace(link, unquote(link)) | ||||
return new_text | ||||
def parse(self, text): | ||||
neko259
|
r1067 | return self.parser.format(text) | ||
neko259
|
r1499 | |||
parser = Parser() | ||||
neko259
|
r2048 | |||
neko259
|
r1499 | def get_parser(): | ||
return parser | ||||