using Markdig;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
namespace Elternbeirat.Web.Shared;
///
/// Renders the editors' Markdown (the body, intro and answer
/// fields from PocketBase) to the HTML the pages show.
///
///
/// The pipeline is CommonMark plus exactly two extensions, each there for a reason
/// an editor can see:
///
/// -
///
/// Custom containers (::: name … :::) turn a block
/// into <div class="name">. That is how editors use the
/// design blocks (kennzahlen, aufruf, kacheln,
/// hinweis) without writing HTML. An unknown name just yields a
/// div without styling, so a typo never breaks a page.
///
///
/// -
///
/// Pipe tables: plain CommonMark has no tables at all, so without
/// this the table styles in app.css could never apply.
///
///
///
///
/// Raw HTML in the Markdown is escaped, not passed through
/// ('s DisableHtml). The result is
/// rendered as a MarkupString, so a passed-through <script>
/// would run in the visitor's browser, and the privacy text promises that no
/// program code runs there. Editors style pages with the blocks instead.
///
///
/// After parsing, a paragraph (or list item) that consists of nothing but one
/// link gets the class . Pure CSS cannot tell
/// "a link alone on its line" from "a link inside a sentence" (selectors ignore
/// the text around an element), but the stylesheet needs exactly that to show a
/// lone mailto: link as a button and a lone PDF link as a file card.
///
///
///
public static class Markdown
{
///
/// The class marking a paragraph or list item whose only content is one link.
/// app.css keys the mail button and the PDF card off it.
///
public const string LoneLinkClass = "lone-link";
private static readonly MarkdownPipeline Pipeline =
new MarkdownPipelineBuilder()
.UseCustomContainers()
.UsePipeTables()
.DisableHtml()
.Build();
///
/// Converts a Markdown string to HTML.
///
/// The editor's Markdown; may be .
///
/// The rendered HTML, or an empty string for or empty
/// input, so callers can bind the result directly.
///
///
///
/// Markdown.ToHtml("::: hinweis\nBitte vormerken.\n:::");
/// // <div class="hinweis"><p>Bitte vormerken.</p></div>
///
///
public static string ToHtml(string? markdown) =>
string.IsNullOrEmpty(markdown)
? ""
: Markdig.Markdown.ToHtml(MarkLoneLinks(Markdig.Markdown.Parse(markdown, Pipeline)), Pipeline);
///
/// Adds to every paragraph that holds nothing but
/// one link.
///
/// The parsed document; changed in place.
/// The same , for chaining.
///
/// Inside a list item the class goes on the item, not the paragraph: in a tight
/// list Markdig writes no <p> at all, so a class on the paragraph
/// would be silently dropped.
///
private static MarkdownDocument MarkLoneLinks(MarkdownDocument document)
{
foreach (var paragraph in document.Descendants().Where(IsLoneLink))
{
MarkdownObject target = paragraph.Parent is ListItemBlock { Count: 1 } item ? item : paragraph;
target.GetAttributes().AddClass(LoneLinkClass);
}
return document;
}
///
/// Whether a paragraph's content is exactly one link, ignoring surrounding
/// whitespace and line breaks.
///
/// The paragraph to inspect.
///
/// for one link (inline [text](url) or an
/// autolink <url>, but not an image) and nothing else.
///
private static bool IsLoneLink(ParagraphBlock paragraph) =>
paragraph.Inline?
.Where(inline => !IsBlank(inline))
.ToList() is [LinkInline { IsImage: false } or AutolinkInline];
///
/// Whether an inline carries no visible content (whitespace or a line break).
///
/// The inline to inspect.
/// if it can be ignored when looking for a lone link.
private static bool IsBlank(Inline inline) =>
inline switch
{
LineBreakInline => true,
LiteralInline literal => literal.Content.IsEmptyOrWhitespace(),
_ => false,
};
}