Compare commits

...

13 Commits

31 changed files with 936 additions and 353 deletions

View File

@@ -4,5 +4,7 @@
data/db/* data/db/*
data/static/avatars/* data/static/avatars/*
!data/static/avatars/default.webp !data/static/avatars/default.webp
data/static/badges/user
data/_cached
.local/ .local/

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ data/db/*
data/static/avatars/* data/static/avatars/*
!data/static/avatars/default.webp !data/static/avatars/default.webp
data/static/badges/user data/static/badges/user
data/_cached
config/secrets.prod.env config/secrets.prod.env
config/pyrom_config.toml config/pyrom_config.toml

View File

@@ -80,8 +80,8 @@ Repo: https://github.com/emcconville/wand
## Bitty ## Bitty
Affected files: [`data/static/js/vnd/bitty-6.0.0-rc3.min.js`](./data/static/js/vnd/bitty-6.0.0-rc3.min.js) Affected files: [`data/static/js/vnd/bitty-7.0.0.js`](./data/static/js/vnd/bitty-7.0.0.js)
URL: https://bitty.alanwsmith.com/ URL: https://bitty-js.com/
License: CC0 1.0 License: CC0 1.0
Author: alan w smith https://www.alanwsmith.com/ Author: alan w smith https://www.alanwsmith.com/
Repo: https://github.com/alanwsmith/bitty Repo: https://github.com/alanwsmith/bitty

View File

@@ -3,15 +3,15 @@ from dotenv import load_dotenv
from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads from .models import Avatars, Users, PostHistory, Posts, MOTD, BadgeUploads
from .auth import digest from .auth import digest
from .routes.users import is_logged_in, get_active_user, get_prefers_theme from .routes.users import is_logged_in, get_active_user, get_prefers_theme
from .routes.threads import get_post_url
from .constants import ( from .constants import (
PermissionLevel, permission_level_string, PermissionLevel, permission_level_string,
InfoboxKind, InfoboxHTMLClass, InfoboxKind, InfoboxHTMLClass,
REACTION_EMOJI, MOTD_BANNED_TAGS, REACTION_EMOJI, MOTD_BANNED_TAGS,
SIG_BANNED_TAGS, STRICT_BANNED_TAGS, SIG_BANNED_TAGS, STRICT_BANNED_TAGS,
) )
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION from .lib.babycode import babycode_to_html, babycode_to_rssxml, EMOJI, BABYCODE_VERSION
from datetime import datetime from datetime import datetime, timezone
from flask_caching import Cache
import os import os
import time import time
import secrets import secrets
@@ -55,6 +55,18 @@ def reparse_babycode():
print('Re-parsing babycode, this may take a while...') print('Re-parsing babycode, this may take a while...')
from .db import db from .db import db
from .constants import MOTD_BANNED_TAGS from .constants import MOTD_BANNED_TAGS
post_histories_without_rss = PostHistory.findall([
('markup_language', '=', 'babycode'),
('content_rss', 'IS', None),
])
with db.transaction():
for ph in post_histories_without_rss:
ph.update({
'content_rss': babycode_to_rssxml(ph['original_markup']),
})
post_histories = PostHistory.findall([ post_histories = PostHistory.findall([
('markup_language', '=', 'babycode'), ('markup_language', '=', 'babycode'),
('format_version', 'IS NOT', BABYCODE_VERSION) ('format_version', 'IS NOT', BABYCODE_VERSION)
@@ -65,6 +77,7 @@ def reparse_babycode():
for ph in post_histories: for ph in post_histories:
ph.update({ ph.update({
'content': babycode_to_html(ph['original_markup']).result, 'content': babycode_to_html(ph['original_markup']).result,
'content_rss': babycode_to_rssxml(ph['original_markup']),
'format_version': BABYCODE_VERSION, 'format_version': BABYCODE_VERSION,
}) })
print('Re-parsing posts done.') print('Re-parsing posts done.')
@@ -125,6 +138,8 @@ def bind_default_badges(path):
}) })
cache = Cache()
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__)
app.config['SITE_NAME'] = 'Pyrom' app.config['SITE_NAME'] = 'Pyrom'
@@ -133,6 +148,10 @@ def create_app():
app.config['USERS_CAN_INVITE'] = False app.config['USERS_CAN_INVITE'] = False
app.config['ADMIN_CONTACT_INFO'] = '' app.config['ADMIN_CONTACT_INFO'] = ''
app.config['GUIDE_DESCRIPTION'] = '' app.config['GUIDE_DESCRIPTION'] = ''
app.config['CACHE_TYPE'] = 'FileSystemCache'
app.config['CACHE_DEFAULT_TIMEOUT'] = 300
try: try:
app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False) app.config.from_file('../config/pyrom_config.toml', load=tomllib.load, text=False)
except FileNotFoundError: except FileNotFoundError:
@@ -142,6 +161,7 @@ def create_app():
app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static") app.static_folder = os.path.join(os.path.dirname(__file__), "../data/static")
app.debug = True app.debug = True
app.config["DB_PATH"] = "data/db/db.dev.sqlite" app.config["DB_PATH"] = "data/db/db.dev.sqlite"
app.config["SERVER_NAME"] = "localhost:8080"
load_dotenv() load_dotenv()
else: else:
app.config["DB_PATH"] = "data/db/db.prod.sqlite" app.config["DB_PATH"] = "data/db/db.prod.sqlite"
@@ -156,6 +176,13 @@ def create_app():
os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True) os.makedirs(os.path.dirname(app.config["DB_PATH"]), exist_ok = True)
os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True) os.makedirs(os.path.dirname(app.config["BADGES_UPLOAD_PATH"]), exist_ok = True)
if app.config['CACHE_TYPE'] == 'FileSystemCache':
cache_dir = app.config.get('CACHE_DIR', 'data/_cached')
os.makedirs(cache_dir, exist_ok = True)
app.config['CACHE_DIR'] = cache_dir
cache.init_app(app)
css_dir = 'data/static/css/' css_dir = 'data/static/css/'
allowed_themes = [] allowed_themes = []
for f in os.listdir(css_dir): for f in os.listdir(css_dir):
@@ -167,20 +194,6 @@ def create_app():
allowed_themes.sort(key=(lambda x: (x != 'style', x))) allowed_themes.sort(key=(lambda x: (x != 'style', x)))
app.config['allowed_themes'] = allowed_themes app.config['allowed_themes'] = allowed_themes
with app.app_context():
from .schema import create as create_tables
from .migrations import run_migrations
create_tables()
run_migrations()
create_default_avatar()
create_admin()
create_deleted_user()
reparse_babycode()
bind_default_badges(app.config['BADGES_PATH'])
from app.routes.app import bp as app_bp from app.routes.app import bp as app_bp
from app.routes.topics import bp as topics_bp from app.routes.topics import bp as topics_bp
from app.routes.threads import bp as threads_bp from app.routes.threads import bp as threads_bp
@@ -200,6 +213,20 @@ def create_app():
app.register_blueprint(hyperapi_bp) app.register_blueprint(hyperapi_bp)
app.register_blueprint(guides_bp) app.register_blueprint(guides_bp)
with app.app_context():
from .schema import create as create_tables
from .migrations import run_migrations
create_tables()
run_migrations()
create_default_avatar()
create_admin()
create_deleted_user()
reparse_babycode()
bind_default_badges(app.config['BADGES_PATH'])
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_SECURE'] = True
@app.before_request @app.before_request
@@ -229,10 +256,12 @@ def create_app():
@app.context_processor @app.context_processor
def inject_funcs(): def inject_funcs():
from .routes.threads import get_post_url
return { return {
'get_post_url': get_post_url, 'get_post_url': get_post_url,
'get_prefers_theme': get_prefers_theme, 'get_prefers_theme': get_prefers_theme,
'get_motds': MOTD.get_all, 'get_motds': MOTD.get_all,
'get_time_now': lambda: int(time.time()),
} }
@app.template_filter("ts_datetime") @app.template_filter("ts_datetime")
@@ -251,12 +280,12 @@ def create_app():
return permission_level_string(term) return permission_level_string(term)
@app.template_filter('babycode') @app.template_filter('babycode')
def babycode_filter(markup): def babycode_filter(markup, nofrag=False):
return babycode_to_html(markup).result return babycode_to_html(markup, fragment=not nofrag).result
@app.template_filter('babycode_strict') @app.template_filter('babycode_strict')
def babycode_strict_filter(markup): def babycode_strict_filter(markup, nofrag=False):
return babycode_to_html(markup, STRICT_BANNED_TAGS).result return babycode_to_html(markup, banned_tags=STRICT_BANNED_TAGS, fragment=not nofrag).result
@app.template_filter('extract_h2') @app.template_filter('extract_h2')
def extract_h2(content): def extract_h2(content):
@@ -308,4 +337,8 @@ def create_app():
def fromjson(subject: str): def fromjson(subject: str):
return json.loads(subject) return json.loads(subject)
@app.template_filter('iso8601')
def unix_to_iso8601(subject: str):
return datetime.fromtimestamp(int(subject), timezone.utc).isoformat()
return app return app

View File

@@ -6,17 +6,204 @@ from pygments.lexers import get_lexer_by_name
from pygments.util import ClassNotFound as PygmentsClassNotFound from pygments.util import ClassNotFound as PygmentsClassNotFound
import re import re
class BabycodeParseResult: BABYCODE_VERSION = 8
class BabycodeError(Exception):
pass
class BabycodeRenderError(BabycodeError):
pass
class UnknownASTElementError(BabycodeRenderError):
def __init__(self, element_type, element=None):
self.element_type = element_type
self.element = element
message = f'Unknown AST element: {element_type}'
if element:
message += f' (element: {element})'
super().__init__(message)
class BabycodeRenderResult:
def __init__(self, result, mentions=[]): def __init__(self, result, mentions=[]):
self.result = result self.result = result
self.mentions = mentions self.mentions = mentions
def __str__(self): def __str__(self):
return self.result return self.result
BABYCODE_VERSION = 5 class BabycodeRenderer:
def __init__(self, tag_map, void_tag_map, emote_map, fragment=False):
self.tag_map = tag_map
self.void_tag_map = void_tag_map
self.emote_map = emote_map
self.fragment = fragment
def make_mention(self, element):
raise NotImplementedError
def transform_para_whitespace(self, text):
# markdown rules:
# two spaces at end of line -> <br>
text = re.sub(r' +\n', '<br>', text)
# single newlines -> space (collapsed)
text = re.sub(r'\n', ' ', text)
return text
def wrap_in_paragraphs(self, nodes, context_is_block=True, is_root=False):
result = []
current_paragraph = []
is_first_para = is_root and self.fragment
def flush_paragraph():
# TIL nonlocal exists
nonlocal result, current_paragraph, is_first_para
if not current_paragraph:
return
para_content = ''.join(current_paragraph)
if para_content.strip(): # skip empty paragraphs
if is_first_para:
result.append(para_content)
is_first_para = False
else:
result.append(f"<p>{para_content}</p>")
current_paragraph.clear()
for node in nodes:
if isinstance(node, str):
paras = re.split(r'\n\n+', node)
for i, para in enumerate(paras):
if i > 0 and context_is_block:
flush_paragraph()
if para:
processed = self.transform_para_whitespace(para)
current_paragraph.append(processed)
else:
inline = is_inline(node)
if inline and context_is_block:
# inline child within a paragraph context
current_paragraph.append(self.fold(node))
elif not inline and context_is_block:
# block child within a block context
flush_paragraph()
if is_root:
# this is relevant for fragment.
# fragment only applies to the first inline node(s).
# if the first element is a block, reset "fragment mode".
is_first_para = False
result.append(self.fold(node))
else:
# either inline in inline context, or block in inline context
current_paragraph.append(self.fold(node))
if context_is_block:
# flush final para if we're in a block context
flush_paragraph()
elif current_paragraph:
# inline context - just append whatever we collected
result.append(''.join(current_paragraph))
return ''.join(result)
def fold(self, element):
if isinstance(element, str):
return element
match element['type']:
case 'bbcode':
tag_name = element['name']
if is_inline(element):
# inline tag
# since its inline, all children should be processed inline
content = "".join(self.fold(child) for child in element['children'])
return self.tag_map[tag_name](content, element['attr'])
else:
# block tag
if tag_name in {'ul', 'ol', 'code', 'img'}:
# these handle their own internal structure
content = ''.join(
child if isinstance(child, str) else self.fold(child)
for child in element['children']
)
return self.tag_map[tag_name](content, element['attr'])
else:
# block elements that can contain paragraphs
content = self.wrap_in_paragraphs(element['children'], context_is_block=True, is_root=False)
return self.tag_map[tag_name](content, element['attr'])
case 'bbcode_void':
return self.void_tag_map[element['name']](element['attr'])
case 'link':
return f"<a href=\"{element['url']}\">{element['url']}</a>"
case 'emote':
return self.emote_map[element['name']]
case 'rule':
return '<hr>'
case 'mention':
return self.make_mention(element)
case _:
raise UnknownASTElementError(
element_type=element['type'],
element=element
)
def render(self, ast):
out = self.wrap_in_paragraphs(ast, context_is_block=True, is_root=True)
return out
class HTMLRenderer(BabycodeRenderer):
def __init__(self, fragment=False):
super().__init__(TAGS, VOID_TAGS, EMOJI, fragment)
self.mentions = []
def make_mention(self, e):
from ..models import Users
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 self.mentions:
self.mentions.append(mention_data)
return f"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.page', username=target_user.username)}' title='@{target_user.username}' data-init='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
def render(self, ast):
out = super().render(ast)
return BabycodeRenderResult(out, self.mentions)
class RSSXMLRenderer(BabycodeRenderer):
def __init__(self, fragment=False):
super().__init__(RSS_TAGS, VOID_TAGS, RSS_EMOJI, fragment)
def make_mention(self, e):
from ..models import Users
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']}"
return f'<a href="{url_for('users.page', username=target_user.username, _external=True)}" title="@{target_user.username}">{target_user.get_readable_name()}</a>'
NAMED_COLORS = [ NAMED_COLORS = [
'black', 'silver', 'gray', 'white', 'maroon', 'red', 'black', 'silver', 'gray', 'white', 'maroon', 'red',
@@ -49,114 +236,11 @@ NAMED_COLORS = [
'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen',
] ]
def is_tag(e, tag=None):
if e is None:
return False
if isinstance(e, str):
return False
if e['type'] != 'bbcode':
return False
if tag is None:
return True
return e['name'] == tag
def is_text(e):
return isinstance(e, str)
def tag_code(children, attr, surrounding):
is_inline = children.find('\n') == -1
if is_inline:
return f"<code class=\"inline-code\">{children}</code>"
else:
input_code = children.strip()
button = f"<button type=button class=\"copy-code\" value=\"{input_code}\" data-send=\"copyCode\" data-receive=\"copyCode\">Copy</button>"
unhighlighted = f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">code block</span>{button}</span><code>{input_code}</code></pre>"
if not attr:
return unhighlighted
try:
lexer = get_lexer_by_name(attr.strip())
formatter = HtmlFormatter(nowrap=True)
return f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">{lexer.name}</span>{button}</span><code>{highlight(input_code.unescape(), lexer, formatter)}</code></pre>"
except PygmentsClassNotFound:
return unhighlighted
def tag_list(children):
list_body = re.sub(r" +\n", "<br>", children.strip())
list_body = re.sub(r"\n\n+", "\1", list_body)
return " ".join([f"<li>{x}</li>" 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"<span style='color: {potential_color};'>{children}</span>"
m = re.match(hex_re, potential_color)
if m:
return f"<span style='color: #{m.group(1)};'>{children}</span>"
# 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"<div class='accordion-content post-accordion-content hidden'>{children}</div>"
container = f"""<div class='accordion hidden' data-receive='toggleAccordion'><div class='accordion-header'><button type='button' class='accordion-toggle' data-send='toggleAccordion'>+</button><span>{spoiler_name}</span></div>{content}</div>"""
return container
def tag_image(children, attr, surrounding):
img = f"<img class=\"post-image\" src=\"{attr}\" alt=\"{children}\">"
if not is_tag(surrounding[0], 'img'):
img = f"<div class=post-img-container>{img}"
if not is_tag(surrounding[1], 'img'):
img = f"{img}</div>"
return img
TAGS = {
"b": lambda children, attr, _: f"<strong>{children}</strong>",
"i": lambda children, attr, _: f"<em>{children}</em>",
"s": lambda children, attr, _: f"<del>{children}</del>",
"u": lambda children, attr, _: f"<u>{children}</u>",
"img": tag_image,
"url": lambda children, attr, _: f"<a href={attr}>{children}</a>",
"quote": lambda children, attr, _: f"<blockquote>{children}</blockquote>",
"code": tag_code,
"ul": lambda children, attr, _: f"<ul>{tag_list(children)}</ul>",
"ol": lambda children, attr, _: f"<ol>{tag_list(children)}</ol>",
"big": lambda children, attr, _: f"<span style='font-size: 2rem;'>{children}</span>",
"small": lambda children, attr, _: f"<span style='font-size: 0.75rem;'>{children}</span>",
"color": tag_color,
"center": lambda children, attr, _: f"<div style='text-align: center;'>{children}</div>",
"right": lambda children, attr, _: f"<div style='text-align: right;'>{children}</div>",
"spoiler": tag_spoiler,
}
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 <img> 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'
}
def make_emoji(name, code): def make_emoji(name, code):
return f' <img class=emoji src="/static/emoji/{name}.png" alt="{name}" title=":{code}:">' return f' <img class=emoji src="/static/emoji/{name}.png" alt="{name}" title=":{code}:">'
EMOJI = { EMOJI = {
'angry': make_emoji('angry', 'angry'), 'angry': make_emoji('angry', 'angry'),
@@ -203,12 +287,208 @@ EMOJI = {
'wink': make_emoji('wink', 'wink'), 'wink': make_emoji('wink', 'wink'),
} }
RSS_EMOJI = {
**EMOJI,
'angry': '😡',
'(': '🙁',
'D': '😃',
'imp': '😈',
'angryimp': '👿',
'impangry': '👿',
'lobster': '🦞',
'|': '😐',
'pensive': '😔',
'scissors': '✂️',
')': '🙂',
'smiletear': '🥲',
'crytear': '🥲',
',': '😭',
'T': '😭',
'cry': '😭',
'sob': '😭',
'o': '😮',
'O': '😮',
'hmm': '🤔',
'think': '🤔',
'thinking': '🤔',
'P': '😛',
'p': '😛',
'weary': '😩',
';': '😉',
'wink': '😉',
}
TEXT_ONLY = ["code"] TEXT_ONLY = ["code"]
def break_lines(text):
text = re.sub(r" +\n", "<br>", text) def tag_code(children, attr):
text = re.sub(r"\n\n+", "<br><br>", text) is_inline = children.find('\n') == -1
return text if is_inline:
return f"<code class=\"inline-code\">{children}</code>"
else:
input_code = children.strip()
button = f"<button type=button class=\"copy-code\" value=\"{input_code}\" data-send=\"copyCode\" data-receive=\"copyCode\">Copy</button>"
unhighlighted = f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">code block</span>{button}</span><code>{input_code}</code></pre>"
if not attr:
return unhighlighted
try:
lexer = get_lexer_by_name(attr.strip())
formatter = HtmlFormatter(nowrap=True)
return f"<pre><span class=\"copy-code-container\"><span class=\"code-language-identifier\">{lexer.name}</span>{button}</span><code>{highlight(Markup(input_code).unescape(), lexer, formatter)}</code></pre>"
except PygmentsClassNotFound:
return unhighlighted
def tag_list(children):
list_body = re.sub(r" +\n", "<br>", children.strip())
list_body = re.sub(r"\n\n+", "\1", list_body)
return " ".join([f"<li>{x}</li>" 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"<span style='color: {potential_color};'>{children}</span>"
m = re.match(hex_re, potential_color)
if m:
return f"<span style='color: #{m.group(1)};'>{children}</span>"
# 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"<div class='accordion-content post-accordion-content hidden'>{children}</div>"
container = f"""<div class='accordion hidden' data-receive='toggleAccordion'><div class='accordion-header'><button type='button' class='accordion-toggle' data-send='toggleAccordion'>+</button><span>{spoiler_name}</span></div>{content}</div>"""
return container
def tag_image(children, attr):
img = f"<img class=\"post-image\" src=\"{attr}\" alt=\"{children}\">"
return f"<div class=post-img-container>{img}</div>"
TAGS = {
"b": lambda children, attr: f"<strong>{children}</strong>",
"i": lambda children, attr: f"<em>{children}</em>",
"s": lambda children, attr: f"<del>{children}</del>",
"u": lambda children, attr: f"<u>{children}</u>",
"img": tag_image,
"url": lambda children, attr: f"<a href={attr}>{children}</a>",
"quote": lambda children, attr: f"<blockquote>{children}</blockquote>",
"code": tag_code,
"ul": lambda children, attr: f"<ul>{tag_list(children)}</ul>",
"ol": lambda children, attr: f"<ol>{tag_list(children)}</ol>",
"big": lambda children, attr: f"<span style='font-size: 2rem;'>{children}</span>",
"small": lambda children, attr: f"<span style='font-size: 0.75rem;'>{children}</span>",
"color": tag_color,
"center": lambda children, attr: f"<div style='text-align: center;'>{children}</div>",
"right": lambda children, attr: f"<div style='text-align: right;'>{children}</div>",
"spoiler": tag_spoiler,
}
def tag_code_rss(children, attr):
is_inline = children.find('\n') == -1
if is_inline:
return f'<code>{children}</code>'
else:
return f'<pre><code>{children}</code></pre>'
def tag_url_rss(children, attr):
if attr.startswith('/'):
from flask import current_app
uri = f"{current_app.config['PREFERRED_URL_SCHEME']}://{current_app.config['SERVER_NAME']}{attr}"
return f"<a href={uri}>{children}</a>"
return f"<a href={attr}>{children}</a>"
def tag_image_rss(children, attr):
if attr.startswith('/'):
from flask import current_app
uri = f"{current_app.config['PREFERRED_URL_SCHEME']}://{current_app.config['SERVER_NAME']}{attr}"
return f'<img src="{uri}" alt={children} />'
return f'<img src="{attr}" alt={children} />'
RSS_TAGS = {
**TAGS,
'img': tag_image_rss,
'url': tag_url_rss,
'spoiler': lambda children, attr: f'<details><summary>{attr or "Spoiler"} (click to reveal)</summary>{children}</details>',
'code': tag_code_rss,
'big': lambda children, attr: f'<span style="font-size: 1.2em">{children}</span>',
'small': lambda children, attr: f'<small>{children}</small>'
}
VOID_TAGS = {
'lb': lambda attr: '[',
'rb': lambda attr: ']',
'at': lambda attr: '@',
'd': lambda attr: '-',
}
# [img] is considered block for the purposes of collapsing whitespace,
# despite being potentially inline (since the resulting <img> 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', 'at', 'd'
}
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): def is_inline(e):
if e is None: if e is None:
@@ -219,29 +499,12 @@ def is_inline(e):
if is_tag(e): if is_tag(e):
if is_tag(e, 'code'): # special case, since [code] can be inline OR block 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 return e['name'] in INLINE_TAGS
return e['type'] != 'rule' return e['type'] != 'rule'
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']}"
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"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.page', username=target_user.username)}' title='@{target_user.username}' data-init='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
def should_collapse(text, surrounding): def should_collapse(text, surrounding):
if not isinstance(text, str): if not isinstance(text, str):
@@ -255,10 +518,30 @@ def should_collapse(text, surrounding):
return False return False
def sanitize(s): def sanitize(s):
return escape(s.strip().replace('\r\n', '\n').replace('\r', '\n')) 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()) allowed_tags = set(TAGS.keys())
if banned_tags is not None: if banned_tags is not None:
for tag in banned_tags: for tag in banned_tags:
@@ -281,44 +564,38 @@ def babycode_to_html(s, banned_tags=[]):
) )
if not should_collapse(e, surrounding): if not should_collapse(e, surrounding):
elements.append(e) 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']: def babycode_to_html(s: str, banned_tags=[], fragment=False) -> BabycodeRenderResult:
case "bbcode": """
c = "" transforms a string of babycode into html.
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"<a href=\"{element['url']}\">{element['url']}</a>"
case 'emote':
return EMOJI[element['name']]
case "rule":
return "<hr>"
case "mention":
return make_mention(element, mentions)
for i in range(len(elements)): parameters:
e = elements[i]
surrounding = ( s (str) - babycode string
elements[i - 1] if i-1 >= 0 else None,
elements[i + 1] if i+1 < len(elements) else None banned_tags (list) - babycode tags to exclude from being parsed. they will remain as plain text in the transformation.
)
out = out + fold(e, False, surrounding) fragment (bool) - skip adding an html p tag to the first element if it is inline.
return BabycodeParseResult(out, mentions) """
ast = babycode_ast(s, banned_tags)
r = HTMLRenderer(fragment=fragment)
return r.render(ast)
def babycode_to_rssxml(s: str, banned_tags=[], fragment=False) -> str:
"""
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)

10
app/lib/render_atom.py Normal file
View File

@@ -0,0 +1,10 @@
from flask import make_response, render_template, request
def render_atom_template(template, *args, **kwargs):
injects = {
**kwargs,
'__current_page': request.url,
}
r = make_response(render_template(template, *args, **injects))
r.mimetype = 'application/xml'
return r

View File

@@ -43,6 +43,7 @@ MIGRATIONS = [
add_signature_format, add_signature_format,
create_default_bookmark_collections, create_default_bookmark_collections,
add_display_name, add_display_name,
'ALTER TABLE "post_history" ADD COLUMN "content_rss" STRING DEFAULT NULL'
] ]
def run_migrations(): def run_migrations():

View File

@@ -230,6 +230,38 @@ class Topics(Model):
return db.query(q, self.id, per_page, (page - 1) * per_page) return db.query(q, self.id, per_page, (page - 1) * per_page)
def get_threads_with_op_rss(self):
q = """
SELECT
threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
users.username AS started_by,
users.display_name AS started_by_display_name,
ph.content_rss AS original_post_content,
posts.id AS original_post_id
FROM
threads
JOIN users ON users.id = threads.user_id
JOIN (
SELECT
posts.thread_id,
posts.id,
posts.user_id,
posts.created_at,
posts.current_revision_id,
ROW_NUMBER() OVER (PARTITION BY posts.thread_id ORDER BY posts.created_at ASC) AS rn
FROM
posts
) posts ON posts.thread_id = threads.id AND posts.rn = 1
JOIN
post_history ph ON ph.id = posts.current_revision_id
JOIN
users u ON u.id = posts.user_id
WHERE
threads.topic_id = ?
ORDER BY threads.created_at DESC"""
return db.query(q, self.id)
class Threads(Model): class Threads(Model):
table = "threads" table = "threads"
@@ -238,6 +270,10 @@ class Threads(Model):
q = Posts.FULL_POSTS_QUERY + " WHERE posts.thread_id = ? ORDER BY posts.created_at ASC LIMIT ? OFFSET ?" q = Posts.FULL_POSTS_QUERY + " WHERE posts.thread_id = ? ORDER BY posts.created_at ASC LIMIT ? OFFSET ?"
return db.query(q, self.id, limit, offset) return db.query(q, self.id, limit, offset)
def get_posts_rss(self):
q = Posts.FULL_POSTS_QUERY + ' WHERE posts.thread_id = ?'
return db.query(q, self.id)
def locked(self): def locked(self):
return bool(self.is_locked) return bool(self.is_locked)
@@ -265,7 +301,7 @@ class Posts(Model):
SELECT SELECT
posts.id, posts.created_at, posts.id, posts.created_at,
post_history.content, post_history.edited_at, post_history.content, post_history.edited_at, post_history.content_rss,
users.username, users.display_name, users.status, users.username, users.display_name, users.status,
avatars.file_path AS avatar_path, posts.thread_id, avatars.file_path AS avatar_path, posts.thread_id,
users.id AS user_id, post_history.original_markup, users.id AS user_id, post_history.original_markup,

View File

@@ -1,7 +1,18 @@
from flask import Blueprint, redirect, url_for, render_template from flask import Blueprint, redirect, url_for, render_template
from app import cache
from datetime import datetime
bp = Blueprint("app", __name__, url_prefix = "/") bp = Blueprint("app", __name__, url_prefix = "/")
@bp.route("/") @bp.route("/")
def index(): def index():
return redirect(url_for("topics.all_topics")) return redirect(url_for("topics.all_topics"))
@bp.route("/cache-test")
def cache_test():
test_value = cache.get('test')
if test_value is None:
test_value = 'cached_value_' + str(datetime.now())
cache.set('test', test_value, timeout=10)
return f"set cache: {test_value}"
return f"cached: {test_value}"

View File

@@ -2,7 +2,7 @@ from flask import (
Blueprint, redirect, url_for, flash, render_template, request Blueprint, redirect, url_for, flash, render_template, request
) )
from .users import login_required, get_active_user from .users import login_required, get_active_user
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION from ..lib.babycode import babycode_to_html, babycode_to_rssxml, BABYCODE_VERSION
from ..constants import InfoboxKind from ..constants import InfoboxKind
from ..db import db from ..db import db
from ..models import Posts, PostHistory, Threads, Topics, Mentions from ..models import Posts, PostHistory, Threads, Topics, Mentions
@@ -12,6 +12,7 @@ bp = Blueprint("posts", __name__, url_prefix = "/post")
def create_post(thread_id, user_id, content, markup_language="babycode"): def create_post(thread_id, user_id, content, markup_language="babycode"):
parsed_content = babycode_to_html(content) parsed_content = babycode_to_html(content)
parsed_rss = babycode_to_rssxml(content)
with db.transaction(): with db.transaction():
post = Posts.create({ post = Posts.create({
"thread_id": thread_id, "thread_id": thread_id,
@@ -22,6 +23,7 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
revision = PostHistory.create({ revision = PostHistory.create({
"post_id": post.id, "post_id": post.id,
"content": parsed_content.result, "content": parsed_content.result,
"content_rss": parsed_rss,
"is_initial_revision": True, "is_initial_revision": True,
"original_markup": content, "original_markup": content,
"markup_language": markup_language, "markup_language": markup_language,
@@ -43,11 +45,13 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
def update_post(post_id, new_content, markup_language='babycode'): def update_post(post_id, new_content, markup_language='babycode'):
parsed_content = babycode_to_html(new_content) parsed_content = babycode_to_html(new_content)
parsed_rss = babycode_to_rssxml(new_content)
with db.transaction(): with db.transaction():
post = Posts.find({'id': post_id}) post = Posts.find({'id': post_id})
new_revision = PostHistory.create({ new_revision = PostHistory.create({
'post_id': post.id, 'post_id': post.id,
'content': parsed_content.result, 'content': parsed_content.result,
"content_rss": parsed_rss,
'is_initial_revision': False, 'is_initial_revision': False,
'original_markup': new_content, 'original_markup': new_content,
'markup_language': markup_language, 'markup_language': markup_language,

View File

@@ -1,31 +1,35 @@
from flask import ( from flask import (
Blueprint, render_template, request, redirect, url_for, flash, Blueprint, render_template, request, redirect, url_for, flash,
abort, abort, current_app,
) )
from .users import login_required, mod_only, get_active_user, is_logged_in from .users import login_required, mod_only, get_active_user, is_logged_in
from ..db import db from ..db import db
from ..models import Threads, Topics, Posts, Subscriptions, Reactions from ..models import Threads, Topics, Posts, Subscriptions, Reactions
from ..constants import InfoboxKind from ..constants import InfoboxKind
from ..lib.render_atom import render_atom_template
from .posts import create_post from .posts import create_post
from slugify import slugify from slugify import slugify
from app import cache
import math import math
import time import time
bp = Blueprint("threads", __name__, url_prefix = "/threads/") bp = Blueprint("threads", __name__, url_prefix = "/threads/")
def get_post_url(post_id, _anchor=False): def get_post_url(post_id, _anchor=False, external=False):
post = Posts.find({'id': post_id}) post = Posts.find({'id': post_id})
if not post: if not post:
return "" return ""
thread = Threads.find({'id': post.thread_id}) thread = Threads.find({'id': post.thread_id})
res = url_for('threads.thread', slug=thread.slug, after=post_id) anchor = None if not _anchor else f'post-{post_id}'
if not _anchor:
return res
return f"{res}#post-{post_id}" return url_for('threads.thread', slug=thread.slug, after=post_id, _external=external, _anchor=anchor)
# if not _anchor:
# return res
# return f"{res}#post-{post_id}"
@bp.get("/<slug>") @bp.get("/<slug>")
@@ -80,9 +84,25 @@ def thread(slug):
is_subscribed = is_subscribed, is_subscribed = is_subscribed,
Reactions = Reactions, Reactions = Reactions,
unread_count = unread_count, unread_count = unread_count,
__feedlink = url_for('.thread_atom', slug=slug, _external=True),
__feedtitle = f'replies to {thread.title}',
) )
@bp.get("/<slug>/feed.atom")
@cache.cached(timeout=5 * 60, unless=lambda: current_app.config.get('DEBUG', False))
def thread_atom(slug):
thread = Threads.find({"slug": slug})
if not thread:
abort(404) # TODO throw an atom friendly 404
return
topic = Topics.find({'id': thread.topic_id})
posts = thread.get_posts_rss()
return render_atom_template('threads/thread.atom', thread=thread, topic=topic, posts=posts, get_post_url=get_post_url)
@bp.post("/<slug>") @bp.post("/<slug>")
@login_required @login_required
def reply(slug): def reply(slug):

View File

@@ -1,11 +1,13 @@
from flask import ( from flask import (
Blueprint, render_template, request, redirect, url_for, flash, session, Blueprint, render_template, request, redirect, url_for, flash, session,
abort, abort, current_app
) )
from .users import login_required, mod_only, get_active_user, is_logged_in from .users import login_required, mod_only, get_active_user, is_logged_in
from ..models import Users, Topics, Threads, Subscriptions from ..models import Users, Topics, Threads, Subscriptions
from ..constants import InfoboxKind from ..constants import InfoboxKind
from ..lib.render_atom import render_atom_template
from slugify import slugify from slugify import slugify
from app import cache
import time import time
import math import math
@@ -80,10 +82,27 @@ def topic(slug):
subscriptions = subscriptions, subscriptions = subscriptions,
topic = target_topic, topic = target_topic,
current_page = page, current_page = page,
page_count = page_count page_count = page_count,
__feedlink = url_for('.topic_atom', slug=slug, _external=True),
__feedtitle = f'latest threads in {target_topic.name}',
) )
@bp.get('/<slug>/feed.atom')
@cache.cached(timeout=10 * 60, unless=lambda: current_app.config.get('DEBUG', False))
def topic_atom(slug):
target_topic = Topics.find({
"slug": slug
})
if not target_topic:
abort(404) # TODO throw an atom friendly 404
return
threads_list = target_topic.get_threads_with_op_rss()
return render_atom_template('topics/topic.atom', threads_list=threads_list, target_topic=target_topic)
@bp.get("/<slug>/edit") @bp.get("/<slug>/edit")
@login_required @login_required
@mod_only(".topic", slug = lambda slug: slug) @mod_only(".topic", slug = lambda slug: slug)

20
app/templates/base.atom Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
{% if self.title() %}
<title>{% block title %}{% endblock %}</title>
{% else %}
<title>{{ config.SITE_NAME }}</title>
{% endif %}
{% if self.feed_updated() %}
<updated>{% block feed_updated %}{% endblock %}</updated>
{% else %}
<updated>{{ get_time_now() | iso8601 }}</updated>
{% endif %}
<id>{{ __current_page }}</id>
<link rel="self" href="{{ __current_page }}" />
<link href="{% block canonical_link %}{% endblock %}" />
{% if self.feed_author() %}
<author>{% block feed_author %}{% endblock %}</author>
{% endif %}
{% block content %}{% endblock %}
</feed>

View File

@@ -10,7 +10,10 @@
{% endif %} {% endif %}
<link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}"> <link rel="stylesheet" href="{{ ("/static/css/%s.css" % get_prefers_theme()) | cachebust }}">
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<script src="{{ '/static/js/vnd/bitty-7.0.0-rc1.min.js' | cachebust }}" type="module"></script> <script src="{{ '/static/js/vnd/bitty-7.0.0.js' | cachebust }}" type="module"></script>
{% if __feedlink %}
<link rel="alternate" type="application/atom+xml" href="{{ __feedlink }}" title="{{ __feedtitle }}">
{% endif %}
</head> </head>
<body> <body>
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}"> <bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }}">

View File

@@ -53,3 +53,9 @@
<path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
{%- endmacro %} {%- endmacro %}
{% macro icn_rss(width=24) %}
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 11C9.41828 11 13 14.5817 13 19M5 5C12.732 5 19 11.268 19 19M7 18C7 18.5523 6.55228 19 6 19C5.44772 19 5 18.5523 5 18C5 17.4477 5.44772 17 6 17C6.55228 17 7 17.4477 7 18Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{% endmacro %}

View File

@@ -1,4 +1,4 @@
{% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone %} {% from 'common/icons.html' import icn_image, icn_spoiler, icn_info, icn_lock, icn_warn, icn_error, icn_bookmark, icn_megaphone, icn_rss %}
{% macro pager(current_page, page_count) %} {% macro pager(current_page, page_count) %}
{% set left_start = [1, current_page - 5] | max %} {% set left_start = [1, current_page - 5] | max %}
{% set right_end = [page_count, current_page + 5] | min %} {% set right_end = [page_count, current_page + 5] | min %}
@@ -359,3 +359,11 @@
</div> </div>
</bitty-7-0> </bitty-7-0>
{% endmacro %} {% endmacro %}
{% macro rss_html_content(html) %}
<content type="html">{{ html }}</content>
{% endmacro %}
{% macro rss_button(feed) %}
<a class="linkbutton contain-svg inline icon rss-button" href="{{feed}}" title="it&#39;s actually atom, don&#39;t tell anyone &#59;&#41;">{{ icn_rss(20) }} Subscribe via RSS</a>
{% endmacro %}

View File

@@ -95,12 +95,12 @@
<h2 id="paragraph-rules">Paragraph rules</h2> <h2 id="paragraph-rules">Paragraph rules</h2>
<p>Line breaks in babycode work like Markdown: to start a new paragraph, use two line breaks:</p> <p>Line breaks in babycode work like Markdown: to start a new paragraph, use two line breaks:</p>
{{ '[code]paragraph 1\n\nparagraph 2[/code]' | babycode | safe }} {{ '[code]paragraph 1\n\nparagraph 2[/code]' | babycode | safe }}
Will produce:<br> Will produce:
{{ 'paragraph 1\n\nparagraph 2' | babycode | safe }} {{ 'paragraph 1\n\nparagraph 2' | babycode(true) | safe }}
<p>To break a line without starting a new paragraph, end a line with two spaces:</p> <p>To break a line without starting a new paragraph, end a line with two spaces:</p>
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }} {{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
That will produce:<br> That will produce:<br>
{{ 'paragraph 1 \nstill paragraph 1' | babycode | safe }} {{ 'paragraph 1 \nstill paragraph 1' | babycode(true) | safe }}
<p>Additionally, the following tags will break into a new paragraph:</p> <p>Additionally, the following tags will break into a new paragraph:</p>
<ul> <ul>
<li><code class="inline-code">[code]</code> (code block, not inline);</li> <li><code class="inline-code">[code]</code> (code block, not inline);</li>
@@ -113,21 +113,20 @@
</section> </section>
<section class="guide-section"> <section class="guide-section">
<h2 id="links">Links</h2> <h2 id="links">Links</h2>
<p>Loose links (starting with http:// or https://) will automatically get converted to clickable links. To add a label to a link, use<br><code class="inline-code">[url=https://example.com]Link label[/url]</code>:<br> <p>Loose links (starting with http:// or https://) will automatically get converted to clickable links. To add a label to a link, use <code class="inline-code">[url=https://example.com]Link label[/url]</code>:<br>
<a href="https://example.com">Link label</a></p> <a href="https://example.com">Link label</a></p>
</section> </section>
<section class="guide-section"> <section class="guide-section">
<h2 id="attaching-an-image">Attaching an image</h2> <h2 id="attaching-an-image">Attaching an image</h2>
<p>To add an image to your post, use the <code class="inline-code">[img]</code> tag:<br> <p>To add an image to your post, use the <code class="inline-code">[img]</code> tag:<br>
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code> <code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code></p>
{{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }} {{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }}
</p>
<p>The attribute is the image URL. The text inside the tag will become the image's alt text.</p> <p>The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p> <p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p>
<p>Multiple images attached to a post can be clicked to open a dialog to view them.</p> <p>Multiple images attached to a post can be clicked to open a dialog to view them.</p>
</section> </section>
<section class="guide-section"> <section class="guide-section">
<h2 id="adding-code-blocks">Adding code blocks</h2> <h2 id="adding-code-blocks">Adding code blocūs</h2>
{% set code = 'func _ready() -> void:\n\tprint("hello world!")' %} {% set code = 'func _ready() -> void:\n\tprint("hello world!")' %}
<p>There are two kinds of code blocks recognized by babycode: inline and block. Inline code blocks do not break a paragraph. They can be added with <code class="inline-code">[code]your code here[/code]</code>. As long as there are no line breaks inside the code block, it is considered inline. If there are any, it will produce this:</p> <p>There are two kinds of code blocks recognized by babycode: inline and block. Inline code blocks do not break a paragraph. They can be added with <code class="inline-code">[code]your code here[/code]</code>. As long as there are no line breaks inside the code block, it is considered inline. If there are any, it will produce this:</p>
{{ ('[code]%s[/code]' % code) | babycode | safe }} {{ ('[code]%s[/code]' % code) | babycode | safe }}
@@ -168,9 +167,18 @@
<a class="mention display me" href="#mentions" title="@your-username">Your display name</a> <a class="mention display me" href="#mentions" title="@your-username">Your display name</a>
<p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p> <p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p>
</section> </section>
<section class="guide-section">
{% set hr_example = "some section\n---\nanother section" %}
<h2 id="rule">Horizontal rules</h2>
<p>The special <code class="inline-code">---</code> markup inserts a horizontal separator, also known as a horizontal rule:</p>
{{ ("[code]%s[/code]" % hr_example) | babycode | safe }}
Will become
{{ hr_example | babycode(true) | safe}}
<p>Horizontal rules will always break the current paragraph.</p>
</section>
<section class="guide-section"> <section class="guide-section">
<h2 id="void-tags">Void tags</h2> <h2 id="void-tags">Void tags</h2>
<p>The special void tags <code class="inline-code">[lb]</code>, <code class="inline-code">[rb]</code>, and <code class="inline-code">[@]</code> will appear as the literal characters <code class="inline-code">[</code>, <code class="inline-code">]</code>, and <code class="inline-code">@</code> respectively. Unlike other tags, they are self-contained and have no closing equivalent.</p> <p>The special void tags <code class="inline-code">[lb]</code>, <code class="inline-code">[rb]</code>, <code class="inline-code">[d]</code> and <code class="inline-code">[at]</code> will appear as the literal characters <code class="inline-code">[</code>, <code class="inline-code">]</code>, <code class="inline-code">-</code>, and <code class="inline-code">@</code> respectively. Unlike other tags, they are self-contained and have no closing equivalent.</p>
<ul class="guide-list"> <ul class="guide-list">
{% set lbrb = "[color=red]This text will be red[/color]\n\n[lb]color=red[rb]This text won't be red[lb]/color[rb]" %} {% set lbrb = "[color=red]This text will be red[/color]\n\n[lb]color=red[rb]This text won't be red[lb]/color[rb]" %}
<li><code class="inline-code">[lb]</code> and <code class="inline-code">[rb]</code> allow you to use square brackets without them being interpreted as Babycode: <li><code class="inline-code">[lb]</code> and <code class="inline-code">[rb]</code> allow you to use square brackets without them being interpreted as Babycode:
@@ -178,7 +186,8 @@
Will result in:<br> Will result in:<br>
{{ lbrb | babycode | safe }} {{ lbrb | babycode | safe }}
</li> </li>
<li>The <code class="inline-code">[@]</code> tag allows you to use the @ symbol without it being turned into a mention.</li> <li>The <code class="inline-code">[at]</code> tag allows you to use the @ symbol without it being turned into a mention.</li>
<li>The <code class="inline-code">[d]</code> tag allows you to use the - (dash) symbol without it being turned into a rule.</li>
</ul> </ul>
</section> </section>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends 'base.atom' %}
{% from 'common/macros.html' import rss_html_content %}
{% block title %}replies to {{thread.title}}{% endblock %}
{% block canonical_link %}{{url_for('threads.thread', slug=thread.slug, _external=true)}}{% endblock %}
{% block content %}
{% for post in posts %}
{% set post_url = get_post_url(post.id, _anchor=true, external=true) %}
<entry>
<title>Re: {{ thread.title }}</title>
<link href="{{ post_url }}"/>
<id>{{ post_url }}</id>
<updated>{{ post.edited_at | iso8601 }}</updated>
{{rss_html_content(post.content_rss)}}
<author>
<name>{{ post.display_name }} @{{ post.username }}</name>
</author>
</entry>
{% endfor %}
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% from 'common/macros.html' import pager, babycode_editor_form, full_post, bookmark_button %} {% from 'common/macros.html' import pager, babycode_editor_form, full_post, bookmark_button, rss_button %}
{% from 'common/icons.html' import icn_bookmark %} {% from 'common/icons.html' import icn_bookmark %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ thread.title }}{% endblock %} {% block title %}{{ thread.title }}{% endblock %}
@@ -53,6 +53,7 @@
<input class="warn" type="submit" value="Move thread"> <input class="warn" type="submit" value="Move thread">
</form> </form>
{% endif %} {% endif %}
{{ rss_button(url_for('threads.thread_atom', slug=thread.slug)) }}
</div> </div>
</nav> </nav>
{% for post in posts %} {% for post in posts %}

View File

@@ -0,0 +1,20 @@
{% extends 'base.atom' %}
{% from 'common/macros.html' import rss_html_content %}
{% block title %}latest threads in {{target_topic.name}}{% endblock %}
{% block canonical_link %}{{url_for('topics.topic', slug=target_topic.slug, _external=true)}}{% endblock %}
{% block content %}
<subtitle>{{ target_topic.description }}</subtitle>
{% for thread in threads_list %}
<entry>
<title>[new thread] {{ thread.title | escape }}</title>
<link href="{{ url_for('threads.thread', slug=thread.slug, _external=true)}}" />
<link rel="replies" type="application/atom+xml" href="{{ url_for('threads.thread_atom', slug=thread.slug, _external=true)}}" />
<id>{{ url_for('threads.thread', slug=thread.slug, _external=true)}}</id>
<updated>{{ thread.created_at | iso8601 }}</updated>
{{rss_html_content(thread.original_post_content)}}
<author>
<name>{{thread.started_by_display_name}} @{{ thread.started_by }}</name>
</author>
</entry>
{% endfor %}
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% from 'common/macros.html' import pager, timestamp, motd %} {% from 'common/macros.html' import pager, timestamp, motd, rss_button %}
{% from 'common/icons.html' import icn_lock, icn_sticky %} {% from 'common/icons.html' import icn_lock, icn_sticky %}
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}browsing topic {{ topic['name'] }}{% endblock %} {% block title %}browsing topic {{ topic['name'] }}{% endblock %}
@@ -6,7 +6,7 @@
<nav class="darkbg"> <nav class="darkbg">
<h1 class="thread-title">All threads in "{{topic['name']}}"</h1> <h1 class="thread-title">All threads in "{{topic['name']}}"</h1>
<span>{{topic['description']}}</span> <span>{{topic['description']}}</span>
<div> <div class="thread-actions">
{% if active_user %} {% if active_user %}
{% if not (topic['is_locked']) | int or active_user.is_mod() %} {% if not (topic['is_locked']) | int or active_user.is_mod() %}
<a class="linkbutton" href="{{ url_for("threads.create", topic_id=topic['id']) }}">New thread</a> <a class="linkbutton" href="{{ url_for("threads.create", topic_id=topic['id']) }}">New thread</a>
@@ -18,6 +18,7 @@
<input class="warn" type="submit" id="lock" value="{{"Unlock topic" if topic['is_locked'] else "Lock topic"}}"> <input class="warn" type="submit" id="lock" value="{{"Unlock topic" if topic['is_locked'] else "Lock topic"}}">
</form> </form>
<button type="button" class="critical" id="topic-delete-dialog-open">Delete</button> <button type="button" class="critical" id="topic-delete-dialog-open">Delete</button>
{{ rss_button(url_for('topics.topic_atom', slug=topic.slug)) }}
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -18,6 +18,16 @@
<span>1MB maximum size. Avatar will be cropped to square.</span> <span>1MB maximum size. Avatar will be cropped to square.</span>
</form> </form>
</fieldset> </fieldset>
<fieldset class="hfc">
<legend>Change password</legend>
<form method='post' action='{{ url_for('users.change_password', username=active_user.username) }}'>
<label for="new_password">New password</label><br>
<input type="password" id="new_password" name="new_password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<label for="new_password2">Confirm new password</label><br>
<input type="password" id="new_password2" name="new_password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<input class="warn" type="submit" value="Change password">
</form>
</fieldset>
<fieldset class="hfc"> <fieldset class="hfc">
<legend>Personalization</legend> <legend>Personalization</legend>
<form method='post'> <form method='post'>
@@ -44,16 +54,6 @@
</form> </form>
</fieldset> </fieldset>
<fieldset class="hfc"> <fieldset class="hfc">
<legend>Change password</legend>
<form method='post' action='{{ url_for('users.change_password', username=active_user.username) }}'>
<label for="new_password">New password</label><br>
<input type="password" id="new_password" name="new_password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<label for="new_password2">Confirm new password</label><br>
<input type="password" id="new_password2" name="new_password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<input class="warn" type="submit" value="Change password">
</form>
</fieldset>
<fieldset>
<legend>Badges</legend> <legend>Badges</legend>
<a href="{{ url_for('guides.guide_page', category='user-guides', slug='settings', _anchor='badges')}}">Badges help</a> <a href="{{ url_for('guides.guide_page', category='user-guides', slug='settings', _anchor='badges')}}">Badges help</a>
<bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorForm" data-listeners="click input submit change"> <bitty-7-0 data-connect="{{ '/static/js/bitties/pyrom-bitty.js' | cachebust }} BadgeEditorForm" data-listeners="click input submit change">

View File

@@ -44,13 +44,13 @@
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton { .reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default; cursor: default;
font-size: 1rem; font-size: 1em;
font-family: "Cadman", sans-serif; font-family: "Cadman", sans-serif;
text-decoration: none; text-decoration: none;
border: 1px solid black; border: 1px solid black;
border-radius: 4px; border-radius: 4px;
padding: 5px 20px; padding: 5px 20px;
margin: 10px 0; margin: 5px 0;
} }
body { body {
@@ -60,6 +60,11 @@ body {
color: black; color: black;
} }
@media (orientation: portrait) {
body {
margin: 20px 0;
}
}
:where(a:link) { :where(a:link) {
color: #c11c1c; color: #c11c1c;
} }
@@ -69,7 +74,7 @@ body {
} }
.big { .big {
font-size: 1.8rem; font-size: 1.8em;
} }
#topnav { #topnav {
@@ -114,7 +119,7 @@ body {
.site-title { .site-title {
font-family: "site-title"; font-family: "site-title";
font-size: 3rem; font-size: 3em;
margin: 0 20px; margin: 0 20px;
text-decoration: none; text-decoration: none;
color: black; color: black;
@@ -122,14 +127,15 @@ body {
.thread-title { .thread-title {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
.thread-actions { .thread-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 0 5px;
flex-wrap: wrap;
} }
.post { .post {
@@ -222,7 +228,7 @@ code {
pre code { pre code {
display: block; display: block;
background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126); background-color: rgb(38.5714173228, 40.9237007874, 35.6762992126);
font-size: 1rem; font-size: 1em;
color: white; color: white;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
@@ -606,7 +612,7 @@ pre code { /* Literal.Number.Integer.Long */ }
display: inline-block; display: inline-block;
margin: 4px; margin: 4px;
border-radius: 4px; border-radius: 4px;
font-size: 1rem; font-size: 1em;
white-space: pre; white-space: pre;
} }
@@ -799,7 +805,7 @@ input[type=file]::file-selector-button {
} }
p { p {
margin: 15px 0; margin: 10px 0;
} }
.pagebutton { .pagebutton {
@@ -859,7 +865,7 @@ input[type=text], input[type=password], textarea, select {
resize: vertical; resize: vertical;
color: black; color: black;
background-color: rgb(217.8, 225.6, 208.2); background-color: rgb(217.8, 225.6, 208.2);
font-size: 100%; font-size: 1em;
font-family: inherit; font-family: inherit;
} }
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus { input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
@@ -1123,7 +1129,7 @@ textarea {
} }
.babycode-preview-errors-container { .babycode-preview-errors-container {
font-size: 0.8rem; font-size: 0.8em;
} }
.tab-button { .tab-button {
@@ -1269,9 +1275,6 @@ ul.horizontal li, ol.horizontal li {
padding: 5px 10px; padding: 5px 10px;
min-width: 36px; min-width: 36px;
} }
.babycode-button > * {
font-size: 1rem;
}
.quote-popover { .quote-popover {
position: absolute; position: absolute;
@@ -1455,7 +1458,7 @@ a.mention:hover, a.mention:visited:hover {
display: grid; display: grid;
gap: 10px; gap: 10px;
--grid-item-max-width: calc((100% - 10px) / 2); --grid-item-max-width: calc((100% - 10px) / 2);
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr)); grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
} }
.settings-grid fieldset { .settings-grid fieldset {
border: 1px solid white; border: 1px solid white;
@@ -1523,3 +1526,16 @@ img.badge-button {
justify-content: center; justify-content: center;
gap: 5px; gap: 5px;
} }
.rss-button {
background-color: #fba668;
color: black;
}
.rss-button:hover {
background-color: rgb(251.8, 183.8, 134.2);
color: black;
}
.rss-button:active {
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
color: black;
}

View File

@@ -44,13 +44,13 @@
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton { .reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default; cursor: default;
font-size: 1rem; font-size: 1em;
font-family: "Cadman", sans-serif; font-family: "Cadman", sans-serif;
text-decoration: none; text-decoration: none;
border: 1px solid black; border: 1px solid black;
border-radius: 8px; border-radius: 8px;
padding: 5px 20px; padding: 5px 20px;
margin: 10px 0; margin: 5px 0;
} }
body { body {
@@ -60,6 +60,11 @@ body {
color: #e6e6e6; color: #e6e6e6;
} }
@media (orientation: portrait) {
body {
margin: 20px 0;
}
}
:where(a:link) { :where(a:link) {
color: #e87fe1; color: #e87fe1;
} }
@@ -69,7 +74,7 @@ body {
} }
.big { .big {
font-size: 1.8rem; font-size: 1.8em;
} }
#topnav { #topnav {
@@ -114,7 +119,7 @@ body {
.site-title { .site-title {
font-family: "site-title"; font-family: "site-title";
font-size: 3rem; font-size: 3em;
margin: 0 20px; margin: 0 20px;
text-decoration: none; text-decoration: none;
color: white; color: white;
@@ -122,14 +127,15 @@ body {
.thread-title { .thread-title {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
.thread-actions { .thread-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 0 5px;
flex-wrap: wrap;
} }
.post { .post {
@@ -222,7 +228,7 @@ code {
pre code { pre code {
display: block; display: block;
background-color: #302731; background-color: #302731;
font-size: 1rem; font-size: 1em;
color: white; color: white;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
@@ -606,7 +612,7 @@ pre code { /* Literal.Number.Integer.Long */ }
display: inline-block; display: inline-block;
margin: 4px; margin: 4px;
border-radius: 8px; border-radius: 8px;
font-size: 1rem; font-size: 1em;
white-space: pre; white-space: pre;
} }
@@ -799,7 +805,7 @@ input[type=file]::file-selector-button {
} }
p { p {
margin: 15px 0; margin: 10px 0;
} }
.pagebutton { .pagebutton {
@@ -859,7 +865,7 @@ input[type=text], input[type=password], textarea, select {
resize: vertical; resize: vertical;
color: #e6e6e6; color: #e6e6e6;
background-color: #371e37; background-color: #371e37;
font-size: 100%; font-size: 1em;
font-family: inherit; font-family: inherit;
} }
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus { input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
@@ -1123,7 +1129,7 @@ textarea {
} }
.babycode-preview-errors-container { .babycode-preview-errors-container {
font-size: 0.8rem; font-size: 0.8em;
} }
.tab-button { .tab-button {
@@ -1269,9 +1275,6 @@ ul.horizontal li, ol.horizontal li {
padding: 5px 10px; padding: 5px 10px;
min-width: 36px; min-width: 36px;
} }
.babycode-button > * {
font-size: 1rem;
}
.quote-popover { .quote-popover {
position: absolute; position: absolute;
@@ -1455,7 +1458,7 @@ a.mention:hover, a.mention:visited:hover {
display: grid; display: grid;
gap: 10px; gap: 10px;
--grid-item-max-width: calc((100% - 10px) / 2); --grid-item-max-width: calc((100% - 10px) / 2);
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr)); grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
} }
.settings-grid fieldset { .settings-grid fieldset {
border: 1px solid black; border: 1px solid black;
@@ -1524,6 +1527,19 @@ img.badge-button {
gap: 5px; gap: 5px;
} }
.rss-button {
background-color: #fba668;
color: black;
}
.rss-button:hover {
background-color: rgb(251.8, 183.8, 134.2);
color: black;
}
.rss-button:active {
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
color: black;
}
#topnav { #topnav {
margin-bottom: 10px; margin-bottom: 10px;
border: 10px solid rgb(40, 40, 40); border: 10px solid rgb(40, 40, 40);

View File

@@ -44,13 +44,13 @@
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton { .reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default; cursor: default;
font-size: 1rem; font-size: 1em;
font-family: "Cadman", sans-serif; font-family: "Cadman", sans-serif;
text-decoration: none; text-decoration: none;
border: 1px solid black; border: 1px solid black;
border-radius: 16px; border-radius: 16px;
padding: 8px 12px; padding: 8px 12px;
margin: 6px 0; margin: 3px 0;
} }
body { body {
@@ -60,6 +60,11 @@ body {
color: black; color: black;
} }
@media (orientation: portrait) {
body {
margin: 12px 0;
}
}
:where(a:link) { :where(a:link) {
color: black; color: black;
} }
@@ -69,7 +74,7 @@ body {
} }
.big { .big {
font-size: 1.8rem; font-size: 1.8em;
} }
#topnav { #topnav {
@@ -114,7 +119,7 @@ body {
.site-title { .site-title {
font-family: "site-title"; font-family: "site-title";
font-size: 3rem; font-size: 3em;
margin: 0 12px; margin: 0 12px;
text-decoration: none; text-decoration: none;
color: black; color: black;
@@ -122,14 +127,15 @@ body {
.thread-title { .thread-title {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
.thread-actions { .thread-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 3px; gap: 0 3px;
flex-wrap: wrap;
} }
.post { .post {
@@ -222,7 +228,7 @@ code {
pre code { pre code {
display: block; display: block;
background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607); background-color: rgb(41.7051685393, 28.2759550562, 24.6948314607);
font-size: 1rem; font-size: 1em;
color: white; color: white;
border-bottom-right-radius: 16px; border-bottom-right-radius: 16px;
border-bottom-left-radius: 16px; border-bottom-left-radius: 16px;
@@ -606,7 +612,7 @@ pre code { /* Literal.Number.Integer.Long */ }
display: inline-block; display: inline-block;
margin: 4px; margin: 4px;
border-radius: 16px; border-radius: 16px;
font-size: 1rem; font-size: 1em;
white-space: pre; white-space: pre;
} }
@@ -799,7 +805,7 @@ input[type=file]::file-selector-button {
} }
p { p {
margin: 8px 0; margin: 6px 0;
} }
.pagebutton { .pagebutton {
@@ -859,7 +865,7 @@ input[type=text], input[type=password], textarea, select {
resize: vertical; resize: vertical;
color: black; color: black;
background-color: rgb(247.2, 175.2, 156); background-color: rgb(247.2, 175.2, 156);
font-size: 100%; font-size: 1em;
font-family: inherit; font-family: inherit;
} }
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus { input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
@@ -1123,7 +1129,7 @@ textarea {
} }
.babycode-preview-errors-container { .babycode-preview-errors-container {
font-size: 0.8rem; font-size: 0.8em;
} }
.tab-button { .tab-button {
@@ -1269,9 +1275,6 @@ ul.horizontal li, ol.horizontal li {
padding: 3px 6px; padding: 3px 6px;
min-width: 36px; min-width: 36px;
} }
.babycode-button > * {
font-size: 1rem;
}
.quote-popover { .quote-popover {
position: absolute; position: absolute;
@@ -1455,7 +1458,7 @@ a.mention:hover, a.mention:visited:hover {
display: grid; display: grid;
gap: 6px; gap: 6px;
--grid-item-max-width: calc((100% - 6px) / 2); --grid-item-max-width: calc((100% - 6px) / 2);
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr)); grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
} }
.settings-grid fieldset { .settings-grid fieldset {
border: 1px solid white; border: 1px solid white;
@@ -1524,6 +1527,19 @@ img.badge-button {
gap: 3px; gap: 3px;
} }
.rss-button {
background-color: #fba668;
color: black;
}
.rss-button:hover {
background-color: rgb(251.8, 183.8, 134.2);
color: black;
}
.rss-button:active {
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
color: black;
}
#topnav { #topnav {
border-top-left-radius: 16px; border-top-left-radius: 16px;
border-top-right-radius: 16px; border-top-right-radius: 16px;

View File

@@ -44,13 +44,13 @@
.reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton { .reaction-button.active, .tab-button, .currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
cursor: default; cursor: default;
font-size: 1rem; font-size: 1em;
font-family: "Cadman", sans-serif; font-family: "Cadman", sans-serif;
text-decoration: none; text-decoration: none;
border: 1px solid black; border: 1px solid black;
border-radius: 4px; border-radius: 4px;
padding: 5px 20px; padding: 5px 20px;
margin: 10px 0; margin: 5px 0;
} }
body { body {
@@ -60,6 +60,11 @@ body {
color: black; color: black;
} }
@media (orientation: portrait) {
body {
margin: 20px 0;
}
}
:where(a:link) { :where(a:link) {
color: #711579; color: #711579;
} }
@@ -69,7 +74,7 @@ body {
} }
.big { .big {
font-size: 1.8rem; font-size: 1.8em;
} }
#topnav { #topnav {
@@ -114,7 +119,7 @@ body {
.site-title { .site-title {
font-family: "site-title"; font-family: "site-title";
font-size: 3rem; font-size: 3em;
margin: 0 20px; margin: 0 20px;
text-decoration: none; text-decoration: none;
color: black; color: black;
@@ -122,14 +127,15 @@ body {
.thread-title { .thread-title {
margin: 0; margin: 0;
font-size: 1.5rem; font-size: 1.5em;
font-weight: bold; font-weight: bold;
} }
.thread-actions { .thread-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 0 5px;
flex-wrap: wrap;
} }
.post { .post {
@@ -222,7 +228,7 @@ code {
pre code { pre code {
display: block; display: block;
background-color: rgb(37.9418181818, 42.3818181818, 50.8581818182); background-color: rgb(37.9418181818, 42.3818181818, 50.8581818182);
font-size: 1rem; font-size: 1em;
color: white; color: white;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
@@ -606,7 +612,7 @@ pre code { /* Literal.Number.Integer.Long */ }
display: inline-block; display: inline-block;
margin: 4px; margin: 4px;
border-radius: 4px; border-radius: 4px;
font-size: 1rem; font-size: 1em;
white-space: pre; white-space: pre;
} }
@@ -799,7 +805,7 @@ input[type=file]::file-selector-button {
} }
p { p {
margin: 15px 0; margin: 10px 0;
} }
.pagebutton { .pagebutton {
@@ -859,7 +865,7 @@ input[type=text], input[type=password], textarea, select {
resize: vertical; resize: vertical;
color: black; color: black;
background-color: rgb(225.6, 232.2, 244.8); background-color: rgb(225.6, 232.2, 244.8);
font-size: 100%; font-size: 1em;
font-family: inherit; font-family: inherit;
} }
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus { input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
@@ -1123,7 +1129,7 @@ textarea {
} }
.babycode-preview-errors-container { .babycode-preview-errors-container {
font-size: 0.8rem; font-size: 0.8em;
} }
.tab-button { .tab-button {
@@ -1269,9 +1275,6 @@ ul.horizontal li, ol.horizontal li {
padding: 5px 10px; padding: 5px 10px;
min-width: 36px; min-width: 36px;
} }
.babycode-button > * {
font-size: 1rem;
}
.quote-popover { .quote-popover {
position: absolute; position: absolute;
@@ -1455,7 +1458,7 @@ a.mention:hover, a.mention:visited:hover {
display: grid; display: grid;
gap: 10px; gap: 10px;
--grid-item-max-width: calc((100% - 10px) / 2); --grid-item-max-width: calc((100% - 10px) / 2);
grid-template-columns: repeat(auto-fill, minmax(max(400px, var(--grid-item-max-width)), 1fr)); grid-template-columns: repeat(auto-fill, minmax(max(600px, var(--grid-item-max-width)), 1fr));
} }
.settings-grid fieldset { .settings-grid fieldset {
border: 1px solid white; border: 1px solid white;
@@ -1523,3 +1526,16 @@ img.badge-button {
justify-content: center; justify-content: center;
gap: 5px; gap: 5px;
} }
.rss-button {
background-color: #fba668;
color: black;
}
.rss-button:hover {
background-color: rgb(251.8, 183.8, 134.2);
color: black;
}
.rss-button:active {
background-color: rgb(186.8501612903, 155.5098387097, 132.6498387097);
color: black;
}

View File

@@ -7,12 +7,12 @@ const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))}
export default class { export default class {
async showBookmarkMenu(ev, el) { async showBookmarkMenu(ev, el) {
if ((el.sender.dataset.bookmarkId === el.ds('bookmarkId')) && el.childElementCount === 0) { if ((ev.sender.dataset.bookmarkId === el.prop('bookmarkId')) && el.childElementCount === 0) {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
'id': el.sender.dataset.conceptId, 'id': ev.sender.dataset.conceptId,
'require_reload': el.dataset.requireReload, 'require_reload': el.dataset.requireReload,
}); });
const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${el.sender.dataset.bookmarkType}?${searchParams}`; const bookmarkMenuHref = `${bookmarkMenuHrefTemplate}/${ev.sender.dataset.bookmarkType}?${searchParams}`;
const res = await this.api.getHTML(bookmarkMenuHref); const res = await this.api.getHTML(bookmarkMenuHref);
if (res.error) { if (res.error) {
return; return;
@@ -50,9 +50,9 @@ export default class {
} }
selectBookmarkCollection(ev, el) { selectBookmarkCollection(ev, el) {
const clicked = el.sender; const clicked = ev.sender;
if (el.sender === el) { if (ev.sender === el) {
if (clicked.classList.contains('selected')) { if (clicked.classList.contains('selected')) {
clicked.classList.remove('selected'); clicked.classList.remove('selected');
} else { } else {
@@ -64,7 +64,7 @@ export default class {
} }
async saveBookmarks(ev, el) { async saveBookmarks(ev, el) {
const bookmarkHref = el.ds('bookmarkEndpoint'); const bookmarkHref = el.prop('bookmarkEndpoint');
const collection = el.querySelector('.bookmark-dropdown-item.selected'); const collection = el.querySelector('.bookmark-dropdown-item.selected');
let data = {}; let data = {};
if (collection) { if (collection) {
@@ -73,7 +73,7 @@ export default class {
data['memo'] = el.querySelector('.bookmark-memo-input').value; data['memo'] = el.querySelector('.bookmark-memo-input').value;
} else { } else {
data['operation'] = 'remove'; data['operation'] = 'remove';
data['collection_id'] = el.ds('originallyContainedIn'); data['collection_id'] = el.prop('originallyContainedIn');
} }
const options = { const options = {
@@ -83,7 +83,7 @@ export default class {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
} }
const requireReload = el.dsInt('requireReload') !== 0; const requireReload = el.propToInt('requireReload') !== 0;
el.remove(); el.remove();
await fetch(bookmarkHref, options); await fetch(bookmarkHref, options);
if (requireReload) { if (requireReload) {
@@ -104,10 +104,10 @@ export default class {
toggleAccordion(ev, el) { toggleAccordion(ev, el) {
const accordion = el; const accordion = el;
const header = accordion.querySelector('.accordion-header'); const header = accordion.querySelector('.accordion-header');
if (!header.contains(el.sender)){ if (!header.contains(ev.sender)){
return; return;
} }
const btn = el.sender; const btn = ev.sender;
const content = el.querySelector('.accordion-content'); const content = el.querySelector('.accordion-content');
// these are all meant to be in sync // these are all meant to be in sync
accordion.classList.toggle('hidden'); accordion.classList.toggle('hidden');
@@ -117,15 +117,15 @@ export default class {
toggleTab(ev, el) { toggleTab(ev, el) {
const tabButtonsContainer = el.querySelector('.tab-buttons'); const tabButtonsContainer = el.querySelector('.tab-buttons');
if (!el.contains(el.sender)) { if (!el.contains(ev.sender)) {
return; return;
} }
if (el.sender.classList.contains('active')) { if (ev.sender.classList.contains('active')) {
return; return;
} }
const targetId = el.senderDs('targetId'); const targetId = ev.sender.prop('targetId');
const contents = el.querySelectorAll('.tab-content'); const contents = el.querySelectorAll('.tab-content');
for (let content of contents) { for (let content of contents) {
if (content.id === targetId) { if (content.id === targetId) {
@@ -145,7 +145,7 @@ export default class {
#previousMarkup = null; #previousMarkup = null;
async babycodePreview(ev, el) { async babycodePreview(ev, el) {
if (el.sender.classList.contains('active')) { if (ev.sender.classList.contains('active')) {
return; return;
} }
@@ -200,9 +200,9 @@ export default class {
} }
insertBabycodeTag(ev, el) { insertBabycodeTag(ev, el) {
const tagStart = el.senderDs('tag'); const tagStart = ev.sender.prop('tag');
const breakLine = 'breakLine' in el.sender.dataset; const breakLine = 'breakLine' in ev.sender.dataset;
const prefill = 'prefill' in el.sender.dataset ? el.sender.dataset.prefill : ''; const prefill = 'prefill' in ev.sender.dataset ? ev.sender.dataset.prefill : '';
const hasAttr = tagStart[tagStart.length - 1] === '='; const hasAttr = tagStart[tagStart.length - 1] === '=';
let tagEnd = tagStart; let tagEnd = tagStart;
@@ -250,13 +250,13 @@ export default class {
} }
addQuote(ev, el) { addQuote(ev, el) {
el.value += el.sender.value; el.value += ev.sender.value;
el.scrollIntoView(); el.scrollIntoView();
el.focus(); el.focus();
} }
convertTimestamps(ev, el) { convertTimestamps(ev, el) {
const timestamp = el.dsInt('utc'); const timestamp = el.propToInt('utc');
if (!isNaN(timestamp)) { if (!isNaN(timestamp)) {
const date = new Date(timestamp * 1000); const date = new Date(timestamp * 1000);
el.textContent = date.toLocaleString(); el.textContent = date.toLocaleString();
@@ -273,7 +273,7 @@ export default class {
this.#currentUsername = userInfo.value.user.username; this.#currentUsername = userInfo.value.user.username;
} }
if (el.ds('username') === this.#currentUsername) { if (el.prop('username') === this.#currentUsername) {
el.classList.add('me'); el.classList.add('me');
} }
} }
@@ -316,18 +316,18 @@ export class BadgeEditorForm {
} }
deleteBadge(ev, el) { deleteBadge(ev, el) {
if (!el.contains(el.sender)) { if (!el.contains(ev.sender)) {
return; return;
} }
el.remove(); el.remove();
this.api.localTrigger('updateBadgeCount'); this.api.localTrigger('updateBadgeCount');
} }
updateBadgeCount(_ev, el) { updateBadgeCount(ev, el) {
const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length; const badgeCount = el.parentNode.parentNode.querySelectorAll('.settings-badge-container').length;
if (el.dsInt('disableIfMax') === 1) { if (el.propToInt('disableIfMax') === 1) {
el.disabled = badgeCount === 10; el.disabled = badgeCount === 10;
} else if (el.dsInt('count') === 1) { } else if (el.propToInt('count') === 1) {
el.textContent = `${badgeCount}/10`; el.textContent = `${badgeCount}/10`;
} }
} }
@@ -364,13 +364,13 @@ export class BadgeEditorBadge {
if (ev.type !== 'change') { if (ev.type !== 'change') {
return; return;
} }
// TODO: el.sender doesn't have a bittyParentBittyId // TODO: ev.sender doesn't have a bittyParent
const selectBittyParent = el.sender.closest('bitty-7-0'); const selectBittyParent = ev.sender.closest('bitty-7-0');
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) { if (el.bittyParent !== selectBittyParent) {
return; return;
} }
if (ev.val === 'custom') { if (ev.value === 'custom') {
if (this.#badgeCustomImageData) { if (this.#badgeCustomImageData) {
el.src = this.#badgeCustomImageData; el.src = this.#badgeCustomImageData;
} else { } else {
@@ -378,7 +378,7 @@ export class BadgeEditorBadge {
} }
return; return;
} }
const option = el.sender.selectedOptions[0]; const option = ev.sender.selectedOptions[0];
el.src = option.dataset.filePath; el.src = option.dataset.filePath;
} }
@@ -386,13 +386,13 @@ export class BadgeEditorBadge {
if (ev.type !== 'change') { if (ev.type !== 'change') {
return; return;
} }
if (el.bittyParentBittyId !== el.sender.bittyParentBittyId) { if (el.bittyParent !== ev.sender.bittyParent) {
return; return;
} }
const file = ev.target.files[0]; const file = ev.target.files[0];
if (file.size >= 1000 * 500) { if (file.size >= 1000 * 500) {
this.api.trigger('badgeErrorSize'); this.api.localTrigger('badgeErrorSize');
this.#badgeCustomImageData = null; this.#badgeCustomImageData = null;
el.removeAttribute('src'); el.removeAttribute('src');
return; return;
@@ -403,14 +403,14 @@ export class BadgeEditorBadge {
reader.onload = async e => { reader.onload = async e => {
const dimsValid = await validateBase64Img(e.target.result); const dimsValid = await validateBase64Img(e.target.result);
if (!dimsValid) { if (!dimsValid) {
this.api.trigger('badgeErrorDim'); this.api.localTrigger('badgeErrorDim');
this.#badgeCustomImageData = null; this.#badgeCustomImageData = null;
el.removeAttribute('src'); el.removeAttribute('src');
return; return;
} }
this.#badgeCustomImageData = e.target.result; this.#badgeCustomImageData = e.target.result;
el.src = this.#badgeCustomImageData; el.src = this.#badgeCustomImageData;
this.api.trigger('badgeHideErrors'); this.api.localTrigger('badgeHideErrors');
} }
reader.readAsDataURL(file); reader.readAsDataURL(file);
@@ -420,13 +420,13 @@ export class BadgeEditorBadge {
if (ev.type !== 'change') { if (ev.type !== 'change') {
return; return;
} }
// TODO: el.sender doesn't have a bittyParentBittyId // TODO: ev.sender doesn't have a bittyParent
const selectBittyParent = el.sender.closest('bitty-7-0'); const selectBittyParent = ev.sender.closest('bitty-7-0');
if (el.bittyParentBittyId !== selectBittyParent.dataset.bittyid) { if (el.bittyParent !== selectBittyParent) {
return; return;
} }
const filePicker = el.querySelector('input[type=file]'); const filePicker = el.querySelector('input[type=file]');
if (ev.val === 'custom') { if (ev.value === 'custom') {
el.classList.remove('hidden'); el.classList.remove('hidden');
if (filePicker.dataset.validity) { if (filePicker.dataset.validity) {
filePicker.setCustomValidity(filePicker.dataset.validity); filePicker.setCustomValidity(filePicker.dataset.validity);
@@ -440,37 +440,28 @@ export class BadgeEditorBadge {
} }
openBadgeFilePicker(ev, el) { openBadgeFilePicker(ev, el) {
// TODO: el.sender doesn't have a bittyParentBittyId // TODO: ev.sender doesn't have a bittyParent
if (el.sender.parentNode !== el.parentNode) { if (ev.sender.parentNode !== el.parentNode) {
return; return;
} }
el.click(); el.click();
} }
badgeErrorSize(_ev, el) { badgeErrorSize(ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
const validity = "Image can't be over 500KB." const validity = "Image can't be over 500KB."
el.dataset.validity = validity; el.dataset.validity = validity;
el.setCustomValidity(validity); el.setCustomValidity(validity);
el.reportValidity(); el.reportValidity();
} }
badgeErrorDim(_ev, el) { badgeErrorDim(ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
const validity = "Image must be exactly 88x31 pixels." const validity = "Image must be exactly 88x31 pixels."
el.dataset.validity = validity; el.dataset.validity = validity;
el.setCustomValidity(validity); el.setCustomValidity(validity);
el.reportValidity(); el.reportValidity();
} }
badgeHideErrors(_ev, el) { badgeHideErrors(ev, el) {
if (el.sender !== el.bittyParent) {
return;
}
delete el.dataset.validity; delete el.dataset.validity;
el.setCustomValidity(''); el.setCustomValidity('');
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,11 @@
argon2-cffi==25.1.0 argon2-cffi==25.1.0
argon2-cffi-bindings==21.2.0 argon2-cffi-bindings==21.2.0
blinker==1.9.0 blinker==1.9.0
cachelib==0.13.0
cffi==1.17.1 cffi==1.17.1
click==8.2.1 click==8.2.1
Flask==3.1.1 Flask==3.1.1
Flask-Caching==2.3.1
itsdangerous==2.2.0 itsdangerous==2.2.0
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.2

View File

@@ -115,10 +115,10 @@ $DEFAULT_BORDER_RADIUS: 4px !default;
$button_border: $DEFAULT_BORDER !default; $button_border: $DEFAULT_BORDER !default;
$button_padding: $SMALL_PADDING $BIG_PADDING !default; $button_padding: $SMALL_PADDING $BIG_PADDING !default;
$button_border_radius: $DEFAULT_BORDER_RADIUS !default; $button_border_radius: $DEFAULT_BORDER_RADIUS !default;
$button_margin: $MEDIUM_PADDING $ZERO_PADDING !default; $button_margin: $SMALL_PADDING $ZERO_PADDING !default;
%button-base { %button-base {
cursor: default; cursor: default;
font-size: 1rem; font-size: 1em;
font-family: "Cadman", sans-serif; font-family: "Cadman", sans-serif;
text-decoration: none; text-decoration: none;
border: $button_border; border: $button_border;
@@ -178,6 +178,13 @@ body {
color: $DEFAULT_FONT_COLOR; color: $DEFAULT_FONT_COLOR;
} }
$body_portrait_margin: $BIG_PADDING $ZERO_PADDING !default;
@media (orientation: portrait) {
body {
margin: $body_portrait_margin;
}
}
$link_color: #c11c1c !default; $link_color: #c11c1c !default;
$link_color_visited: #730c0c !default; $link_color_visited: #730c0c !default;
:where(a:link){ :where(a:link){
@@ -188,7 +195,7 @@ $link_color_visited: #730c0c !default;
} }
.big { .big {
font-size: 1.8rem; font-size: 1.8em;
} }
$topnav_color: $ACCENT_COLOR !default; $topnav_color: $ACCENT_COLOR !default;
@@ -224,7 +231,7 @@ $user_actions_gap: $MEDIUM_BIG_PADDING !default;
} }
$site_title_margin: $ZERO_PADDING $BIG_PADDING !default; $site_title_margin: $ZERO_PADDING $BIG_PADDING !default;
$site_title_size: 3rem !default; $site_title_size: 3em !default;
$site_title_color: $DEFAULT_FONT_COLOR !default; $site_title_color: $DEFAULT_FONT_COLOR !default;
.site-title { .site-title {
font-family: "site-title"; font-family: "site-title";
@@ -235,18 +242,19 @@ $site_title_color: $DEFAULT_FONT_COLOR !default;
} }
$thread_title_margin: $ZERO_PADDING !default; $thread_title_margin: $ZERO_PADDING !default;
$thread_title_size: 1.5rem !default; $thread_title_size: 1.5em !default;
.thread-title { .thread-title {
margin: $thread_title_margin; margin: $thread_title_margin;
font-size: $thread_title_size; font-size: $thread_title_size;
font-weight: bold; font-weight: bold;
} }
$thread_actions_gap: $SMALL_PADDING !default; $thread_actions_gap: $ZERO_PADDING $SMALL_PADDING !default;
.thread-actions { .thread-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: $thread_actions_gap; gap: $thread_actions_gap;
flex-wrap: wrap;
} }
$post_usercard_width: 230px !default; $post_usercard_width: 230px !default;
@@ -372,7 +380,7 @@ $code_border_left: $MEDIUM_PADDING solid $LIGHT_2 !default;
pre code { pre code {
display: block; display: block;
background-color: $code_background_color; background-color: $code_background_color;
font-size: 1rem; font-size: 1em;
color: $code_font_color; color: $code_font_color;
border-bottom-right-radius: $code_border_radius; border-bottom-right-radius: $code_border_radius;
border-bottom-left-radius: $code_border_radius; border-bottom-left-radius: $code_border_radius;
@@ -496,7 +504,7 @@ $inline_code_padding: $SMALL_PADDING $MEDIUM_PADDING !default;
display: inline-block; display: inline-block;
margin: $inline_code_margin; margin: $inline_code_margin;
border-radius: $inline_code_border_radius; border-radius: $inline_code_border_radius;
font-size: 1rem; font-size: 1em;
white-space: pre; white-space: pre;
} }
@@ -647,7 +655,7 @@ input[type="file"]::file-selector-button {
margin: $MEDIUM_PADDING; margin: $MEDIUM_PADDING;
} }
$para_margin: $MEDIUM_BIG_PADDING $ZERO_PADDING !default; $para_margin: $MEDIUM_PADDING $ZERO_PADDING !default;
p { p {
margin: $para_margin; margin: $para_margin;
} }
@@ -700,7 +708,7 @@ input[type="text"], input[type="password"], textarea, select {
resize: vertical; resize: vertical;
color: $text_input_font_color; color: $text_input_font_color;
background-color: $text_input_background; background-color: $text_input_background;
font-size: 100%; font-size: 1em;
font-family: inherit; font-family: inherit;
&:focus { &:focus {
@@ -1041,7 +1049,7 @@ $post_editing_context_margin: $BIG_PADDING $ZERO_PADDING !default;
} }
.babycode-preview-errors-container { .babycode-preview-errors-container {
font-size: 0.8rem; font-size: 0.8em;
} }
$tab_button_color: $BUTTON_COLOR !default; $tab_button_color: $BUTTON_COLOR !default;
@@ -1199,10 +1207,6 @@ $babycode_button_min_width: $accordion_button_size !default;
.babycode-button { .babycode-button {
padding: $babycode_button_padding; padding: $babycode_button_padding;
min-width: $babycode_button_min_width; min-width: $babycode_button_min_width;
&> * {
font-size: 1rem;
}
} }
$quote_fragment_background_color: #00000080 !default; $quote_fragment_background_color: #00000080 !default;
@@ -1415,7 +1419,7 @@ a.mention, a.mention:visited {
} }
$settings_grid_gap: $MEDIUM_PADDING !default; $settings_grid_gap: $MEDIUM_PADDING !default;
$settings_grid_item_min_width: 400px !default; $settings_grid_item_min_width: 600px !default;
$settings_grid_fieldset_border: 1px solid $DEFAULT_FONT_COLOR_INVERSE !default; $settings_grid_fieldset_border: 1px solid $DEFAULT_FONT_COLOR_INVERSE !default;
$settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default; $settings_grid_fieldset_border_radius: $DEFAULT_BORDER_RADIUS !default;
.settings-grid { .settings-grid {
@@ -1511,3 +1515,24 @@ $badges_container_gap: $SMALL_PADDING !default;
justify-content: center; justify-content: center;
gap: $badges_container_gap; gap: $badges_container_gap;
} }
$rss_button_color: #fba668 !default;
$rss_button_color_hover: color.scale($rss_button_color, $lightness: 20%) !default;
$rss_button_color_active: color.scale($rss_button_color, $lightness: -10%, $saturation: -70%) !default;
$rss_button_font_color: black !default;
$rss_button_font_color_hover: black !default;
$rss_button_font_color_active: black !default;
.rss-button {
background-color: $rss_button_color;
color: $rss_button_font_color;
&:hover {
background-color: $rss_button_color_hover;
color: $rss_button_font_color_hover;
}
&:active {
background-color: $rss_button_color_active;
color: $rss_button_font_color_active;
}
}