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);