mirror of
https://github.com/liberatedsystems/openCom-Companion.git
synced 2025-02-23 09:49:51 +01:00
228 lines
7.7 KiB
Python
228 lines
7.7 KiB
Python
from mistune.core import BaseRenderer
|
|
from mistune.util import escape as escape_text, striptags, safe_entity
|
|
from urllib.parse import urljoin, urlparse
|
|
|
|
|
|
class BBCodeRenderer(BaseRenderer):
|
|
"""A renderer for converting Markdown to BBCode."""
|
|
_escape: bool
|
|
NAME = 'bbcode'
|
|
|
|
def __init__(self, escape=False, domain=None):
|
|
super(BBCodeRenderer, self).__init__()
|
|
self._escape = escape
|
|
self.domain = domain
|
|
|
|
def render_token(self, token, state):
|
|
func = self._get_method(token['type'])
|
|
attrs = token.get('attrs')
|
|
|
|
if 'raw' in token:
|
|
text = token['raw']
|
|
elif 'children' in token:
|
|
text = self.render_tokens(token['children'], state)
|
|
else:
|
|
if attrs:
|
|
return func(**attrs)
|
|
else:
|
|
return func()
|
|
if attrs:
|
|
return func(text, **attrs)
|
|
else:
|
|
return func(text)
|
|
|
|
def safe_url(self, url: str) -> str:
|
|
# Simple URL sanitization
|
|
if url.startswith(('javascript:', 'vbscript:', 'data:')):
|
|
return '#harmful-link'
|
|
# Check if the URL is absolute by looking for a netloc part in the URL
|
|
if not urlparse(url).netloc:
|
|
url = urljoin(self.domain, url)
|
|
return url
|
|
|
|
def text(self, text: str) -> str:
|
|
if self._escape:
|
|
return escape_text(text)
|
|
return text
|
|
|
|
def emphasis(self, text: str) -> str:
|
|
return '[i]' + text + '[/i]'
|
|
|
|
def strong(self, text: str) -> str:
|
|
return '[b]' + text + '[/b]'
|
|
|
|
def link(self, text: str, url: str, title=None) -> str:
|
|
return '[url=' + self.safe_url(url) + ']' + text + '[/url]'
|
|
|
|
def image(self, text: str, url: str, title=None) -> str:
|
|
alt_text = f' alt="{text}"' if text else ''
|
|
img_tag = f'[img{alt_text}]' + self.safe_url(url) + '[/img]'
|
|
# Check if alt text starts with 'pixel' and treat it as pixel art
|
|
if text and text.lower().startswith('pixel'):
|
|
return f'[pixelate]{img_tag}[/pixelate]'
|
|
return img_tag
|
|
|
|
def codespan(self, text: str) -> str:
|
|
return '[icode]' + text + '[/icode]'
|
|
|
|
def linebreak(self) -> str:
|
|
return '\n'
|
|
|
|
def softbreak(self) -> str:
|
|
return ''
|
|
|
|
def inline_html(self, html: str) -> str:
|
|
if self._escape:
|
|
return escape_text(html)
|
|
return html
|
|
|
|
def paragraph(self, text: str) -> str:
|
|
return text + '\n\n'
|
|
|
|
def heading(self, text: str, level: int, **attrs) -> str:
|
|
if 1 <= level <= 3:
|
|
return f"[HEADING={level}]{text}[/HEADING]\n"
|
|
else:
|
|
# Handle cases where level is outside 1-3
|
|
return f"[HEADING=3]{text}[/HEADING]\n"
|
|
|
|
def blank_line(self) -> str:
|
|
return ''
|
|
|
|
def thematic_break(self) -> str:
|
|
return '[hr][/hr]\n'
|
|
|
|
def block_text(self, text: str) -> str:
|
|
return text
|
|
|
|
def block_code(self, code: str, **attrs) -> str:
|
|
# Renders blocks of code using the language specified in Markdown
|
|
special_cases = {
|
|
'plaintext': None # Default [CODE]
|
|
}
|
|
|
|
if 'info' in attrs:
|
|
lang_info = safe_entity(attrs['info'].strip())
|
|
lang = lang_info.split(None, 1)[0].lower()
|
|
# Check if the language needs special handling
|
|
bbcode_lang = special_cases.get(lang, lang) # Use the special case if it exists, otherwise use lang as is
|
|
if bbcode_lang:
|
|
return f"[CODE={bbcode_lang}]{escape_text(code)}[/CODE]\n"
|
|
else:
|
|
return f"[CODE]{escape_text(code)}[/CODE]\n"
|
|
else:
|
|
# No language specified, render with a generic [CODE] tag
|
|
return f"[CODE]{escape_text(code)}[/CODE]\n"
|
|
|
|
def block_quote(self, text: str) -> str:
|
|
return '[QUOTE]\n' + text + '[/QUOTE]\n'
|
|
|
|
def block_html(self, html: str) -> str:
|
|
if self._escape:
|
|
return '<p>' + escape_text(html.strip()) + '</p>\n'
|
|
return html + '\n'
|
|
|
|
def block_error(self, text: str) -> str:
|
|
return '[color=red][icode]' + text + '[/icode][/color]\n'
|
|
|
|
def list(self, text: str, ordered: bool, **attrs) -> str:
|
|
# For ordered lists, always use [list=1] to get automatic sequential numbering
|
|
# For unordered lists, use [list]
|
|
tag = 'list=1' if ordered else 'list'
|
|
return '[{}]'.format(tag) + text + '[/list]\n'
|
|
|
|
def list_item(self, text: str) -> str:
|
|
return '[*]' + text + '\n'
|
|
|
|
def strikethrough(self, text: str) -> str:
|
|
return '[s]' + text + '[/s]'
|
|
|
|
def mark(self, text: str) -> str:
|
|
# Simulate the mark effect with a background color in BBCode
|
|
return '[mark]' + text + '[/mark]'
|
|
|
|
def insert(self, text: str) -> str:
|
|
# Use underline to represent insertion
|
|
return '[u]' + text + '[/u]'
|
|
|
|
def superscript(self, text: str) -> str:
|
|
return '[sup]' + text + '[/sup]'
|
|
|
|
def subscript(self, text: str) -> str:
|
|
return '[sub]' + text + '[/sub]'
|
|
|
|
def inline_spoiler(self, text: str) -> str:
|
|
return '[ISPOILER]' + text + '[/ISPOILER]'
|
|
|
|
def block_spoiler(self, text: str) -> str:
|
|
return '[SPOILER]\n' + text + '\n[/SPOILER]'
|
|
|
|
def footnote_ref(self, key: str, index: int):
|
|
# Use superscript for the footnote reference
|
|
return f'[sup][u][JUMPTO=fn-{index}]{index}[/JUMPTO][/u][/sup]'
|
|
|
|
def footnotes(self, text: str):
|
|
# Optionally wrap all footnotes in a specific section if needed
|
|
return '[b]Footnotes:[/b]\n' + text
|
|
|
|
def footnote_item(self, text: str, key: str, index: int):
|
|
# Define the footnote with an anchor at the end of the document
|
|
return f'[ANAME=fn-{index}]{index}[/ANAME]. {text}'
|
|
|
|
def table(self, children, **attrs):
|
|
# Starting with a full-width table by default if not specified
|
|
# width = attrs.get('width', '100%') # comment out until XF 2.3
|
|
# return f'[TABLE width="{width}"]\n' + children + '[/TABLE]\n' # comment out until XF 2.3
|
|
return '[TABLE]\n' + children + '[/TABLE]\n'
|
|
|
|
def table_head(self, children, **attrs):
|
|
return '[TR]\n' + children + '[/TR]\n'
|
|
|
|
def table_body(self, children, **attrs):
|
|
return children
|
|
|
|
def table_row(self, children, **attrs):
|
|
return '[TR]\n' + children + '[/TR]\n'
|
|
|
|
def table_cell(self, text, align=None, head=False, **attrs):
|
|
# BBCode does not support direct cell alignment,
|
|
# use [LEFT], [CENTER], or [RIGHT] tags
|
|
|
|
# Use th for header cells and td for normal cells
|
|
tag = 'TH' if head else 'TD'
|
|
|
|
# Initialize alignment tags
|
|
alignment_start = ''
|
|
alignment_end = ''
|
|
|
|
if align == 'center':
|
|
alignment_start = '[CENTER]'
|
|
alignment_end = '[/CENTER]'
|
|
elif align == 'right':
|
|
alignment_start = '[RIGHT]'
|
|
alignment_end = '[/RIGHT]'
|
|
elif align == 'left':
|
|
alignment_start = '[LEFT]'
|
|
alignment_end = '[/LEFT]'
|
|
|
|
return f'[{tag}]{alignment_start}{text}{alignment_end}[/{tag}]\n'
|
|
|
|
def task_list_item(self, text: str, checked: bool = False) -> str:
|
|
# Using emojis to represent the checkbox
|
|
checkbox_emoji = '🗹' if checked else '☐'
|
|
return checkbox_emoji + ' ' + text + '\n'
|
|
|
|
def def_list(self, text: str) -> str:
|
|
# No specific BBCode tag for <dl>, so we just use the plain text grouping
|
|
return '\n' + text + '\n'
|
|
|
|
def def_list_head(self, text: str) -> str:
|
|
return '[b]' + text + '[/b]' + ' ' + ':' + '\n'
|
|
|
|
def def_list_item(self, text: str) -> str:
|
|
return '[INDENT]' + text + '[/INDENT]\n'
|
|
|
|
def abbr(self, text: str, title: str) -> str:
|
|
if title:
|
|
return f'[abbr={title}]{text}[/abbr]'
|
|
return text |