# 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 from django.utils.translation import ugettext_lazy as _ import boards from boards import settings from swineboard.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 = ' ' REGEX_ANY_LINE_BREAK = re.compile(r'(' + LINE_BREAK_HTML + '|\r|\n)') QUOTE_REPLACEMENT = '\g<1>>' 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 = '' # Tag name to enclose the text in the form tag_name = '' has_input = 'false' input_prompt = '' 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 = '' tag_name = 'quote' class SpoilerPattern(TextFormatter): name = 'spoiler' preview_left = '' preview_right = '' tag_name = 'spoiler' class CommentPattern(TextFormatter): name = '' preview_left = '// ' preview_right = '' tag_name = 'comment' # TODO Use tag here class StrikeThroughPattern(TextFormatter): name = 's' preview_left = '' preview_right = '' tag_name = 's' class ItalicPattern(TextFormatter): name = 'i' preview_left = '' preview_right = '' tag_name = 'i' class BoldPattern(TextFormatter): name = 'b' preview_left = '' preview_right = '' tag_name = 'b' class CodePattern(TextFormatter): name = 'code' preview_left = '' preview_right = '' tag_name = 'code' class HintPattern(TextFormatter): name = 'hint' preview_left = '' preview_right = '' tag_name = 'hint' class ColorPattern(TextFormatter): name = 'color' preview_left = '' preview_right = '' tag_name = 'color' has_input = 'true' input_prompt = _('Enter color (name or #ccc code)') 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 = options.get('quote') or options.get('source') if source: result = '
%s
%s
' % (source, value) else: # Insert a ">" at the start of every line result = '>{}'.format( REGEX_ANY_LINE_BREAK.sub(QUOTE_REPLACEMENT, value)) 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, ColorPattern, ] 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