using Elternbeirat.Web.Shared; using Markdig.Syntax; using Markdig.Syntax.Inlines; namespace Elternbeirat.Web.Features.Home; /// /// The head of the home page, taken apart from the home page's Markdown body: /// a large heading, an intro and a row of buttons. /// /// The large heading. /// The intro rendered to HTML, or an empty string. /// The buttons, in the order the editor wrote them; may be empty. /// /// Everything visible here is written by editors in PocketBase. The page only gives /// it the hero layout, so the editor needs no new field and no special syntax: /// /// The first # heading becomes the large heading. /// /// /// A last paragraph made of nothing but links becomes the button row; /// the first link is the primary button. /// /// /// Everything else is the intro text. /// /// /// keeps a static heading when PocketBase cannot be /// reached, so the start page never renders without a title. /// /// /// /// /// HomeIntro.Parse("# Willkommen\n\nSchön, dass Sie da sind.\n\n[Termine](/events) [Kontakt](/contact)"); /// // Title "Willkommen", IntroHtml "<p>Schön, dass Sie da sind.</p>\n", two links /// /// public sealed record HomeIntro(string Title, string IntroHtml, IReadOnlyList Links) { /// /// The heading shown when the body has none or PocketBase is not reachable. /// public const string FallbackTitle = "Elternbeirat der IGMH"; /// /// The head of the home page without any content from PocketBase: the static /// heading, no intro, no buttons. /// public static HomeIntro Fallback { get; } = new(FallbackTitle, "", []); /// /// Takes the home page's body apart into heading, intro and buttons. /// /// The body; may be or empty. /// /// The parts; for an empty body. Without a level-1 /// heading, the title is and the rest is used as is. /// /// /// Only top-level blocks are looked at: a heading or a link paragraph inside a /// list or a ::: block stays part of the intro. Links in the button row /// may be separated by spaces or line breaks; any other text in the paragraph /// (e.g. "oder") keeps it a normal paragraph, because the button row would drop /// that text. /// public static HomeIntro Parse(string? markdown) { if (string.IsNullOrWhiteSpace(markdown)) { return Fallback; } var document = Markdown.Parse(markdown); var heading = document.OfType().FirstOrDefault(block => block.Level == 1); if (heading is not null) { document.Remove(heading); } var links = document.LastChild is ParagraphBlock last ? LinksOnly(last) : []; if (links.Count > 0) { document.RemoveAt(document.Count - 1); } var title = heading?.Inline is { } inline ? Markdown.PlainText(inline) : ""; return new HomeIntro( string.IsNullOrEmpty(title) ? FallbackTitle : title, Markdown.Render(document), links); } /// /// Reads the links of a paragraph that holds nothing but links. /// /// The paragraph to inspect. /// /// Its links, or an empty list if the paragraph contains anything besides links /// and whitespace (including an image). /// private static List LinksOnly(ParagraphBlock paragraph) => paragraph.Inline?.Where(inline => !Markdown.IsBlank(inline)).ToList() is { Count: > 0 } inlines && inlines.All(inline => inline is LinkInline { IsImage: false } or AutolinkInline) ? [.. inlines.Select(ToLink)] : []; /// /// Turns a link inline into a button. /// /// A or . /// The button's target and visible text. /// is neither kind of link. private static HomeLink ToLink(Inline inline) => inline switch { LinkInline link => new HomeLink(link.Url ?? "", Markdown.PlainText(link)), AutolinkInline autolink => new HomeLink(autolink.Url, autolink.Url), _ => throw new ArgumentException("Not a link.", nameof(inline)), }; } /// /// A button in the head of the home page. /// /// The link target as the editor wrote it. /// The visible button text. public sealed record HomeLink(string Href, string Text);