diff --git a/app/main.py b/app/main.py index 6ea8008..65d539b 100644 --- a/app/main.py +++ b/app/main.py @@ -16,6 +16,7 @@ from typing import Optional import docker import requests +import mistune from fastapi import Depends, FastAPI, File, Form, HTTPException, Query, Request, UploadFile, status from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles @@ -299,34 +300,16 @@ def normalize_web_target(url: str) -> str: return f"http://{raw}" +_md = mistune.create_markdown( + escape=True, + plugins=["strikethrough", "table", "task_lists"], +) + def format_service_comment(raw_text: str) -> Markup: raw = (raw_text or "").replace("\r\n", "\n").replace("\r", "\n").strip() if not raw: return Markup("") - escaped = str(escape(raw)) - # Support pasted/plain markdown-like bold fragments. - escaped = re.sub(r"\*\*(.+?)\*\*", r"\1", escaped, flags=re.DOTALL) - # Allow a small safe subset of pasted HTML tags. - replacements = { - "<b>": "", - "</b>": "", - "<strong>": "", - "</strong>": "", - "<i>": "", - "</i>": "", - "<em>": "", - "</em>": "", - "<u>": "", - "</u>": "", - "<br>": "
", - "<br/>": "
", - "<br />": "
", - } - for src, dst in replacements.items(): - escaped = escaped.replace(src, dst) - escaped = escaped.replace("\n", "
") - return Markup(escaped) - + return Markup(_md(raw)) def parse_rdp_target(target: str) -> dict: raw = (target or "").strip() diff --git a/app/requirements.txt b/app/requirements.txt index c7e391d..1cd98c5 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -7,3 +7,4 @@ jinja2==3.1.6 passlib[argon2]==1.7.4 docker==7.1.0 itsdangerous==2.2.0 +mistune==3.1.3 diff --git a/app/static/style.css b/app/static/style.css index 4eecba8..1e587d0 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -781,3 +781,34 @@ button { color: rgba(240, 248, 255, 0.95); text-shadow: 0 2px 10px rgba(9, 44, 72, 0.45); } + +/* Markdown inside service card comments */ +.tile-comment p { margin: 0 0 0.4em; } +.tile-comment p:last-child { margin-bottom: 0; } +.tile-comment ul, .tile-comment ol { margin: 0.3em 0 0.4em 1.2em; padding: 0; } +.tile-comment li { margin-bottom: 0.15em; } +.tile-comment h1,.tile-comment h2,.tile-comment h3, +.tile-comment h4,.tile-comment h5,.tile-comment h6 { + font-size: 0.85em; font-weight: 700; margin: 0.4em 0 0.2em; +} +.tile-comment code { + font-family: monospace; font-size: 0.88em; + background: rgba(0,0,0,.06); border-radius: 3px; padding: 0.1em 0.3em; +} +.tile-comment pre { + background: rgba(0,0,0,.06); border-radius: 4px; + padding: 0.4em 0.6em; overflow-x: auto; font-size: 0.82em; +} +.tile-comment pre code { background: none; padding: 0; } +.tile-comment blockquote { + border-left: 3px solid #c7d9e8; margin: 0.3em 0 0.3em 0; + padding-left: 0.6em; color: #5a7a90; +} +.tile-comment a { color: #1668a6; text-decoration: underline; } +.tile-comment table { border-collapse: collapse; font-size: 0.82em; margin: 0.3em 0; } +.tile-comment th, .tile-comment td { + border: 1px solid #c7d9e8; padding: 0.2em 0.5em; +} +.tile-comment th { background: #eaf2fb; font-weight: 700; } +.tile-comment del { text-decoration: line-through; color: #7a9aaf; } +.tile-comment input[type=checkbox] { margin-right: 0.3em; }