"
");
html.Block(shaded.Content);
html.Write("
");
diff --git a/MarkupString.Html/LayoutCss.cs b/MarkupString.Html/LayoutCss.cs
index 87392c3..6d45099 100644
--- a/MarkupString.Html/LayoutCss.cs
+++ b/MarkupString.Html/LayoutCss.cs
@@ -91,8 +91,10 @@ public static class LayoutCss
".ms-grid.ms-down { display: block; }\n" +
".ms-grid > li { break-inside: avoid; white-space: pre-wrap; overflow-wrap: anywhere; }\n" +
".ms-pre { margin: 0; font: inherit; white-space: pre; overflow-x: auto; }\n" +
- ".ms-shaded { -webkit-background-clip: text; background-clip: text; color: transparent; }\n" +
- ".ms-shaded :is(.ms-box, .ms-gauge-bar), .ms-shaded :is(.ms-rule, .ms-divider)::before, .ms-shaded :is(.ms-rule, .ms-divider)::after, .ms-shaded .ms-line { border-image: var(--ms-shade) 1; }\n" +
+ ".ms-shaded:not(:has(.ms-box, .ms-flex, .ms-rule, .ms-divider, .ms-gauge)) { width: fit-content; max-width: 100%; }\n" +
+ ".ms-shaded:not(.ms-shade-borders) { -webkit-background-clip: text; background-clip: text; color: transparent; }\n" +
+ ".ms-shaded:not(.ms-shade-borders) :is(.ms-box-title, .ms-rule-title, .ms-field > dt, .ms-table th, .ms-bullets .ms-marker) { color: transparent; }\n" +
+ ".ms-shaded:not(.ms-shade-text) :is(.ms-box, .ms-gauge-bar), .ms-shaded:not(.ms-shade-text) :is(.ms-rule, .ms-divider)::before, .ms-shaded:not(.ms-shade-text) :is(.ms-rule, .ms-divider)::after, .ms-shaded:not(.ms-shade-text) .ms-line { border-image: var(--ms-shade) 1; }\n" +
".ms-table-wrap { max-width: 100%; overflow-x: auto; }\n" +
".ms-table { border-collapse: collapse; }\n" +
".ms-table.ms-fill { width: 100%; }\n" +
diff --git a/MarkupString.Tests/Layout/CompositionTests.cs b/MarkupString.Tests/Layout/CompositionTests.cs
index 30ddac3..2036c28 100644
--- a/MarkupString.Tests/Layout/CompositionTests.cs
+++ b/MarkupString.Tests/Layout/CompositionTests.cs
@@ -165,6 +165,81 @@ public async Task Shaded_ColoursTheBordersAndTextOfABlock()
await Assert.That(ansi).Contains("\u001b[38;2;0;0;255m+");
}
+ [Test]
+ public async Task Shaded_ATheme_ColoursNoneOfTheParts_SoTheGradientShows()
+ {
+ var green = AnsiMarkup.Create(foreground: new AnsiColor.Rgb(0, 128, 0));
+ var theme = new LayoutTheme { BorderColor = green, TitleColor = green };
+ var box = P("hi").ToBlock().Bordered(P("T"), BorderStyle.Ascii).Themed(theme).Shaded(RedToBlue);
+ var ansi = BlockLayout.Build(box, 8).Render(MarkupFormat.Ansi, Registry);
+
+ await Assert.That(ansi).DoesNotContain("38;2;0;128;0");
+ await Assert.That(ansi).Contains("\u001b[38;2;255;0;0m+");
+ await Assert.That(ansi).Contains("\u001b[38;2;0;0;255m+");
+ }
+
+ [Test]
+ public async Task Shaded_Across_RunsOverWhatIsDrawn_NotThePaddingAfterIt()
+ {
+ var ansi = BlockLayout.Build(P("abc").ToBlock().Shaded(RedToBlue), 40).Render(MarkupFormat.Ansi, Registry);
+
+ await Assert.That(ansi).Contains("\u001b[38;2;255;0;0ma");
+ await Assert.That(ansi).Contains("\u001b[38;2;0;0;255mc");
+ }
+
+ [Test]
+ public async Task Shaded_Borders_ColoursTheBoxDrawingAndLeavesTheTextInItsTheme()
+ {
+ var green = AnsiMarkup.Create(foreground: new AnsiColor.Rgb(0, 128, 0));
+ var box = P("hi").ToBlock().Bordered(P("T"), BorderStyle.Ascii).Themed(new LayoutTheme { BorderColor = green, TitleColor = green })
+ .Shaded(RedToBlue) with
+ { Target = GradientTarget.Borders };
+ var text = BlockLayout.Build(box, 8);
+ var ansi = text.Render(MarkupFormat.Ansi, Registry);
+
+ await Assert.That(ansi).Contains("\u001b[38;2;255;0;0m+");
+ await Assert.That(ansi).Contains("\u001b[38;2;0;0;255m+");
+ await Assert.That(ansi).Contains("\u001b[38;2;0;128;0mT");
+ await Assert.That(ansi).Contains("\u001b[0m hi");
+ await Assert.That(text.Render(MarkupFormat.Html, Registry)).Contains("
run.Markups).OfType
()).IsEmpty();
+ }
+
+ [Test]
+ public async Task Shaded_Target_IsKeptThroughTheWire()
+ {
+ var box = P("hi").ToBlock().Bordered(border: BorderStyle.Ascii).Shaded(RedToBlue) with { Target = GradientTarget.Text };
+ var read = MarkupTextSerializer.Deserialize(MarkupTextSerializer.Serialize(BlockLayout.Build(box, 6), Registry), Registry);
+
+ await Assert.That(BlockLayout.AsBlock(read)).IsTypeOf();
+ await Assert.That(((Shaded)BlockLayout.AsBlock(read)).Target).IsEqualTo(GradientTarget.Text);
+ }
+
+ [Test]
+ public async Task Shaded_ColourTheTextSetsItself_IsKept()
+ {
+ var green = MarkupText.Wrap(AnsiMarkup.Create(foreground: new AnsiColor.Rgb(0, 128, 0)), "g");
+ var ansi = BlockLayout.Build(new TextBlock(MarkupText.Concat([P("a"), green, P("b")])).Shaded(RedToBlue), 3).Render(MarkupFormat.Ansi, Registry);
+
+ await Assert.That(ansi).Contains("\u001b[38;2;0;128;0mg");
+ }
+
[Test]
public async Task Shaded_InHtml_ClipsTheGradientToTheText()
{
diff --git a/MarkupString/ColorGradient.cs b/MarkupString/ColorGradient.cs
index d895ba4..4e75077 100644
--- a/MarkupString/ColorGradient.cs
+++ b/MarkupString/ColorGradient.cs
@@ -64,6 +64,22 @@ public enum GradientFlow
Diagonal,
}
+/// Which parts of a layout a block's gradient colours.
+public enum GradientTarget
+{
+ /// Everything that shows: borders, rules and text alike.
+ All,
+
+ ///
+ /// The box drawing only: borders and the rules across them, table column separators and heading
+ /// rules, tree guides, gauge brackets. The text keeps its own colour, or its theme's.
+ ///
+ Borders,
+
+ /// The text only, titles and labels with it. The box drawing keeps its theme's colour.
+ Text,
+}
+
///
/// Colours blended one into the next, evenly spaced from 0 to 1. gives the colour
/// at a point; the same gradient for a page. Colours are blended in
@@ -137,8 +153,11 @@ public MarkupText Shade(MarkupText text, GradientFlow flow = GradientFlow.Charac
return MarkupText.Join(MarkupText.NewLine, ShadeLines(lines, flow, lines.Max(line => line.DisplayWidth)));
}
- /// Each of shaded as one block cells wide.
- internal MarkupText[] ShadeLines(IReadOnlyList lines, GradientFlow flow, int width)
+ ///
+ /// Each of shaded as one block cells wide; given
+ /// , only the characters it says yes to, which alone count along the way.
+ ///
+ internal MarkupText[] ShadeLines(IReadOnlyList lines, GradientFlow flow, int width, Func? shades = null)
{
if (IsEmpty || lines.Count == 0) return [.. lines];
@@ -153,7 +172,7 @@ internal MarkupText[] ShadeLines(IReadOnlyList lines, GradientFlow f
var inWord = false;
foreach (var character in lines[row].EnumerateGraphemes())
{
- var visible = !string.IsNullOrWhiteSpace(character.ToPlainText());
+ var visible = !string.IsNullOrWhiteSpace(character.ToPlainText()) && (shades?.Invoke(character) ?? true);
if (visible && !inWord) words++;
inWord = visible;
cells[row].Add((row, visible ? column : -1, words - 1, character));
diff --git a/MarkupString/Layout/Blocks/Block.cs b/MarkupString/Layout/Blocks/Block.cs
index 56e0106..8b8de8c 100644
--- a/MarkupString/Layout/Blocks/Block.cs
+++ b/MarkupString/Layout/Blocks/Block.cs
@@ -95,6 +95,18 @@ public sealed record LayoutContext
/// The look a block takes where it sets none of its own.
public LayoutTheme Theme { get; init; } = LayoutTheme.Default;
+ ///
+ /// Inside a block, the parts its gradient colours; null outside one. The theme's
+ /// colours for those parts are left off, as they would otherwise win over the gradient, and while only
+ /// some parts are shaded the box drawing is marked () so the gradient can tell it
+ /// from the text. A stripe is a background, and stays.
+ ///
+ internal GradientTarget? Shading { get; init; }
+
+ private bool ShadesText => Shading is GradientTarget.All or GradientTarget.Text;
+
+ private bool ShadesLines => Shading is GradientTarget.All or GradientTarget.Borders;
+
/// Where text with no alignment of its own sits: a table column's, or one set with .
public Alignment TextAlignment { get; init; } = Alignment.Left;
@@ -151,7 +163,7 @@ public TreeGuide Guide(TreeGuide? guide)
///
/// in the theme's colour for , or as it is when the theme
- /// sets none. Colour the text sets itself wins.
+ /// sets none or a gradient shades the text (). Colour the text sets itself wins.
///
/// The part's colour: theme => theme.BorderColor.
/// The piece.
@@ -159,9 +171,24 @@ public MarkupText Paint(Func part, MarkupText text)
{
ArgumentNullException.ThrowIfNull(part);
ArgumentNullException.ThrowIfNull(text);
- return part(Theme) is { } markup && text.Length > 0 ? MarkupText.Wrap(markup, text) : text;
+ return !ShadesText && part(Theme) is { } markup && text.Length > 0 ? MarkupText.Wrap(markup, text) : text;
}
+ ///
+ /// A piece of box drawing outside a border (a gauge's bracket, a table's column separator or heading rule), in
+ /// the theme's colour for as
+ /// draws it, unless a gradient shades the box drawing, and marked as box drawing ().
+ ///
+ internal MarkupText PaintLine(Func part, MarkupText text) =>
+ Line(!ShadesLines && part(Theme) is { } markup && text.Length > 0 ? MarkupText.Wrap(markup, text) : text);
+
+ ///
+ /// marked as box drawing while a gradient shades only the box drawing or
+ /// only the text (); as it is otherwise.
+ ///
+ internal MarkupText Line(MarkupText piece) =>
+ Shading is GradientTarget.Borders or GradientTarget.Text && piece.Length > 0 ? MarkupText.Wrap(LinePiece.Instance, piece) : piece;
+
///
/// The lines from on, a row of a striped table or list, laid on the theme's
/// stripe colour when is odd (the second, the fourth, ...). Colour a cell sets
@@ -177,8 +204,9 @@ internal void Stripe(IList lines, int first, int row)
/// with every piece in the theme's border colour.
internal BorderStyle Paint(BorderStyle style)
{
- if (Theme.BorderColor is not { } markup) return style;
- MarkupText Piece(MarkupText piece) => piece.Length > 0 ? MarkupText.Wrap(markup, piece) : piece;
+ var markup = ShadesLines ? null : Theme.BorderColor;
+ if (markup is null && Shading is null or GradientTarget.All) return style;
+ MarkupText Piece(MarkupText piece) => Line(markup is not null && piece.Length > 0 ? MarkupText.Wrap(markup, piece) : piece);
return style with
{
TopLeft = Piece(style.TopLeft),
@@ -199,8 +227,9 @@ internal BorderStyle Paint(BorderStyle style)
/// with every piece in the theme's guide colour.
internal TreeGuide Paint(TreeGuide guide)
{
- if (Theme.GuideColor is not { } markup) return guide;
- MarkupText Piece(MarkupText piece) => piece.Length > 0 ? MarkupText.Wrap(markup, piece) : piece;
+ var markup = ShadesLines ? null : Theme.GuideColor;
+ if (markup is null && Shading is null or GradientTarget.All) return guide;
+ MarkupText Piece(MarkupText piece) => Line(markup is not null && piece.Length > 0 ? MarkupText.Wrap(markup, piece) : piece);
return guide with { Branch = Piece(guide.Branch), Last = Piece(guide.Last), Pipe = Piece(guide.Pipe), Blank = Piece(guide.Blank) };
}
}
diff --git a/MarkupString/Layout/Blocks/Gauge.cs b/MarkupString/Layout/Blocks/Gauge.cs
index 83fd452..ae3fa1a 100644
--- a/MarkupString/Layout/Blocks/Gauge.cs
+++ b/MarkupString/Layout/Blocks/Gauge.cs
@@ -53,8 +53,8 @@ public override Block MapText(Func map) =>
public override void Draw(LayoutContext context, int width, IList lines)
{
var theme = context.Theme;
- var open = context.Paint(t => t.BorderColor, context.Glyph(Open ?? theme.Piece(t => t.GaugeOpen), "["));
- var close = context.Paint(t => t.BorderColor, context.Glyph(Close ?? theme.Piece(t => t.GaugeClose), "]"));
+ var open = context.PaintLine(t => t.BorderColor, context.Glyph(Open ?? theme.Piece(t => t.GaugeOpen), "["));
+ var close = context.PaintLine(t => t.BorderColor, context.Glyph(Close ?? theme.Piece(t => t.GaugeClose), "]"));
var filled = context.Glyph(Filled ?? theme.Piece(t => t.GaugeFilled), "#");
var empty = context.Glyph(Empty ?? theme.Piece(t => t.GaugeEmpty), "-");
var label = Label is { Length: > 0 } text ? MarkupText.Concat([text, MarkupText.Space]) : MarkupText.Empty;
diff --git a/MarkupString/Layout/Blocks/LayoutJson.cs b/MarkupString/Layout/Blocks/LayoutJson.cs
index 0cc76c3..16274ff 100644
--- a/MarkupString/Layout/Blocks/LayoutJson.cs
+++ b/MarkupString/Layout/Blocks/LayoutJson.cs
@@ -261,8 +261,13 @@ public static IMarkup Read(JsonElement element, MarkupRegistry? registry)
w.Block("c", b.Content);
w.Gradient("gr", b.Gradient);
w.Enum("fl", b.Flow, GradientFlow.Across);
+ w.Enum("tg", b.Target);
},
- r => new Shaded(r.Block("c") ?? new Stack([]), r.Gradient("gr") ?? new ColorGradient([])) { Flow = r.Enum("fl", GradientFlow.Across) }),
+ r => new Shaded(r.Block("c") ?? new Stack([]), r.Gradient("gr") ?? new ColorGradient([]))
+ {
+ Flow = r.Enum("fl", GradientFlow.Across),
+ Target = r.Enum("tg", GradientTarget.All),
+ }),
BlockCodec.Create("colored",
(b, w) =>
{
diff --git a/MarkupString/Layout/Blocks/Modifiers.cs b/MarkupString/Layout/Blocks/Modifiers.cs
index 0258b56..2e6e590 100644
--- a/MarkupString/Layout/Blocks/Modifiers.cs
+++ b/MarkupString/Layout/Blocks/Modifiers.cs
@@ -18,7 +18,8 @@ public override void Draw(LayoutContext context, int width, IList li
///
/// drawn in the colours of : its borders, rules
-/// and text alike. Spaces are left alone, and colour the content sets itself is kept.
+/// and text alike. Spaces are left alone, and colour the content sets itself is kept; the colours a
+/// theme gives its parts are not, so a themed box is shaded borders and all.
///
/// What is shaded.
/// The colours.
@@ -31,9 +32,30 @@ public override Block MapText(Func map) =>
/// Which way the colours run.
public GradientFlow Flow { get; init; } = GradientFlow.Across;
+ /// Which parts the colours fall on: everything, the box drawing only, or the text only.
+ public GradientTarget Target { get; init; } = GradientTarget.All;
+
///
- public override void Draw(LayoutContext context, int width, IList lines) =>
- lines.AddRange(Gradient.ShadeLines(context.Lines(Content, width), Flow, width));
+ public override void Draw(LayoutContext context, int width, IList lines)
+ {
+ if (Gradient.IsEmpty)
+ {
+ context.Draw(Content, width, lines);
+ return;
+ }
+ // The colours run over what is drawn, not the padding after it: a table narrower than the
+ // screen is shaded across its own columns.
+ var drawn = (context with { Shading = Target }).Lines(Content, width);
+ var used = drawn.Count == 0 ? width : Math.Max(1, drawn.Max(line => line.Trim(TrimType.TrimEnd).DisplayWidth));
+ if (Target == GradientTarget.All)
+ {
+ lines.AddRange(Gradient.ShadeLines(drawn, Flow, used));
+ return;
+ }
+ var borders = Target == GradientTarget.Borders;
+ foreach (var line in Gradient.ShadeLines(drawn, Flow, used, character => LinePiece.Marks(character) == borders))
+ lines.Add(LinePiece.Unmark(line));
+ }
}
/// drawn under — a colour, say — which colour it sets itself overrides.
@@ -122,3 +144,19 @@ public static Shaded Shaded(this Block content, ColorGradient gradient, Gradient
/// Blocks side by side.
public static Flex InRow(this IEnumerable blocks) => new([.. blocks]);
}
+
+///
+/// Marks a piece of box drawing while a block that shades only the box drawing, or
+/// only the text, is drawn, so its gradient can tell the two apart. Taken off again once it is shaded.
+///
+internal sealed record LinePiece : IDataMarkup
+{
+ public static LinePiece Instance { get; } = new();
+
+ /// Whether is box drawing.
+ public static bool Marks(MarkupText character) =>
+ !character.Runs.IsDefaultOrEmpty && character.Runs.Any(run => run.Markups.Any(markup => markup is LinePiece));
+
+ /// without the mark.
+ public static MarkupText Unmark(MarkupText line) => line.WithoutMarkup(markup => markup is LinePiece);
+}
diff --git a/MarkupString/Layout/Blocks/Table.cs b/MarkupString/Layout/Blocks/Table.cs
index 12bd998..e3e333e 100644
--- a/MarkupString/Layout/Blocks/Table.cs
+++ b/MarkupString/Layout/Blocks/Table.cs
@@ -47,7 +47,7 @@ public override void Draw(LayoutContext context, int width, IList li
{
if (Columns.IsDefaultOrEmpty) return;
// Measured as drawn: an ASCII reader's separator may be the wider " | " stand-in.
- var divider = Separator is { } drawn ? context.Paint(theme => theme.SeparatorColor, context.Glyph(drawn, " | ")) : BlockText.Blank(Gap);
+ var divider = Separator is { } drawn ? context.PaintLine(theme => theme.SeparatorColor, context.Glyph(drawn, " | ")) : BlockText.Blank(Gap);
if (Widths(context, width, divider.DisplayWidth) is not { } widths)
{
DrawCards(context, width, lines);
@@ -61,7 +61,7 @@ public override void Draw(LayoutContext context, int width, IList li
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);
- if (rule.Length > 0) lines.Add(BlockText.Fit(context.Paint(theme => theme.HeaderRuleColor, BlockText.Run(context.Glyph(rule, "-"), tableWidth)), width));
+ 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];
var index = 0;
diff --git a/MarkupString/MarkupText.cs b/MarkupString/MarkupText.cs
index f276449..b7c62a1 100644
--- a/MarkupString/MarkupText.cs
+++ b/MarkupString/MarkupText.cs
@@ -197,29 +197,32 @@ public MarkupText WithoutVisualMarkup()
/// The text with every data layer () taken off and everything else kept: the
/// counterpart of , for text whose data must not be trusted.
///
- public MarkupText WithoutDataMarkup()
+ public MarkupText WithoutDataMarkup() => WithoutMarkup(static markup => markup is IDataMarkup);
+
+ /// The text with every layer says yes to taken off and everything else kept.
+ internal MarkupText WithoutMarkup(Func drop)
{
if (Runs.IsDefaultOrEmpty) return this;
- var hasData = false;
+ var dropping = false;
foreach (var run in Runs)
{
foreach (var markup in run.Markups)
{
- if (markup is not IDataMarkup) continue;
- hasData = true;
+ if (!drop(markup)) continue;
+ dropping = true;
break;
}
- if (hasData) break;
+ if (dropping) break;
}
- if (!hasData) return this;
+ if (!dropping) return this;
var builder = ImmutableArray.CreateBuilder(Runs.Length);
foreach (var run in Runs)
{
var kept = new List(run.Markups.Count);
foreach (var markup in run.Markups)
- if (markup is not IDataMarkup) kept.Add(markup);
+ if (!drop(markup)) kept.Add(markup);
if (kept.Count == run.Markups.Count) builder.Add(run);
else if (kept.Count > 0) builder.Add(run with { Markups = MarkupSet.Of(kept) });
}
diff --git a/MarkupString/PublicAPI.Unshipped.txt b/MarkupString/PublicAPI.Unshipped.txt
index 9acac77..6054a51 100644
--- a/MarkupString/PublicAPI.Unshipped.txt
+++ b/MarkupString/PublicAPI.Unshipped.txt
@@ -4,3 +4,9 @@ static readonly MarkupString.MarkupFormat.Markdown -> MarkupString.MarkupFormat!
MarkupString.IDataMarkup
MarkupString.MarkupText.WithoutVisualMarkup() -> MarkupString.MarkupText!
MarkupString.MarkupText.WithoutDataMarkup() -> MarkupString.MarkupText!
+MarkupString.GradientTarget
+MarkupString.GradientTarget.All = 0 -> MarkupString.GradientTarget
+MarkupString.GradientTarget.Borders = 1 -> MarkupString.GradientTarget
+MarkupString.GradientTarget.Text = 2 -> MarkupString.GradientTarget
+MarkupString.Layout.Shaded.Target.get -> MarkupString.GradientTarget
+MarkupString.Layout.Shaded.Target.init -> void