Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
RGB when the picture is opaque, filtered Up, at zlib level 2; sixel reads each band once and writes a
colour only over the columns it reaches.

### Fixed

- **A box round a picture fits it in HTML.** The `<img>` of a `Figure` sat on the text baseline,
leaving a band the height of a descender below it, and the box's side padding left gaps left and
right but none above. The picture now aligns to the top of its line, and a box whose body is only a
picture on a line of its own (`ms-box-picture`) has no padding and is no wider than the picture.
A `Frame`'s `Padding` reaches the HTML as `--ms-pad` (it was always `1ch`).

## 2.11.2 — 2026-10-07

### Fixed
Expand Down
12 changes: 11 additions & 1 deletion MarkupString.Html/Emitters/LayoutHtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,17 @@ private static void Frame(Frame frame, HtmlLayoutWriter html)
{
html.Write("<fieldset class=\"ms-box ms-border-");
html.Write(Css(BorderOf(frame.Border, html).Name));
html.Write("\">");
// A box round nothing but a picture fits it: no padding, and no wider than the picture.
if (frame.Body is Figure { Float: FigureFloat.None, Beside: null } figure && html.AllowsImage(figure.Image.Source))
html.Write(" ms-box-picture");
html.Write("\"");
if (frame.Padding != 1)
{
html.Write(" style=\"--ms-pad:");
html.Write(Number(Math.Max(0, frame.Padding)));
html.Write("ch\"");
}
html.Write(">");
if (frame.Title is { Length: > 0 } title)
{
html.Write("<legend class=\"ms-box-title\"");
Expand Down
7 changes: 5 additions & 2 deletions MarkupString.Html/LayoutCss.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace MarkupString.Html;
/// second row of a striped table or list (a faint grey when unset). A page
/// sets these to theme every layout on it; a layout's own theme sets them on its block, and a game's
/// default theme sets the <c>-default</c> form of each, under what the page sets.
/// A box's padding is <c>--ms-pad</c> (<c>1ch</c> unset), set on the box when its own differs.
/// Everything is sized in <c>ch</c>, the width of a cell in the terminal font the layout was laid out
/// in, and every row wraps, so nothing scrolls sideways on a narrow page.
/// </remarks>
Expand All @@ -20,7 +21,7 @@ public static class LayoutCss
public static readonly string Fixed =
".ms-layout { display: block; max-width: 100%; white-space: normal; }\n" +
".ms-text { white-space: pre-wrap; overflow-wrap: anywhere; }\n" +
".ms-box { margin: 0; padding: 0 1ch; min-width: 0; border: 1px solid var(--ms-border, var(--ms-border-default, currentColor)); }\n" +
".ms-box { margin: 0; padding: 0 var(--ms-pad, 1ch); min-width: 0; border: 1px solid var(--ms-border, var(--ms-border-default, currentColor)); }\n" +
".ms-box.ms-border-none { border: none; padding: 0; }\n" +
".ms-box.ms-border-double { border: 3px double var(--ms-border, var(--ms-border-default, currentColor)); }\n" +
".ms-box.ms-border-heavy { border-width: 2px; }\n" +
Expand All @@ -41,7 +42,9 @@ public static class LayoutCss
".ms-item { min-width: 0; box-sizing: border-box; }\n" +
".ms-flex.ms-divided > .ms-item + .ms-item { border-left: 1px solid var(--ms-separator, var(--ms-separator-default, currentColor)); padding-left: 1ch; }\n" +
".ms-figure { display: flow-root; }\n" +
".ms-figure-image { max-width: 100%; height: auto; }\n" +
".ms-figure-image { max-width: 100%; height: auto; vertical-align: top; }\n" +
".ms-box.ms-box-picture { padding: 0; width: fit-content; max-width: 100%; box-sizing: border-box; }\n" +
".ms-box-picture .ms-figure-image { display: block; }\n" +
".ms-figure-art { margin: 0; font: inherit; white-space: pre; }\n" +
".ms-float-left > .ms-figure-image, .ms-float-left > .ms-figure-art { float: left; max-width: 50%; margin: 0 2ch 0.5em 0; }\n" +
".ms-float-right > .ms-figure-image, .ms-float-right > .ms-figure-art { float: right; max-width: 50%; margin: 0 0 0.5em 2ch; }\n" +
Expand Down
22 changes: 22 additions & 0 deletions MarkupString.Tests/Layout/BlockLayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,28 @@ public async Task Figure_InHtml_FloatsThePicture()
await Assert.That(html).Contains("<div class=\"ms-figure ms-float-right\"><img class=\"ms-figure-image\" src=\"https://example.com/cat.png\" alt=\"A &quot;cat&quot;\"><div class=\"ms-text\">Text</div></div>");
}

[Test]
public async Task Figure_AloneInABox_TheBoxFitsThePicture()
{
var picture = new Figure(new ImageMarkup("https://example.com/cat.png", "A cat"), MarkupText.Empty);
var beside = picture with { Float = FigureFloat.Left, Beside = P("Text") };
var refused = Registry.WithLayoutImages(source => !source.Contains("example.com", StringComparison.Ordinal));

var fitted = BlockLayout.Build(new Frame(picture), 30).Render(MarkupFormat.Html, Registry);
var withText = BlockLayout.Build(new Frame(beside), 30).Render(MarkupFormat.Html, Registry);
var art = BlockLayout.Build(new Frame(picture with { Art = P("=^.^=") }), 30).Render(MarkupFormat.Html, refused);
var tight = BlockLayout.Build(new Frame(P("Text")) { Padding = 0 }, 30).Render(MarkupFormat.Html, Registry);

await Assert.That(fitted).Contains("<fieldset class=\"ms-box ms-border-single ms-box-picture\"><div class=\"ms-figure ms-float-none\">");
await Assert.That(withText).Contains("<fieldset class=\"ms-box ms-border-single\">");
await Assert.That(art).Contains("<fieldset class=\"ms-box ms-border-single\">");
await Assert.That(tight).Contains("<fieldset class=\"ms-box ms-border-single\" style=\"--ms-pad:0ch\">");
// The picture sits on the top of its line rather than its baseline, so no gap for descenders is left under it.
await Assert.That(LayoutCss.Fixed).Contains(".ms-figure-image { max-width: 100%; height: auto; vertical-align: top; }");
await Assert.That(LayoutCss.Fixed).Contains(".ms-box.ms-box-picture { padding: 0; width: fit-content; max-width: 100%; box-sizing: border-box; }");
await Assert.That(LayoutCss.Fixed).Contains(".ms-box { margin: 0; padding: 0 var(--ms-pad, 1ch);");
}

[Test]
public async Task Figure_RefusedPicture_ShowsItsArt()
{
Expand Down
Loading