|
|
# coding=utf-8
|
|
|
from xml import etree
|
|
|
|
|
|
import re
|
|
|
import random
|
|
|
import bbcode
|
|
|
|
|
|
from urllib.parse import unquote
|
|
|
|
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
|
from django.urls import reverse
|
|
|
|
|
|
import boards
|
|
|
from boards import settings
|
|
|
from neboard.settings import ALLOWED_HOSTS
|
|
|
|
|
|
|
|
|
__author__ = 'neko259'
|
|
|
|
|
|
|
|
|
REFLINK_PATTERN = re.compile(r'^\d+$')
|
|
|
GLOBAL_REFLINK_PATTERN = re.compile(r'(\w+)::([^:]+)::(\d+)')
|
|
|
MULTI_NEWLINES_PATTERN = re.compile(r'(\r?\n){2,}')
|
|
|
ONE_NEWLINE = '\n'
|
|
|
REGEX_URL = re.compile(r'https?\://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(/\S*)?')
|
|
|
LINE_BREAK_HTML = '<div class="br"></div>'
|
|
|
SPOILER_SPACE = ' '
|
|
|
|
|
|
MAX_SPOILER_MULTIPLIER = 2
|
|
|
MAX_SPOILER_SPACE_COUNT = 20
|
|
|
|
|
|
|
|
|
class TextFormatter:
|
|
|
"""
|
|
|
An interface for formatter that can be used in the text format panel
|
|
|
"""
|
|
|
|
|
|
def __init__(self):
|
|
|
pass
|
|
|
|
|
|
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 = ''
|
|
|
|
|
|
|
|
|
class AutolinkPattern:
|
|
|
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
|
|
|
|
|
|
|
|
|
class QuotePattern(TextFormatter):
|
|
|
name = '>q'
|
|
|
preview_left = '<span class="quote">'
|
|
|
preview_right = '</span>'
|
|
|
|
|
|
format_left = '[quote]'
|
|
|
format_right = '[/quote]'
|
|
|
|
|
|
|
|
|
class SpoilerPattern(TextFormatter):
|
|
|
name = 'spoiler'
|
|
|
preview_left = '<span class="spoiler">'
|
|
|
preview_right = '</span>'
|
|
|
|
|
|
format_left = '[spoiler]'
|
|
|
format_right = '[/spoiler]'
|
|
|
|
|
|
|
|
|
class CommentPattern(TextFormatter):
|
|
|
name = ''
|
|
|
preview_left = '<span class="comment">// '
|
|
|
preview_right = '</span>'
|
|
|
|
|
|
format_left = '[comment]'
|
|
|
format_right = '[/comment]'
|
|
|
|
|
|
|
|
|
# TODO Use <s> tag here
|
|
|
class StrikeThroughPattern(TextFormatter):
|
|
|
name = 's'
|
|
|
preview_left = '<span class="strikethrough">'
|
|
|
preview_right = '</span>'
|
|
|
|
|
|
format_left = '[s]'
|
|
|
format_right = '[/s]'
|
|
|
|
|
|
|
|
|
class ItalicPattern(TextFormatter):
|
|
|
name = 'i'
|
|
|
preview_left = '<i>'
|
|
|
preview_right = '</i>'
|
|
|
|
|
|
format_left = '[i]'
|
|
|
format_right = '[/i]'
|
|
|
|
|
|
|
|
|
class BoldPattern(TextFormatter):
|
|
|
name = 'b'
|
|
|
preview_left = '<b>'
|
|
|
preview_right = '</b>'
|
|
|
|
|
|
format_left = '[b]'
|
|
|
format_right = '[/b]'
|
|
|
|
|
|
|
|
|
class CodePattern(TextFormatter):
|
|
|
name = 'code'
|
|
|
preview_left = '<code>'
|
|
|
preview_right = '</code>'
|
|
|
|
|
|
format_left = '[code]'
|
|
|
format_right = '[/code]'
|
|
|
|
|
|
|
|
|
class HintPattern(TextFormatter):
|
|
|
name = 'hint'
|
|
|
preview_left = '<span class="hint">'
|
|
|
preview_right = '</span>'
|
|
|
|
|
|
format_left = '[hint]'
|
|
|
format_right = '[/hint]'
|
|
|
|
|
|
|
|
|
def render_reflink(tag_name, value, options, parent, context):
|
|
|
result = '>>%s' % value
|
|
|
|
|
|
post = None
|
|
|
if REFLINK_PATTERN.match(value):
|
|
|
post_id = int(value)
|
|
|
|
|
|
try:
|
|
|
post = boards.models.Post.objects.get(id=post_id)
|
|
|
|
|
|
except ObjectDoesNotExist:
|
|
|
pass
|
|
|
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()
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
def render_quote(tag_name, value, options, parent, context):
|
|
|
source = ''
|
|
|
if 'source' in options:
|
|
|
source = options['source']
|
|
|
elif 'quote' in options:
|
|
|
source = options['quote']
|
|
|
|
|
|
if source:
|
|
|
result = '<div class="multiquote"><div class="quote-header">%s</div><div class="quote-text">%s</div></div>' % (source, value)
|
|
|
else:
|
|
|
# Insert a ">" at the start of every line
|
|
|
result = '<span class="quote">>{}</span>'.format(
|
|
|
value.replace(LINE_BREAK_HTML,
|
|
|
'{}>'.format(LINE_BREAK_HTML)))
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
def render_notification(tag_name, value, options, parent, content):
|
|
|
username = value.lower()
|
|
|
|
|
|
return '<a href="{}" class="user-cast">@{}</a>'.format(
|
|
|
reverse('notifications', kwargs={'username': username}), username)
|
|
|
|
|
|
|
|
|
def render_tag(tag_name, value, options, parent, context):
|
|
|
tag_name = value.lower()
|
|
|
|
|
|
tag = boards.models.Tag.objects.get_by_alias(tag_name)
|
|
|
if tag:
|
|
|
url = tag.get_view()
|
|
|
else:
|
|
|
url = tag_name
|
|
|
|
|
|
return url
|
|
|
|
|
|
|
|
|
def render_spoiler(tag_name, value, options, parent, context):
|
|
|
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)
|
|
|
|
|
|
|
|
|
formatters = [
|
|
|
QuotePattern,
|
|
|
SpoilerPattern,
|
|
|
ItalicPattern,
|
|
|
BoldPattern,
|
|
|
CommentPattern,
|
|
|
StrikeThroughPattern,
|
|
|
CodePattern,
|
|
|
HintPattern,
|
|
|
]
|
|
|
|
|
|
|
|
|
PREPARSE_PATTERNS = {
|
|
|
r'(?<!>)>>(\d+)': r'[post]\1[/post]', # Reflink ">>123"
|
|
|
r'^>([^>].+)': r'[quote]\1[/quote]', # Quote ">text"
|
|
|
r'^//\s?(.+)': r'[comment]\1[/comment]', # Comment "//text"
|
|
|
r'\B@(\w+)': r'[user]\1[/user]', # User notification "@user"
|
|
|
}
|
|
|
|
|
|
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]'
|
|
|
|
|
|
|
|
|
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.
|
|
|
self.parser = bbcode.Parser(newline=LINE_BREAK_HTML)
|
|
|
|
|
|
self.parser.add_formatter('post', render_reflink, strip=True)
|
|
|
self.parser.add_formatter('quote', render_quote, strip=True)
|
|
|
self.parser.add_formatter('hint', render_hint, strip=True)
|
|
|
self.parser.add_formatter('user', render_notification, strip=True)
|
|
|
self.parser.add_formatter('tag', render_tag, strip=True)
|
|
|
self.parser.add_formatter('spoiler', render_spoiler, strip=True)
|
|
|
self.parser.add_simple_formatter(
|
|
|
'comment', '<span class="comment">// %(value)s</span>', strip=True)
|
|
|
self.parser.add_simple_formatter(
|
|
|
's', '<span class="strikethrough">%(value)s</span>')
|
|
|
self.parser.add_simple_formatter('code',
|
|
|
'<pre><code>%(value)s</pre></code>',
|
|
|
render_embedded=False,
|
|
|
escape_html=True,
|
|
|
replace_links=False,
|
|
|
replace_cosmetic=False)
|
|
|
|
|
|
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):
|
|
|
return self.parser.format(text)
|
|
|
|
|
|
|
|
|
parser = Parser()
|
|
|
|
|
|
|
|
|
def get_parser():
|
|
|
return parser
|
|
|
|