/* catcrafts.net Copyright (C) 2026 Catcrafts The source code of this website is made available for viewing purposes only. No permission is granted to copy, modify, distribute, or create derivative works. */ // The Markdown renderer, which is the newest place untrusted text becomes // markup — post bodies are fetched from someone else's server, so every one of // these assertions is ultimately about the same thing: nothing in a body can // escape into the document. The structural cases are here too, because a parser // that silently drops a construct loses content invisibly. import std; import Catcrafts.Shared; using namespace Catcrafts; namespace { int failures = 0; void Check(bool ok, std::string_view what, std::string_view got = {}) { if (ok) return; ++failures; std::println(std::cerr, "FAIL: {}{}{}", what, got.empty() ? "" : " got: ", got); } void CheckEq(const Html::SafeHtml& actual, std::string_view expected, std::string_view what) { Check(actual.View() == expected, what, actual.View()); } } // namespace int main() { auto md = [](std::string_view text, std::span media = {}) { return Markdown::Render(text, media); }; // ── the guarantee ───────────────────────────────────────────────── CheckEq(md(""), "

<script>alert(1)</script>

", "md: html is text, never markup"); CheckEq(md("![x](javascript:alert(1))"), R"(
x
)", "md: javascript: image source neutralised"); CheckEq(md("[x](javascript:alert(1))"), R"(

x

)", "md: javascript: link neutralised"); CheckEq(md("a \" b & c"), "

a " b & c

", "md: quotes and ampersands escaped"); // A code span is verbatim text, and verbatim is exactly where an escaper // is most often forgotten. CheckEq(md("``"), "

<b>

", "md: code span escaped"); // ── blocks ──────────────────────────────────────────────────────── CheckEq(md(""), "", "md: empty body renders nothing"); CheckEq(md("plain text"), "

plain text

", "md: paragraph"); // Demotion by one: the page h1 is the post title, so a body's own top-level // heading is a section within it. CheckEq(md("# Heading"), "

Heading

", "md: h1 demoted to h2"); CheckEq(md("### Heading"), "

Heading

", "md: h3 demoted to h4"); CheckEq(md("#nothashtag"), "

#nothashtag

", "md: # without a space is not a heading"); CheckEq(md("> quoted"), R"(

quoted

)", "md: blockquote"); // The quoted lines are re-parsed, so a multi-paragraph quote keeps its // paragraphs instead of collapsing into one run-on line. CheckEq(md("> one\n>\n> two"), R"(

one

two

)", "md: blockquote keeps its paragraphs"); CheckEq(md("- a\n- b"), R"()", "md: unordered list"); CheckEq(md("1. a\n2. b"), R"(
  1. a
  2. b
)", "md: ordered list"); // A list resumed after an interrupting paragraph continues its numbering. // Without the start attribute the mini-guide in one of these posts renders // as steps 1-4 followed by steps 1, 2, 3. CheckEq(md("5. e"), R"(
  1. e
)", "md: ordered list keeps the number it announced"); // Blank lines between items are spacing, not seven one-item lists. CheckEq(md("1. a\n\n2. b"), R"(
  1. a
  2. b
)", "md: blank line inside a list does not split it"); CheckEq(md("---"), "
", "md: thematic break"); CheckEq(md("- - -"), "
", "md: spaced rule is not a one-item list"); // Whitespace in pasted terminal output is the content. CheckEq(md("```\n a\tb\n```"), "
  a\tb\n
", "md: fenced code is verbatim"); // An unterminated fence must not swallow the document into nothing. Check(md("```\nx").View().find("x") != std::string_view::npos, "md: unterminated fence still renders its content"); // ── inline ──────────────────────────────────────────────────────── CheckEq(md("**bold**"), "

bold

", "md: strong"); CheckEq(md("*em*"), "

em

", "md: emphasis"); CheckEq(md("2 * 3 * 4"), "

2 * 3 * 4

", "md: spaced asterisks stay literal"); // Underscores are deliberately inert: these posts paste kernel symbol // names into prose, and italicising half of one is worse than not // italicising a word that used the underscore form. CheckEq(md("kworker/u16:8-qc_ufs_qos_swq"), "

kworker/u16:8-qc_ufs_qos_swq

", "md: underscores are not emphasis"); CheckEq(md("\\*literal\\*"), "

*literal*

", "md: backslash escape"); CheckEq(md("[label](https://x.example/y)"), R"(

label

)", "md: link"); // Bare addresses are pasted constantly in these posts; leaving them inert // would strip most of the outbound value out of the page. CheckEq(md("see https://x.example/y"), R"(

see https://x.example/y

)", "md: bare URL autolinked"); // ── embedded media ──────────────────────────────────────────────── // A paragraph that is nothing but images becomes the same media block the // cards use, rather than a

of pictures. CheckEq(md("![a](/media/x.webp)"), R"(

a
)", "md: image-only paragraph is a media block"); Check(md("text ![a](/media/x.webp)").View().starts_with("

text media; PostMedia img; img.src = "/media/x.webp"; img.kind = "image"; img.avif = "/media/x.avif"; img.fallback = "/media/x.png"; img.width = 800; img.height = 600; media.push_back(img); PostMedia vid; vid.src = "/media/v.mp4"; vid.kind = "video"; vid.poster = "/media/v.poster.webp"; vid.fallback = "/media/v.h264.mp4"; vid.width = 1080; vid.height = 1920; media.push_back(vid); const auto out = md("![a](/media/x.webp)", media); Check(out.View().find(R"(width="800" height="600")") != std::string_view::npos, "md: inline image carries its dimensions", out.View()); // Routed through :Media, so a body image gets the same format ladder a // card image does rather than a second, plainer implementation. Check(out.View().find(R"()") != std::string_view::npos && out.View().find(R"(src="/media/x.png")") != std::string_view::npos, "md: inline image gets the avif/png ladder", out.View()); // An inline video gets the same treatment a headline one does, // fallback source and all. const auto vout = md("![](/media/v.mp4)", media); Check(vout.View().find(R"(poster="/media/v.poster.webp")") != std::string_view::npos && vout.View().find("codecs=av01") != std::string_view::npos && vout.View().find(R"( > > > > > > > deep").View().empty(), "md: over-deep nesting terminates"); if (failures != 0) { std::println(std::cerr, "{} check(s) failed", failures); return 1; } return 0; }