add babycode preview

This commit is contained in:
2026-05-28 10:00:10 +03:00
parent daf205f200
commit 27314f34a5
6 changed files with 104 additions and 7 deletions

View File

@@ -203,12 +203,14 @@ def create_app():
from app.routes.guides import bp as guides_bp from app.routes.guides import bp as guides_bp
from app.routes.mod import bp as mod_bp from app.routes.mod import bp as mod_bp
from app.routes.posts import bp as posts_bp from app.routes.posts import bp as posts_bp
from app.routes.api import bp as api_bp
app.register_blueprint(topics_bp) app.register_blueprint(topics_bp)
app.register_blueprint(threads_bp) app.register_blueprint(threads_bp)
app.register_blueprint(users_bp) app.register_blueprint(users_bp)
app.register_blueprint(guides_bp) app.register_blueprint(guides_bp)
app.register_blueprint(mod_bp) app.register_blueprint(mod_bp)
app.register_blueprint(posts_bp) app.register_blueprint(posts_bp)
app.register_blueprint(api_bp)
with app.app_context(): with app.app_context():
from .schema import create as create_tables from .schema import create as create_tables

View File

@@ -105,6 +105,14 @@ def login_required(view_func):
return view_func(*args, **kwargs) return view_func(*args, **kwargs)
return wrapper return wrapper
def hard_login_required(view_func):
@wraps(view_func)
def wrapper(*args, **kwargs):
if not is_logged_in():
abort(403)
return view_func(*args, **kwargs)
return wrapper
def mod_only(view_func): def mod_only(view_func):
@wraps(view_func) @wraps(view_func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):

21
app/routes/api.py Normal file
View File

@@ -0,0 +1,21 @@
from flask import Blueprint, request
from ..auth import is_logged_in, hard_login_required, get_active_user
from ..lib.babycode import babycode_to_html
from ..models import APIRateLimits
bp = Blueprint('api', __name__, url_prefix='/api/')
@bp.post('/babycode-preview/')
@hard_login_required
def babycode_preview():
user = get_active_user()
if not APIRateLimits.is_allowed(user.id, 'babycode_preview', 5):
return {'error': 'too many requests'}, 429
markup = str(request.json.get('markup', ''))
if not markup:
return {'error': 'markup field missing or invalid type'}, 400
banned_tags = request.json.get('banned_tags', [])
if not isinstance(banned_tags, list):
return {'error': 'banned_tags field is invalid type'}, 400
rendered = babycode_to_html(markup, banned_tags).result
return {'html': rendered}

View File

@@ -72,15 +72,15 @@
</span> </span>
{%- endmacro %} {%- endmacro %}
{% macro tabs(prefix='', labels = []) -%} {% macro tabs(prefix='', labels=[], signal_ss=[], signal_rs=[]) -%}
<div class="tab-container" data-r="setTab"> <div class="tab-container" data-r="setTab">
<div class="tab-bar" role="tablist"> <div class="tab-bar" role="tablist">
{%- for tab_label in labels -%} {%- for tab_label in labels -%}
<button type="button" class="tab-button" role="tab" aria-selected="{{'true' if loop.index0==0 else 'false'}}" id="{{prefix+'-'+(tab_label | lower)+'-tab'}}" aria-controls="{{prefix+'-'+(tab_label | lower)+'-content'}}" disabled data-r="enhance" data-s="setTab" data-tab-index="{{loop.index0}}">{{tab_label}}</button> <button type="button" class="tab-button" role="tab" aria-selected="{{'true' if loop.index0==0 else 'false'}}" id="{{prefix+'-'+(tab_label | lower)+'-tab'}}" aria-controls="{{prefix+'-'+(tab_label | lower)+'-content'}}" disabled data-r="enhance" data-s="setTab {{signal_ss[loop.index0] if signal_ss[loop.index0] else ''}}" data-tab-index="{{loop.index0}}">{{tab_label}}</button>
{%- endfor -%} {%- endfor -%}
</div> </div>
{%- for tab_label in labels -%} {%- for tab_label in labels -%}
<div class="plank secondary-bg even no-shadow tab-content {{'hidden' if loop.index0!=0 else ''}}" role="tabpanel" aria-labelledby="{{prefix+'-'+(tab_label | lower)+'-tab'}}" id="{{prefix+'-'+(tab_label | lower)+'-content'}}"> <div class="plank secondary-bg even no-shadow tab-content {{'hidden' if loop.index0!=0 else ''}}" role="tabpanel" aria-labelledby="{{prefix+'-'+(tab_label | lower)+'-tab'}}" id="{{prefix+'-'+(tab_label | lower)+'-content'}}" data-r="{{signal_rs[loop.index0] if signal_rs[loop.index0] else ''}}">
{{- caller(loop.index0) -}} {{- caller(loop.index0) -}}
</div> </div>
{%- endfor -%} {%- endfor -%}
@@ -94,7 +94,7 @@
id='babycode-content', id='babycode-content',
banned_tags=[] banned_tags=[]
) -%} ) -%}
{%- call(idx) tabs(prefix='babycode', labels=['Write', 'Preview']) -%} {%- call(idx) tabs(prefix='babycode', labels=['Write', 'Preview'], signal_ss=[none, 'babycodePreviewInit'], signal_rs=[none, 'babycodePreview']) -%}
{%- if idx == 0 -%} {%- if idx == 0 -%}
<span class="babycode-editor-controls"> <span class="babycode-editor-controls">
<span class="button-row js-only" data-r="enhance"> <span class="button-row js-only" data-r="enhance">
@@ -109,10 +109,10 @@
<button type="button" title="insert spoiler" class="minimal" data-babycode-tag="spoiler=" data-break-line data-prefill="spoiler content" data-s="insertBabycode">s</button> <button type="button" title="insert spoiler" class="minimal" data-babycode-tag="spoiler=" data-break-line data-prefill="spoiler content" data-s="insertBabycode">s</button>
<button type="button" title="insert emoji&hellip;" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button> <button type="button" title="insert emoji&hellip;" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
</span> </span>
<span class="flex-last">{# stub: char count #}</span> <span class="flex-last js-only" data-r="enhance">stub: char count</span>
</span> </span>
<input type="hidden" name="babycode_banned_tags" id="{{id}}-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}"> <input type="hidden" name="babycode_banned_tags" id="{{id}}-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}} autocomplete="off" maxlength="5000" data-r="insertBabycode">{{ prefill }}</textarea> <textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}} autocomplete="off" maxlength="5000" data-r="insertBabycode babycodePreviewInit" data-banned-tags="{{banned_tags | unique | list | tojson | forceescape}}">{{ prefill }}</textarea>
{%- if banned_tags -%} {%- if banned_tags -%}
<div> <div>
<span>Forbidden tags:</span> <span>Forbidden tags:</span>
@@ -124,6 +124,8 @@
</div> </div>
{%- endif -%} {%- endif -%}
<a href="##">babycode help</a> <a href="##">babycode help</a>
{%- else -%}
<div data-r="showBabycodePreview"></div>
{%- endif -%} {%- endif -%}
{%- endcall -%} {%- endcall -%}
{%- endmacro %} {%- endmacro %}

View File

@@ -17,6 +17,7 @@
<input type="submit" value="Clear MOTD" class="warn"> <input type="submit" value="Clear MOTD" class="warn">
</form> </form>
</fieldset> </fieldset>
{{babycode_editor_component(placeholder='test', id='test-content')}}
<fieldset class="plank" id="sort-topics"> <fieldset class="plank" id="sort-topics">
<legend>Sort topics</legend> <legend>Sort topics</legend>
<p>Drag topics around to reorder them. Press "Save order" when done.</p> <p>Drag topics around to reorder them. Press "Save order" when done.</p>

View File

@@ -1,4 +1,6 @@
export const b = {} export const b = {
babycodePreviewEndpoint: '/api/babycode-preview/',
}
export function setTab(_, sender, el) { export function setTab(_, sender, el) {
if (sender.ariaSelected === 'true') { if (sender.ariaSelected === 'true') {
@@ -78,3 +80,64 @@ export function insertBabycode(_, sender, el) {
} }
el.focus(); el.focus();
} }
export function babycodePreviewInit(ev, sender, el) {
if (!sender.parentNode.parentNode.contains(el)) { // tab container > tab bar > button
return;
}
b.send({ text: el.value, sender: sender, bannedTags: JSON.parse(el.dataset.bannedTags) }, 'babycodePreview');
}
export async function babycodePreview(payload, _, el) {
if (!payload.sender.parentNode.parentNode.contains(el)) {
return;
}
if (!payload.text.trim()) {
b.send({ plain: 'Type something to get a preview.', sender: el }, 'showBabycodePreview');
return;
}
const options = {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
markup: payload.text,
banned_tags: payload.bannedTags,
}),
}
const f = await fetch(b.babycodePreviewEndpoint, options);
try {
if (!f.ok) {
console.error(f);
let msg = '';
switch (f.status) {
case 429:
return;
default:
msg = '(Something went wrong. Try again later.)'
}
b.send({ plain: msg, sender: el }, 'showBabycodePreview');
return;
}
b.send({ ...(await f.json()), sender: el }, 'showBabycodePreview');
} catch (error) {
b.send({ plain: '(Something went wrong. Try again later.)', sender: el }, 'showBabycodePreview');
console.error(error);
return;
}
}
export function showBabycodePreview(payload, _, el) {
if (!payload.sender.parentNode.contains(el)) {
return;
}
if (payload.plain) {
el.innerHTML = `<p>${payload.plain}</p>`;
} else {
el.innerHTML = payload.html;
}
}