Style editor Markdown with .markdown-body and add ::: design blocks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tleiningerandClaude Opus 5.5 committed 2026-10-01 12:52:51 +02:00
1 parent ca0b3c311c
commit 5eb36ad3e9
12 files changed
+683 -17

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
using Elternbeirat.Web.Shared;
namespace Elternbeirat.Web.Tests;
/// <summary>
/// Unit tests for the Markdown pipeline behind every editor-written body. Pure logic,
/// no PocketBase needed.
/// </summary>
public class MarkdownTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
public void Empty_input_renders_nothing(string? markdown) =>
Markdown.ToHtml(markdown).ShouldBe("");
[Fact]
public void Custom_container_renders_a_div_with_its_name_as_class()
{
var html = Markdown.ToHtml("::: kennzahlen\n- **seit 1975** Gründung\n:::");
html.ShouldContain("<div class=\"kennzahlen\">");
html.ShouldContain("<strong>seit 1975</strong>");
}
[Fact]
public void Unknown_container_renders_its_content_as_normal_text()
{
// A typo in the block name must cost the styling, never the text.
var html = Markdown.ToHtml("::: gibtsnicht\nNormaler Text.\n:::");
html.ShouldContain("<p>Normaler Text.</p>");
html.ShouldNotContain(":::");
}
[Fact]
public void Raw_html_is_escaped_not_passed_through()
{
// The body is rendered as a MarkupString; a passed-through <script> would run
// in the visitor's browser.
var html = Markdown.ToHtml("Hallo <script>alert(1)</script>\n\n<div onclick=\"x()\">Box</div>");
html.ShouldNotContain("<script>");
html.ShouldNotContain("<div onclick");
html.ShouldContain("&lt;script&gt;");
}
[Fact]
public void Pipe_table_renders_a_table()
{
var html = Markdown.ToHtml("| Fach | Raum |\n|---|---|\n| Mathe | A1 |");
html.ShouldContain("<table>");
html.ShouldContain("<td>Mathe</td>");
}
[Theory]
[InlineData("[kontakt@example.org](mailto:kontakt@example.org)")]
[InlineData("<mailto:kontakt@example.org>")] // autolink
[InlineData("[Satzung](/dokumente/satzung.pdf) ")] // trailing whitespace ignored
public void Link_alone_in_its_paragraph_is_marked(string markdown) =>
Markdown.ToHtml(markdown).ShouldStartWith($"<p class=\"{Markdown.LoneLinkClass}\">");
[Theory]
[InlineData("Schreiben Sie an [uns](mailto:kontakt@example.org).")]
[InlineData("[eins](/a) und [zwei](/b)")]
[InlineData("![Foto](/bild.jpg)")] // an image is not a link
[InlineData("Nur Text.")]
public void Paragraph_that_is_not_a_single_link_is_not_marked(string markdown) =>
Markdown.ToHtml(markdown).ShouldNotContain(Markdown.LoneLinkClass);
[Fact]
public void Lone_link_in_a_tight_list_marks_the_list_item()
{
// A tight list renders no <p>, so the class has to sit on the <li>.
var html = Markdown.ToHtml("- [Antrag](/dokumente/antrag.pdf)\n- Text mit [Link](/x)");
html.ShouldContain($"<li class=\"{Markdown.LoneLinkClass}\"><a href=\"/dokumente/antrag.pdf\">");
html.ShouldContain("<li>Text mit");
}
}
+5 -1
View File
@@ -236,7 +236,11 @@ public sealed class PocketBaseFixture : IAsyncLifetime
});
await CreateRecordAsync(http, "pages", new
{
title = "Förderverein", body = "# Förderverein", location = "header",
// The body uses a design block, so the smoke tests can follow a ":::"
// container through PocketBase and the page into the rendered HTML.
title = "Förderverein",
body = "Der Förderverein unterstützt die Schule.\n\n::: kennzahlen\n- **seit 1975** Gründung\n:::",
location = "header",
order = 3, slug = "patrons", embed = Array.Empty<string>(), @public = true,
});
// Page record for the FAQ list. Route /faqs is served by FaqList, which reads
+16 -1
View File
@@ -138,7 +138,22 @@ public sealed partial class RouteSmokeTests : IDisposable
var html = await client.GetStringAsync(new Uri("/patrons", UriKind.Relative));
html.ShouldContain("Förderverein"); // the page body, rendered from Markdown
html.ShouldContain("Der Förderverein unterstützt die Schule."); // the page body, rendered from Markdown
}
[Fact]
public async Task Content_page_body_carries_the_markdown_styles_and_blocks()
{
// The editor-content styles in app.css all hang off .markdown-body, and the
// design blocks off the class a ":::" container renders. Assert both reach
// the markup of a real page, so a renamed wrapper or a pipeline without
// custom containers fails here instead of silently unstyling the site.
var client = _factory.CreateClient();
var html = await client.GetStringAsync(new Uri("/patrons", UriKind.Relative));
html.ShouldContain("class=\"markdown-body\"");
html.ShouldContain("<div class=\"kennzahlen\">");
}
[Fact]