" for x in list_body.split("\1") if x])
-
-def tag_color(children, attr, surrounding):
- if not attr:
- return f"[color]{children}[/color]"
-
- hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
- potential_color = attr.lower().strip()
-
- if potential_color in NAMED_COLORS:
- return f"{children}"
-
- m = re.match(hex_re, potential_color)
- if m:
- return f"{children}"
-
- # return just the way it was if we can't parse it
- return f"[color={attr}]{children}[/color]"
-
-def tag_spoiler(children, attr, surrounding):
- spoiler_name = attr if attr else "Spoiler"
- content = f"
" for x in list_body.split("\1") if x])
+
+def tag_color(children, attr):
+ if not attr:
+ return f"[color]{children}[/color]"
+
+ hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
+ potential_color = attr.lower().strip()
+
+ if potential_color in NAMED_COLORS:
+ return f"{children}"
+
+ m = re.match(hex_re, potential_color)
+ if m:
+ return f"{children}"
+
+ # return just the way it was if we can't parse it
+ return f"[color={attr}]{children}[/color]"
+
+def tag_spoiler(children, attr):
+ spoiler_name = attr if attr else "Spoiler"
+ content = f"
'
+
+RSS_TAGS = {
+ **TAGS,
+ 'img': lambda children, attr: f'',
+ 'spoiler': lambda children, attr: f'{attr or "Spoiler"}{children}',
+ 'code': tag_code_rss,
+
+ 'big': lambda children, attr: f'{children}',
+ 'small': lambda children, attr: f'{children}'
+}
+
+VOID_TAGS = {
+ 'lb': lambda attr: '[',
+ 'rb': lambda attr: ']',
+ '@': lambda attr: '@',
+}
+
+# [img] is considered block for the purposes of collapsing whitespace,
+# despite being potentially inline (since the resulting tag is inline, but creates a block container around itself and sibling images).
+# [code] has a special case in is_inline().
+INLINE_TAGS = {
+ 'b', 'i', 's', 'u', 'color', 'big', 'small', 'url', 'lb', 'rb', '@'
+}
+
+def is_tag(e, tag=None):
+ if e is None:
+ return False
+ if isinstance(e, str):
+ return False
+ if e['type'] != 'bbcode' and e['type'] != 'bbcode_void':
+ return False
+
+ if tag is None:
+ return True
+
+ return e['name'] == tag
+
+def is_text(e):
+ return isinstance(e, str)
def is_inline(e):
if e is None:
@@ -219,7 +462,7 @@ def is_inline(e):
if is_tag(e):
if is_tag(e, 'code'): # special case, since [code] can be inline OR block
- return '\n' not in e['children']
+ return '\n' not in e['children'][0]
return e['name'] in INLINE_TAGS
@@ -227,21 +470,22 @@ def is_inline(e):
def make_mention(e, mentions):
from ..models import Users
- from flask import url_for
- target_user = Users.find({'username': e['name'].lower()})
- if not target_user:
- return f"@{e['name']}"
+ from flask import url_for, current_app
+ with current_app.test_request_context('/'):
+ target_user = Users.find({'username': e['name'].lower()})
+ if not target_user:
+ return f"@{e['name']}"
- mention_data = {
- 'mention_text': f"@{e['name']}",
- 'mentioned_user_id': int(target_user.id),
- "start": e['start'],
- "end": e['end'],
- }
- if mention_data not in mentions:
- mentions.append(mention_data)
+ mention_data = {
+ 'mention_text': f"@{e['name']}",
+ 'mentioned_user_id': int(target_user.id),
+ "start": e['start'],
+ "end": e['end'],
+ }
+ if mention_data not in mentions:
+ mentions.append(mention_data)
- return f"{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}"
+ return f"{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}"
def should_collapse(text, surrounding):
if not isinstance(text, str):
@@ -255,10 +499,30 @@ def should_collapse(text, surrounding):
return False
+
def sanitize(s):
return escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
-def babycode_to_html(s, banned_tags=[]):
+
+def babycode_ast(s: str, banned_tags=[]):
+ """
+ transforms a string of babycode into an AST.
+ the AST is a list of strings or dicts.
+
+ a string element is plain unformatted text.
+
+ a dict element is a node that contains at least the key `type`.
+
+ possible types are:
+ - bbcode
+ - bbcode_void
+ - link
+ - emote
+ - rule
+ - mention
+
+ bbcode type elements have a children key that is a list of children of that node. the children are themselves elements (string or dict).
+ """
allowed_tags = set(TAGS.keys())
if banned_tags is not None:
for tag in banned_tags:
@@ -281,44 +545,38 @@ def babycode_to_html(s, banned_tags=[]):
)
if not should_collapse(e, surrounding):
elements.append(e)
+ return elements
- out = ""
- mentions = []
- def fold(element, nobr, surrounding):
- if isinstance(element, str):
- if nobr:
- return element
- return break_lines(element)
- match element['type']:
- case "bbcode":
- c = ""
- for i in range(len(element['children'])):
- child = element['children'][i]
- _surrounding = (
- element['children'][i - 1] if i-1 >= 0 else None,
- element['children'][i + 1] if i+1 < len(element['children']) else None
- )
- _nobr = element['name'] == "code" or element['name'] == "ul" or element['name'] == "ol"
- c = c + Markup(fold(child, _nobr, _surrounding))
- res = TAGS[element['name']](c, element['attr'], surrounding)
- return res
- case "bbcode_void":
- return VOID_TAGS[element['name']](element['attr'])
- case "link":
- return f"{element['url']}"
- case 'emote':
- return EMOJI[element['name']]
- case "rule":
- return ""
- case "mention":
- return make_mention(element, mentions)
+def babycode_to_html(s: str, banned_tags=[], fragment=False):
+ """
+ transforms a string of babycode into html.
- for i in range(len(elements)):
- e = elements[i]
- surrounding = (
- elements[i - 1] if i-1 >= 0 else None,
- elements[i + 1] if i+1 < len(elements) else None
- )
- out = out + fold(e, False, surrounding)
- return BabycodeParseResult(out, mentions)
+ parameters:
+
+ s (str) - babycode string
+
+ banned_tags (list) - babycode tags to exclude from being parsed. they will remain as plain text in the transformation.
+
+ fragment (bool) - skip adding an html p tag to the first element if it is inline.
+ """
+ ast = babycode_ast(s, banned_tags)
+ r = HTMLRenderer(fragment=fragment)
+ return r.render(ast)
+
+
+def babycode_to_rssxml(s: str, banned_tags=[], fragment=False):
+ """
+ transforms a string of babycode into rss-compatible x/html.
+
+ parameters:
+
+ s (str) - babycode string
+
+ banned_tags (list) - babycode tags to exclude from being parsed. they will remain as plain text in the transformation.
+
+ fragment (bool) - skip adding an html p tag to the first element if it is inline.
+ """
+ ast = babycode_ast(s, banned_tags)
+ r = RSSXMLRenderer(fragment=fragment)
+ return r.render(ast)