Add ::: team block rendering board members as person cards with initials avatars

This commit is contained in:
tleininger committed 2026-10-01 15:55:28 +02:00
1 parent efb6fa9109
commit a5a68ed122
8 files changed
+815 -10

No files matched your search

+46 -8
View File
@@ -1,6 +1,7 @@
using System.Text;
using System.Text.RegularExpressions;
using Markdig;
using Markdig.Extensions.CustomContainers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -20,8 +21,8 @@ namespace Elternbeirat.Web.Shared;
/// <b>Custom containers</b> (<c>::: name</c> … <c>:::</c>) turn a block
/// into <c>&lt;div class="name"&gt;</c>. That is how editors use the
/// design blocks (<c>kennzahlen</c>, <c>aufruf</c>, <c>kacheln</c>,
/// <c>hinweis</c>) without writing HTML. An unknown name just yields a
/// div without styling, so a typo never breaks a page.
/// <c>hinweis</c>, <c>team</c>) without writing HTML. An unknown name
/// just yields a div without styling, so a typo never breaks a page.
/// </description>
/// </item>
/// <item>
@@ -45,8 +46,14 @@ namespace Elternbeirat.Web.Shared;
/// the text around an element), but the stylesheet needs exactly that to show a
/// lone <c>mailto:</c> link as a button and a lone PDF link as a file card.
/// </para>
/// <para>
/// The list items of a <c>::: team</c> block become person cards
/// (<see cref="TeamMember"/>): the initials avatar and the split into name,
/// role and duties need the text itself, which CSS cannot read.
/// </para>
/// </remarks>
/// <seealso cref="IconSet"/>
/// <seealso cref="TeamMember"/>
public static partial class Markdown
{
/// <summary>
@@ -183,22 +190,24 @@ public static partial class Markdown
Markdig.Markdown.Parse(markdown, Pipeline);
/// <summary>
/// Renders a document from <see cref="Parse"/> to HTML, marking lone links on
/// the way, exactly as <see cref="ToHtml"/> does.
/// Renders a document from <see cref="Parse"/> to HTML, building the team cards
/// and marking lone links on the way, exactly as <see cref="ToHtml"/> does.
/// </summary>
/// <param name="document">The parsed document; changed in place.</param>
/// <returns>The rendered HTML.</returns>
internal static string Render(MarkdownDocument document) =>
Markdig.Markdown.ToHtml(MarkLoneLinks(document), Pipeline);
Markdig.Markdown.ToHtml(MarkLoneLinks(BuildTeamCards(document)), Pipeline);
/// <summary>
/// Flattens inline Markdown to the text a reader sees: emphasis and link
/// markup dropped, link text kept, images left out, whitespace collapsed.
/// </summary>
/// <param name="inline">The inline content, e.g. of a paragraph or heading.</param>
/// <param name="inlines">
/// The inline content, e.g. of a paragraph or heading, or a part of it.
/// </param>
/// <returns>The visible text, trimmed.</returns>
internal static string PlainText(ContainerInline inline) =>
Whitespace().Replace(AppendText(new StringBuilder(), inline).ToString(), " ").Trim();
internal static string PlainText(IEnumerable<Inline> inlines) =>
Whitespace().Replace(inlines.Aggregate(new StringBuilder(), AppendText).ToString(), " ").Trim();
/// <summary>
/// Whether an inline carries no visible content (whitespace or a line break).
@@ -279,6 +288,35 @@ public static partial class Markdown
[GeneratedRegex(@"\s+")]
private static partial Regex Whitespace();
/// <summary>
/// Turns the list items of every <c>::: team</c> block into person cards.
/// </summary>
/// <param name="document">The parsed document; changed in place.</param>
/// <returns>The same <paramref name="document"/>, for chaining.</returns>
/// <remarks>
/// Only lists directly in the block count; a nested list inside a card stays a
/// list. <c>::: team gross</c> adds the class
/// <see cref="TeamMember.LargeModifier"/> to the block, because Markdig only
/// turns the block name into a class, not the words after it.
/// </remarks>
private static MarkdownDocument BuildTeamCards(MarkdownDocument document)
{
foreach (var team in document.Descendants<CustomContainer>().Where(TeamMember.IsTeam).ToList())
{
if (TeamMember.IsLarge(team))
{
team.GetAttributes().AddClass(TeamMember.LargeModifier);
}
foreach (var item in team.OfType<ListBlock>().SelectMany(list => list.OfType<ListItemBlock>()))
{
TeamMember.ToCard(item);
}
}
return document;
}
/// <summary>
/// Adds <see cref="LoneLinkClass"/> to every paragraph that holds nothing but
/// one link.