Design/home hero #54
No files matched your search
@@ -104,11 +104,17 @@ public class MarkdownTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void First_sentence_longer_than_the_limit_is_cut_between_words()
|
||||
{
|
||||
var sentence = Markdown.FirstSentence("Eins zwei drei vier fünf.", maxLength: 12);
|
||||
public void First_sentence_drops_a_colon_that_leads_into_a_list() =>
|
||||
Markdown.FirstSentence("Formulare zum Herunterladen:\n\n- Antrag")
|
||||
.ShouldBe("Formulare zum Herunterladen");
|
||||
|
||||
sentence.ShouldBe("Eins zwei…");
|
||||
[Fact]
|
||||
public void First_sentence_is_never_cut_short()
|
||||
{
|
||||
// Long teasers are clamped by the tile's CSS, not by a character count.
|
||||
var sentence = string.Join(' ', Enumerable.Repeat("Wort", 60)) + ".";
|
||||
|
||||
Markdown.FirstSentence(sentence).ShouldBe(sentence);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -119,8 +125,4 @@ public class MarkdownTests
|
||||
[InlineData("::: hinweis\nText im Baustein.\n:::")]
|
||||
public void First_sentence_is_empty_without_a_top_level_paragraph(string? markdown) =>
|
||||
Markdown.FirstSentence(markdown).ShouldBe("");
|
||||
|
||||
[Fact]
|
||||
public void First_sentence_rejects_a_limit_below_one() =>
|
||||
Should.Throw<ArgumentOutOfRangeException>(() => Markdown.FirstSentence("Text.", maxLength: 0));
|
||||
}
|
||||
@@ -38,8 +38,10 @@
|
||||
|
||||
/* On the gradient everything is white: the global heading colour is a blue that
|
||||
would vanish here. */
|
||||
/* balance evens out the lines, so a short last word ("IGMH") never wraps alone. */
|
||||
.hero h1 {
|
||||
color: var(--color-on-header);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* The intro is editor Markdown, rendered without scope attributes, so it is reached
|
||||
@@ -289,14 +291,23 @@
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
/* The teaser is a whole sentence; the tile cuts it visually after three lines,
|
||||
so the tiles of a row keep a similar height whatever the editor wrote. The
|
||||
-webkit- box is the form every current browser supports for line-clamp. */
|
||||
.tile-text {
|
||||
display: block;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Embeds -------------------------------------------------------------------
|
||||
The blocks the home page opts into (posts, events) sit side by side from desktop
|
||||
width, so the start page shows both without a long scroll. One column below. */
|
||||
width, so the start page shows both without a long scroll. One column below.
|
||||
In the grid the blocks' own top margin no longer collapses with the margin of
|
||||
the tiles above, so it is dropped there; otherwise the gap would be doubled. */
|
||||
@media (min-width: 64rem) {
|
||||
.home-embeds {
|
||||
display: grid;
|
||||
@@ -304,4 +315,8 @@
|
||||
gap: var(--space-5);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.home-embeds ::deep .embed {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
@@ -84,18 +84,12 @@ public static partial class Markdown
|
||||
/// short description on a home page tile.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The editor's Markdown; may be <see langword="null"/>.</param>
|
||||
/// <param name="maxLength">
|
||||
/// The longest sentence kept as is; a longer one is cut at the last word
|
||||
/// boundary before the limit and ends in <c>…</c>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The first sentence of the first top-level paragraph, with all Markdown
|
||||
/// removed and whitespace collapsed, or an empty string when there is no such
|
||||
/// paragraph (empty body, or only headings, lists and blocks).
|
||||
/// removed, whitespace collapsed and a trailing colon dropped, or an empty
|
||||
/// string when there is no such paragraph (empty body, or only headings, lists
|
||||
/// and blocks).
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// <paramref name="maxLength"/> is zero or negative.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// Only top-level paragraphs count: a heading repeats the title, and the text in
|
||||
/// a list or a <c>:::</c> block is rarely a sentence that describes the page.
|
||||
@@ -106,6 +100,12 @@ public static partial class Markdown
|
||||
/// longer abbreviation such as <c>bzw.</c> still does; that costs the rest
|
||||
/// of a teaser, never the page.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A paragraph that leads into a list often ends in <c>:</c> without a
|
||||
/// period; as a teaser the colon would point at a list that is not there.
|
||||
/// The sentence is returned whole: a long one is cut by the tile's CSS,
|
||||
/// which knows the space it has, not by a character count.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
@@ -113,14 +113,12 @@ public static partial class Markdown
|
||||
/// // "Wir sind sieben Eltern."
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static string FirstSentence(string? markdown, int maxLength = 140) =>
|
||||
maxLength <= 0
|
||||
? throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, "Must be positive.")
|
||||
: string.IsNullOrWhiteSpace(markdown)
|
||||
? ""
|
||||
: Parse(markdown).OfType<ParagraphBlock>().FirstOrDefault()?.Inline is { } inline
|
||||
? Shorten(UpToSentenceEnd(PlainText(inline)), maxLength)
|
||||
: "";
|
||||
public static string FirstSentence(string? markdown) =>
|
||||
string.IsNullOrWhiteSpace(markdown)
|
||||
? ""
|
||||
: Parse(markdown).OfType<ParagraphBlock>().FirstOrDefault()?.Inline is { } inline
|
||||
? UpToSentenceEnd(PlainText(inline)).TrimEnd(':', ' ')
|
||||
: "";
|
||||
|
||||
/// <summary>
|
||||
/// Parses Markdown with the site's pipeline, for callers that take the document
|
||||
@@ -194,25 +192,6 @@ public static partial class Markdown
|
||||
private static string UpToSentenceEnd(string text) =>
|
||||
SentenceEnd().Match(text) is { Success: true } end ? text[..(end.Index + 1)] : text;
|
||||
|
||||
/// <summary>
|
||||
/// Shortens a text to at most <paramref name="maxLength"/> characters plus an
|
||||
/// ellipsis, cutting between words.
|
||||
/// </summary>
|
||||
/// <param name="text">Plain text.</param>
|
||||
/// <param name="maxLength">The longest text kept as is.</param>
|
||||
/// <returns>The text, shortened if needed.</returns>
|
||||
/// <remarks>
|
||||
/// The search for a space starts at <paramref name="maxLength"/> itself, so a
|
||||
/// word that ends exactly at the limit is kept. A single word longer than the
|
||||
/// limit is cut hard.
|
||||
/// </remarks>
|
||||
private static string Shorten(string text, int maxLength) =>
|
||||
text.Length <= maxLength
|
||||
? text
|
||||
: text.LastIndexOf(' ', maxLength) is > 0 and var space
|
||||
? $"{text[..space].TrimEnd(',', ';', ':', '-', '–')}…"
|
||||
: $"{text[..maxLength]}…";
|
||||
|
||||
/// <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
|
||||
|
||||
+2
-1
@@ -131,7 +131,8 @@ Darunter erscheinen automatisch:
|
||||
keinen, entfällt die Karte.
|
||||
- **Kacheln** — eine pro öffentlicher Seite mit `location = header` (außer der
|
||||
Startseite), in Menü-Reihenfolge. Der Kacheltext ist der **erste Satz** des
|
||||
`body` (ohne Überschrift, ggf. gekürzt); beginnt der Text mit einer Liste oder
|
||||
`body` (ohne Überschrift, ein Doppelpunkt am Ende fällt weg; ein langer Satz
|
||||
wird nach drei Zeilen mit „…“ abgeschnitten); beginnt der Text mit einer Liste oder
|
||||
einem Baustein, zeigt die Kachel nur den Titel. Auf dem Handy zeigen die
|
||||
Kacheln immer nur Symbol und Titel (zwei Spalten), ab Tablet-Breite auch den
|
||||
Text (drei Spalten). Das Symbol hängt am Slug und ist
|
||||
|
||||
Reference in new issue
Block a user