# 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 = '
' 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 = '' preview_right = '' format_left = '[quote]' format_right = '[/quote]' class SpoilerPattern(TextFormatter): name = 'spoiler' preview_left = '' preview_right = '' format_left = '[spoiler]' format_right = '[/spoiler]' class CommentPattern(TextFormatter): name = '' preview_left = '// ' preview_right = '' format_left = '[comment]' format_right = '[/comment]' # TODO Use tag here class StrikeThroughPattern(TextFormatter): name = 's' preview_left = '' preview_right = '' format_left = '[s]' format_right = '[/s]' class ItalicPattern(TextFormatter): name = 'i' preview_left = '' preview_right = '' format_left = '[i]' format_right = '[/i]' class BoldPattern(TextFormatter): name = 'b' preview_left = '' preview_right = '' format_left = '[b]' format_right = '[/b]' class CodePattern(TextFormatter): name = 'code' preview_left = '' preview_right = '' format_left = '[code]' format_right = '[/code]' class HintPattern(TextFormatter): name = 'hint' preview_left = '' preview_right = '' 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 = '
%s
%s
' % (source, value) else: # Insert a ">" at the start of every line result = '>{}'.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 = '{}'.format(hint, value) else: result = value return result def render_notification(tag_name, value, options, parent, content): username = value.lower() return '@{}'.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 '{}{}{}'.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', '// %(value)s', strip=True) self.parser.add_simple_formatter( 's', '%(value)s') self.parser.add_simple_formatter('code', '
%(value)s
', 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