From 5575a1f37b657d3ea932d6816b358b12e42a29ca Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 16:28:51 +0000 Subject: [PATCH] =?UTF-8?q?Draw=20a=20table's=20heading=20rule=20like=20it?= =?UTF-8?q?s=20border:=20=E2=94=80,=20or=20-=20under=20an=20ASCII=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default rule under a table's headings was a plain -, so a table in a Unicode box, or one laid out for a UTF-8 reader, had an ASCII line under its headings while its border and rules were box-drawing characters. The default is now ─, and - under an ASCII border or for an ASCII reader. A rule a theme or the table names still wins. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Rqqt7UxzSZ6qYAPv4SQUjN --- CHANGELOG.md | 8 +++++++ MarkupString.Tests/Layout/CompositionTests.cs | 2 +- .../Layout/WidgetLayoutTests.cs | 22 ++++++++++++++----- MarkupString/Layout/Blocks/Block.cs | 5 +++-- MarkupString/Layout/Blocks/Table.cs | 11 ++++++++-- docs/layout.md | 5 +++-- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 638aae0..0cd8430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and `MarkupString.Pueblo`. The packages share one version and are released toget The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.24.2 — Unreleased + +### Fixed + +- **A table's heading rule matches its border**: unless a theme names its own, the line under a table's + headings is `─` (the default `LayoutTheme.Defaults.HeaderRule`), and `-` under an ASCII border or for an + ASCII reader. It was always `-`, so a table in a Unicode box had an ASCII rule under its headings. + ## 2.24.1 — Unreleased ### Fixed diff --git a/MarkupString.Tests/Layout/CompositionTests.cs b/MarkupString.Tests/Layout/CompositionTests.cs index 2036c28..e003359 100644 --- a/MarkupString.Tests/Layout/CompositionTests.cs +++ b/MarkupString.Tests/Layout/CompositionTests.cs @@ -215,7 +215,7 @@ public async Task Shaded_Text_ColoursTheTextAndLeavesTheBoxDrawingInItsTheme() var ansi = text.Render(MarkupFormat.Ansi, Registry); await Assert.That(ansi).Contains("\u001b[38;2;0;128;0m|"); - await Assert.That(ansi).Contains("\u001b[38;2;0;128;0m-"); + await Assert.That(ansi).Contains("\u001b[38;2;0;128;0m─"); await Assert.That(ansi).Contains("\u001b[38;2;255;0;0mA"); await Assert.That(ansi).Contains("\u001b[38;2;0;0;255md"); await Assert.That(text.Runs.SelectMany(run => run.Markups).OfType()).IsEmpty(); diff --git a/MarkupString.Tests/Layout/WidgetLayoutTests.cs b/MarkupString.Tests/Layout/WidgetLayoutTests.cs index 09366b0..cd41c9b 100644 --- a/MarkupString.Tests/Layout/WidgetLayoutTests.cs +++ b/MarkupString.Tests/Layout/WidgetLayoutTests.cs @@ -112,6 +112,16 @@ public async Task Grid_InHtml_IsAColumnedList() => await Assert.That(BlockLayout.Build(new Grid(Names), 32).Render(MarkupFormat.Html, Registry)) .Contains("
  • Mannaz
  • "); + /// The rule under the headings is drawn like the border around it: - under an ASCII one or for an ASCII reader. + [Test] + public async Task Table_HeaderRule_FollowsTheBorder() + { + await Assert.That(Lines(Who(), 60)[1]).IsEqualTo(new string('─', 43)); + await Assert.That(Lines(Who(), 60, Ascii)[1]).IsEqualTo(new string('-', 43)); + await Assert.That(Lines(Who().Themed(new LayoutTheme { Border = BorderStyle.Ascii }), 60)[1]).IsEqualTo(new string('-', 43)); + await Assert.That(Lines(Who().Themed(new LayoutTheme { Border = BorderStyle.Ascii, HeaderRule = P("=") }), 60)[1]).IsEqualTo(new string('=', 43)); + } + private static Table Who(int nameMin = 6) => new( [ new TableColumn(P("Name")) { Min = nameMin }, @@ -128,7 +138,7 @@ public async Task Table_ColumnsFitTheirWidestCell() => await Assert.That(Lines(Who(), 60)).IsEquivalentTo(new[] { "Name Idle Doing", - "-------------------------------------------", + "───────────────────────────────────────────", "Mannaz 0s Hooooo?", "Raya 5m Writing a scene in the garden", }); @@ -138,7 +148,7 @@ public async Task Table_TooWide_WrapsFirst() => await Assert.That(Lines(Who(), 30)).IsEquivalentTo(new[] { "Name Idle Doing", - "------------------------------", + "──────────────────────────────", "Mannaz 0s Hooooo?", "Raya 5m Writing a scene", " in the garden", @@ -149,7 +159,7 @@ public async Task Table_TooWideToWrap_LeavesOutTheLeastImportant() => await Assert.That(Lines(Who(), 18)).IsEquivalentTo(new[] { "Name Idle", - "------------", + "────────────", "Mannaz 0s", "Raya 5m", }); @@ -165,7 +175,7 @@ public async Task Table_NoColumnFits_RowsBecomeCards() [Test] public async Task Table_AColumnThatDoesNotWrap_IsWholeOrLeftOut() - => await Assert.That(Lines(Who(), 10)).IsEquivalentTo(new[] { "Name", "------", "Mannaz", "Raya" }); + => await Assert.That(Lines(Who(), 10)).IsEquivalentTo(new[] { "Name", "──────", "Mannaz", "Raya" }); [Test] public async Task Table_InHtml_IsATableThatDropsColumnsWhenNarrow() @@ -218,7 +228,7 @@ public async Task Table_AColumnThatGrows_FillsTheWidth() { var lines = BlockLayout.Build(Growing(0, 1), 60).ToPlainText().Split('\n'); - await Assert.That(lines[1]).IsEqualTo(new string('-', 60)).Because("the heading rule spans the table, which now spans its width"); + await Assert.That(lines[1]).IsEqualTo(new string('─', 60)).Because("the heading rule spans the table, which now spans its width"); await Assert.That(lines[0].TrimEnd()).IsEqualTo("Name Idle Doing"); await Assert.That(lines[3].TrimEnd()).IsEqualTo("Raya 5m Writing a scene in the garden"); } @@ -233,7 +243,7 @@ public async Task Table_AGrowingColumn_StopsAtItsMax() { var table = Growing(0, 1) with { Columns = [.. Growing(0, 1).Columns.SetItem(2, Growing(0, 1).Columns[2] with { Max = 35 })] }; - await Assert.That(Lines(table, 60)[1]).IsEqualTo(new string('-', 49)).Because("Doing grows from 29 to its 35 and no further"); + await Assert.That(Lines(table, 60)[1]).IsEqualTo(new string('─', 49)).Because("Doing grows from 29 to its 35 and no further"); } [Test] diff --git a/MarkupString/Layout/Blocks/Block.cs b/MarkupString/Layout/Blocks/Block.cs index bdf2cee..c29af25 100644 --- a/MarkupString/Layout/Blocks/Block.cs +++ b/MarkupString/Layout/Blocks/Block.cs @@ -261,7 +261,8 @@ public sealed record LayoutTheme /// /// What every unset piece ends up as: light box lines, light tree guides, [█████░░░░░] - /// gauges, • bullets, ": " after a label and - under table headings. + /// gauges, • bullets, ": " after a label and ─ under table headings (- under an + /// ASCII border). /// public static LayoutTheme Defaults { get; } = new() { @@ -273,7 +274,7 @@ public sealed record LayoutTheme GaugeClose = MarkupText.Plain("]"), Bullet = MarkupText.Plain("•"), FieldSeparator = MarkupText.Plain(": "), - HeaderRule = MarkupText.Plain("-"), + HeaderRule = MarkupText.Plain("─"), }; /// Boxes and rules. diff --git a/MarkupString/Layout/Blocks/Table.cs b/MarkupString/Layout/Blocks/Table.cs index 616bc28..5398b68 100644 --- a/MarkupString/Layout/Blocks/Table.cs +++ b/MarkupString/Layout/Blocks/Table.cs @@ -26,7 +26,10 @@ this with /// Drawn between two columns on every line instead of the gap. public MarkupText? Separator { get; init; } - /// The line under the headings, repeated; empty for none; unset, the theme's (-). + /// + /// The line under the headings, repeated; empty for none; unset, the theme's, or ─ (- under an + /// ASCII border) when the theme names none. + /// public MarkupText? HeaderRule { get; init; } /// @@ -60,7 +63,7 @@ public override void Draw(LayoutContext context, int width, IList li MarkupText Join(IEnumerable cells) => BlockText.Fit(MarkupText.Join(divider, cells), width); lines.Add(Join(shown.Select(c => BlockText.Fit(context.Paint(theme => theme.HeadingColor, Columns[c].Header).FormatColumn(BlockText.Column(widths[c], Columns[c].Alignment))[0], widths[c])))); - var rule = HeaderRule ?? context.Theme.Piece(theme => theme.HeaderRule); + var rule = HeaderRule ?? context.Theme.HeaderRule ?? DefaultRule(context); if (rule.Length > 0) lines.Add(BlockText.Fit(context.PaintLine(theme => theme.HeaderRuleColor, BlockText.Run(context.Glyph(rule, "-"), tableWidth)), width)); var cellLines = new List[Columns.Length]; @@ -103,6 +106,10 @@ public override void DrawLinear(LayoutContext context, int width, IListThe default rule under the headings, ASCII when the border it sits among is. + private static MarkupText DefaultRule(LayoutContext context) => + context.Border(null).IsAscii ? MarkupText.Plain("-") : LayoutTheme.Defaults.HeaderRule!; + /// Each row as a card of labelled values, the way a phone shows a table too wide for it. private void DrawCards(LayoutContext context, int width, IList lines) { diff --git a/docs/layout.md b/docs/layout.md index f51b770..cb81e6f 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -475,12 +475,13 @@ widest cell. When the table is too wide, the columns that wrap give way, widest least width; then the column with the highest `Priority` number is left out, and so on. A column that does not wrap is shown whole or not at all. When not even the most important column fits, each row becomes a card of labelled values. In HTML, a column of priority 2 carries `ms-p2` and one of 3 or -more `ms-p3`, which `LayoutCss.Fixed` hides on narrow pages. +more `ms-p3`, which `LayoutCss.Fixed` hides on narrow pages. The rule under the headings is the +theme's `HeaderRule`, or `─` when it names none (`-` under an ASCII border or for an ASCII reader). ``` At 30: At 18: Name Idle Doing Name Idle ------------------------------- ------------ +────────────────────────────── ──────────── Mannaz 0s Hooooo? Mannaz 0s Raya 5m Writing a scene Raya 5m in the garden