Add post and event cards, shared calendar sheet and per-event ICS; rework past events and next-event card
This commit is contained in:
1 parent
d328adac5c
commit
b2dc033de1
33 files changed
+1150
-206
No files matched your search
@@ -55,6 +55,12 @@ public static partial class Markdown
|
||||
/// </summary>
|
||||
public const string LoneLinkClass = "lone-link";
|
||||
|
||||
/// <summary>
|
||||
/// The default teaser length: about two lines on a card, enough to say what a
|
||||
/// post is about without turning the list into a wall of text.
|
||||
/// </summary>
|
||||
public const int TeaserLength = 160;
|
||||
|
||||
private static readonly MarkdownPipeline Pipeline =
|
||||
new MarkdownPipelineBuilder()
|
||||
.UseCustomContainers()
|
||||
@@ -120,6 +126,53 @@ public static partial class Markdown
|
||||
? UpToSentenceEnd(PlainText(inline)).TrimEnd(':', ' ')
|
||||
: "";
|
||||
|
||||
/// <summary>
|
||||
/// Builds a short plain-text teaser from a Markdown text, e.g. for a post card.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The editor's Markdown; may be <see langword="null"/>.</param>
|
||||
/// <param name="maxLength">
|
||||
/// The longest teaser, in characters, before the ellipsis. Defaults to
|
||||
/// <see cref="TeaserLength"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The text of all paragraphs (including those in lists, quotes and <c>:::</c>
|
||||
/// blocks), with all Markdown removed and whitespace collapsed. A longer text is
|
||||
/// cut at the last word boundary within <paramref name="maxLength"/> and ends
|
||||
/// in <c>…</c>. An empty string when there is no paragraph text at all.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// <paramref name="maxLength"/> is zero or negative.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Headings are skipped: on a card the title already heads the teaser. Unlike
|
||||
/// <see cref="FirstSentence"/> the teaser does not stop at the first sentence,
|
||||
/// because a post often opens with a short line ("Liebe Eltern,") that says
|
||||
/// nothing on its own.
|
||||
/// <para>
|
||||
/// Punctuation left dangling at the cut (<c>,</c>, <c>.</c>, <c>:</c> …) is
|
||||
/// dropped, so the teaser never ends in <c>,…</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// Markdown.Teaser("## Rückblick\n\nDer **Basar** war ein voller Erfolg.", 20);
|
||||
/// // "Der Basar war ein…"
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static string Teaser(string? markdown, int maxLength = TeaserLength) =>
|
||||
maxLength <= 0
|
||||
? throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, "The teaser needs room for at least one character.")
|
||||
: string.IsNullOrWhiteSpace(markdown)
|
||||
? ""
|
||||
: Shorten(
|
||||
string.Join(
|
||||
' ',
|
||||
Parse(markdown)
|
||||
.Descendants<ParagraphBlock>()
|
||||
.Select(paragraph => paragraph.Inline is { } inline ? PlainText(inline) : "")
|
||||
.Where(text => text.Length > 0)),
|
||||
maxLength);
|
||||
|
||||
/// <summary>
|
||||
/// Parses Markdown with the site's pipeline, for callers that take the document
|
||||
/// apart before rendering it (see <see cref="Render"/>).
|
||||
@@ -192,6 +245,26 @@ public static partial class Markdown
|
||||
private static string UpToSentenceEnd(string text) =>
|
||||
SentenceEnd().Match(text) is { Success: true } end ? text[..(end.Index + 1)] : text;
|
||||
|
||||
/// <summary>
|
||||
/// Cuts a text to a word boundary and marks the cut.
|
||||
/// </summary>
|
||||
/// <param name="text">Plain text.</param>
|
||||
/// <param name="maxLength">The longest result before the ellipsis.</param>
|
||||
/// <returns>
|
||||
/// The text unchanged if it fits; otherwise everything up to the last space
|
||||
/// within <paramref name="maxLength"/>, trailing punctuation removed, plus
|
||||
/// <c>…</c>. A single word longer than <paramref name="maxLength"/> is cut hard.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// The space is searched one character past the limit: if the text breaks
|
||||
/// exactly at <paramref name="maxLength"/>, the last whole word still fits.
|
||||
/// </remarks>
|
||||
private static string Shorten(string text, int maxLength) =>
|
||||
text.Length <= maxLength
|
||||
? text
|
||||
: (text[..(maxLength + 1)].LastIndexOf(' ') is var space and > 0 ? text[..space] : text[..maxLength])
|
||||
.TrimEnd(' ', ',', ';', ':', '.', '-', '–') + "…";
|
||||
|
||||
/// <summary>
|
||||
/// Matches the end mark of a sentence: <c>.</c>, <c>!</c> or <c>?</c> before a
|
||||
/// space or the end, unless it follows a lone letter (<c>z.</c>) or a number
|
||||
|
||||
Reference in new issue
Block a user