jinja_utils.py
Python script, ASCII text executable
1""" 2This module provides filters and other utilities for the Jinja2 templating engine. 3 4Roundabout - git hosting for everyone <https://roundabout-host.com> 5Copyright (C) 2023-2025 Roundabout developers <root@roundabout-host.com> 6 7This program is free software: you can redistribute it and/or modify 8it under the terms of the GNU Affero General Public License as published by 9the Free Software Foundation, either version 3 of the License, or 10(at your option) any later version. 11 12This program is distributed in the hope that it will be useful, 13but WITHOUT ANY WARRANTY; without even the implied warranty of 14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15GNU Affero General Public License for more details. 16 17You should have received a copy of the GNU Affero General Public License 18along with this program. If not, see <http://www.gnu.org/licenses/>. 19""" 20 21from app import app, _, n_ 22import flask 23from datetime import datetime 24import markdown 25from markupsafe import Markup 26from urllib.parse import urlencode 27 28 29@app.template_global() 30def modify_query(**new_values): 31args = flask.request.args.copy() 32 33for key, value in new_values.items(): 34args[key] = value 35 36return f"{flask.request.path}?{urlencode(args)}" 37 38 39@app.template_filter("split") 40def split(value: str, separator=" ", maxsplit: int = -1): 41return value.split(separator, maxsplit) 42 43 44@app.template_filter("splitlines") 45def splitlines(value: str, keepends: bool = False): 46return value.splitlines(keepends=keepends) 47 48 49@app.template_filter("lstrip") 50def lstrip(value: str, characters=None): 51return value.lstrip(characters) 52 53 54@app.template_filter("rstrip") 55def rstrip(value: str, characters=None): 56return value.rstrip(characters) 57 58 59@app.template_filter("strftime") 60def strftime(value: datetime, syntax: str): 61# Split the strftime syntax into placeholders and literals. 62placeholders = { 63"%a", "%A", "%w", "%d", "%-d", "%b", "%B", "%m", "%-m", "%y", "%Y", 64"%H", "%-H", "%I", "%-I", "%p", "%M", "%-M", "%S", "%-S", "%f", "%z", 65"%Z", "%j", "%-j", "%U", "%W", "%c", "%x", "%X", "%%", "%G", "%u", "%V", 66"%-V", "%s", "%:z", "%e" 67} 68 69tokens = [] 70i = 0 71length = len(syntax) 72while i < length: 73if syntax[i] == "%": 74if i+1 < length and syntax[i:i + 2] in placeholders: 75tokens.append(syntax[i:i + 2]) 76i += 2 77elif i+1 < length and syntax[i:i + 3] in placeholders: 78tokens.append(syntax[i:i + 3]) 79i += 3 80else: 81tokens.append(syntax[i]) 82i += 1 83else: 84tokens.append(syntax[i]) 85i += 1 86 87new_tokens = [] 88 89for token in tokens: 90match token: 91case "%A": 92# Translate the full weekday name. 93weekday_number = value.weekday() 94weekday_names = [ 95_("Monday"), 96_("Tuesday"), 97_("Wednesday"), 98_("Thursday"), 99_("Friday"), 100_("Saturday"), 101_("Sunday") 102] 103new_tokens.append(weekday_names[weekday_number]) 104case "%a": 105# Translate the abbreviated weekday name. 106weekday_number = value.weekday() 107weekday_names = [ 108_("Mon"), 109_("Tue"), 110_("Wed"), 111_("Thu"), 112_("Fri"), 113_("Sat"), 114_("Sun") 115] 116new_tokens.append(weekday_names[weekday_number]) 117case "%B": 118# Translate the full month name. 119month_number = value.month 120month_names = [ 121_("January"), 122_("February"), 123_("March"), 124_("April"), 125_("May"), 126_("June"), 127_("July"), 128_("August"), 129_("September"), 130_("October"), 131_("November"), 132_("December") 133] 134new_tokens.append(month_names[month_number - 1]) 135case "%b": 136# Translate the abbreviated month name. 137month_number = value.month 138month_names = [ 139_("Jan"), 140_("Feb"), 141_("Mar"), 142_("Apr"), 143_("May (short)"), 144_("Jun"), 145_("Jul"), 146_("Aug"), 147_("Sep"), 148_("Oct"), 149_("Nov"), 150_("Dec") 151] 152new_tokens.append(month_names[month_number - 1]) 153case "%p": 154# Translate the AM/PM indicator. 155if value.hour < 12: 156new_tokens.append(_("am")) 157else: 158new_tokens.append(_("pm")) 159case _: 160new_tokens.append(token) 161 162syntax = "".join([value.strftime(token) if token in placeholders else token for token in new_tokens]) 163 164return value.strftime(syntax) 165 166 167@app.template_filter("unixtime") 168def unixtime(value: datetime): 169return round(value.timestamp()) 170 171 172@app.template_filter("decode") 173def decode(value: bytes, codec: str = "UTF-8", errors: str = "strict"): 174return value.decode(codec, errors) 175 176 177@app.template_filter("markdown") 178def parse_markdown(value: str): 179return Markup(markdown.make_html(markdown.tokenise(value))) 180 181 182@app.template_filter("inline_markdown") 183def parse_inline_markdown(value: str): 184return Markup(markdown.make_html(markdown.parse_line(value))) 185 186 187@app.template_filter("parse_diff_location") 188def parse_diff_location(value: str): 189header = value.split("@@")[1].strip() 190return [tuple(int(j) for j in i.lstrip("-+").split(",")) for i in header.split(" ")] 191 192 193@app.template_filter("harvester_protection") 194def harvester_protection(value): 195return "".join(f"&#x{ord(char):x};" for char in value) 196 197 198@app.template_filter("sort") 199def sort(value): 200return sorted(value) 201 202@app.template_filter("profile_url") 203def profile_url(value): 204if "@" in value: 205host = value.split("@")[1] 206user = value.split("@")[0] 207return f"http://{host}/users/{user}" 208else: 209return f"/{value}" 210