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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<a href="#quick-start">Quick start</a> ·
<a href="#packages">Packages</a> ·
<a href="#unicode-aware-layout">Layout</a> ·
<a href="#theme-a-whole-layout">Theming</a> ·
<a href="#documentation">Documentation</a> ·
<a href="#contributing">Contributing</a>
</p>
Expand Down Expand Up @@ -62,6 +63,43 @@ and naturally flowing content in HTML.

![The same MarkupString figure rendered as ANSI terminal art and an HTML image](docs/assets/showcase-image-drawing.png)

## Theme a whole layout

A theme changes semantic parts together: borders, titles, labels, bullets, guides, gauges, table
headings, and striped rows. Presets can change the drawing characters too; generated palettes build
accessible colour roles from a single seed.

```csharp
var registry = MarkupRegistry.Empty.WithAnsi().WithHtml();

var sheet = new Stack(
[
new Fields(
[
new Field(MarkupText.Plain("Name"), MarkupText.Plain("Lyra Vale")),
new Field(MarkupText.Plain("Role"), MarkupText.Plain("Wayfinder")),
]),
new Gauge(7, 10) { Label = MarkupText.Plain("Trail") },
]).Bordered(MarkupText.Plain("WAYFINDER'S JOURNAL"));

var fantasy = sheet.Themed(ThemePalette.Preset("fantasy")!.ToLayoutTheme());
var housePalette = ThemePalette.Generate(
new RgbColor(34, 211, 238),
ThemeHarmony.Triadic,
contrast: 0.35);
var house = sheet.Themed(housePalette.ToLayoutTheme());

var value = BlockLayout.Build(house, width: 46);
var ansi = value.Render(MarkupFormat.Ansi, registry);
var html = value.Render(MarkupFormat.Html, registry);
```

![The same MarkupString layout rendered with a fantasy preset and a generated triadic palette](docs/assets/showcase-theming.png)

Use `.Themed(...)` to override the surrounding theme, or `.ThemedUnder(...)` to provide defaults
that a reader's theme can replace. See [themes and palettes](docs/layout.md#themes-and-palettes) for
the built-in presets, light and dark modes, custom glyphs, base16 schemes, JSON, and contrast checks.

## Why MarkupString?

- **One source of truth** — keep content and semantics together, then choose the output format at
Expand Down
Binary file modified docs/assets/showcase-box-drawing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/showcase-image-drawing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/showcase-theming.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 69 additions & 8 deletions docs/showcase/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,77 @@
figureValue,
registry);

var character = new Stack(
[
new Fields(
[
new Field(P("Name"), P("Lyra Vale")),
new Field(P("Role"), P("Wayfinder")),
new Field(P("Region"), P("Glasswood")),
new Field(P("Status"), P("Ready")),
]) { Columns = 2, Gap = 3, Leader = P("·") },
new Rule(P("CURRENT QUEST")),
new Gauge(7, 10) { Label = P("Trail"), Show = GaugeShow.Percent },
new Bullets([P("Map the moonwell"), P("Return before dawn")]),
]).Bordered(P("WAYFINDER'S JOURNAL"));

var fantasy = BlockLayout.Build(character.Themed(ThemePalette.Preset("fantasy")!.ToLayoutTheme()), 46);
var generatedPalette = ThemePalette.Generate(new RgbColor(34, 211, 238), ThemeHarmony.Triadic, contrast: 0.35);
var generated = BlockLayout.Build(character.Themed(generatedPalette.ToLayoutTheme()), 46);
WriteThemePage(
Path.Combine(output, "theming.html"),
"One layout, a whole new world",
"Presets and generated palettes recolour every semantic part — and can change the shapes too.",
fantasy,
generated,
registry);

static MarkupText P(string text) => MarkupText.Plain(text);

static void WritePage(string path, string title, string subtitle, MarkupText value, MarkupRegistry registry)
{
var ansi = value.Render(MarkupFormat.Ansi, registry);
var ansiForBrowser = AnsiEscapeParser.Parse(ansi).Render(MarkupFormat.Html, registry);
var html = value.Render(MarkupFormat.Html, registry);
var escapedAnsi = WebUtility.HtmlEncode(ansi.Replace("\u001b", "\\e", StringComparison.Ordinal));
WriteComparisonPage(
path, title, subtitle, "MarkupText <span>→</span> renderer <span>→</span> native output",
"ANSI terminal", "SGR + cells", "terminal", ansiForBrowser,
"HTML browser", "semantic elements", "browser", html);
}

static void WriteThemePage(
string path,
string title,
string subtitle,
MarkupText preset,
MarkupText generated,
MarkupRegistry registry)
{
var presetHtml = AnsiEscapeParser.Parse(preset.Render(MarkupFormat.Ansi, registry)).Render(MarkupFormat.Html, registry);
var generatedHtml = AnsiEscapeParser.Parse(generated.Render(MarkupFormat.Ansi, registry)).Render(MarkupFormat.Html, registry);
WriteComparisonPage(
path, title, subtitle, "layout <span>→</span> palette <span>→</span> themed output",
"Fantasy preset", "colours + glyphs", "terminal", presetHtml,
"Generated palette", "triadic harmony", "terminal", generatedHtml);
}

static void WriteComparisonPage(
string path,
string title,
string subtitle,
string flowHtml,
string leftLabel,
string leftNative,
string leftClass,
string leftContent,
string rightLabel,
string rightNative,
string rightClass,
string rightContent)
{
static string PanelBody(string cssClass, string content) => cssClass == "terminal"
? "<pre class=\"terminal\">" + content + "</pre>"
: "<div class=\"browser\">" + content + "</div>";

File.WriteAllText(path, $$"""
<!doctype html>
Expand Down Expand Up @@ -117,26 +180,24 @@ static void WritePage(string path, string title, string subtitle, MarkupText val
.browser .ms-figure-image { max-width: 220px; margin: 0 20px 14px 0; filter: drop-shadow(0 10px 20px rgba(0, 0, 0, .35)); }
.browser .ms-figure { font-family: "DejaVu Sans", sans-serif; }
.browser .ms-text { line-height: 1.65; }
.source { display: none; }
</style>
</head>
<body>
<main>
<header>
<div><h1>{{WebUtility.HtmlEncode(title)}}</h1><p class="subtitle">{{WebUtility.HtmlEncode(subtitle)}}</p></div>
<div class="flow">MarkupText <span>→</span> renderer <span>→</span> native output</div>
<div class="flow">{{flowHtml}}</div>
</header>
<section class="comparison">
<article class="panel">
<div class="panel-head"><i class="dot"></i><span class="label">ANSI terminal</span><span class="native">SGR + cells</span></div>
<pre class="terminal">{{ansiForBrowser}}</pre>
<div class="panel-head"><i class="dot"></i><span class="label">{{WebUtility.HtmlEncode(leftLabel)}}</span><span class="native">{{WebUtility.HtmlEncode(leftNative)}}</span></div>
{{PanelBody(leftClass, leftContent)}}
</article>
<article class="panel">
<div class="panel-head"><i class="dot"></i><span class="label">HTML browser</span><span class="native">semantic elements</span></div>
<div class="browser">{{html}}</div>
<div class="panel-head"><i class="dot"></i><span class="label">{{WebUtility.HtmlEncode(rightLabel)}}</span><span class="native">{{WebUtility.HtmlEncode(rightNative)}}</span></div>
{{PanelBody(rightClass, rightContent)}}
</article>
</section>
<pre class="source">{{escapedAnsi}}</pre>
</main>
</body>
</html>
Expand Down
12 changes: 7 additions & 5 deletions docs/showcase/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# README showcase captures

The two README screenshots are generated from MarkupString values rather than recreated by hand.
The README screenshots are generated from MarkupString values rather than recreated by hand.
The checked-in captures use this supported Linux x64 environment:

- the .NET SDK selected by the repository's `global.json`;
Expand All @@ -23,7 +23,9 @@ present. After restoring the repository once, run from its root:
docs/showcase/capture.sh
```

Each page places the same value's ANSI rendering beside its semantic HTML rendering. The ANSI side
is parsed back only to make its terminal styling visible in the browser capture; its text and SGR
styling come from `Render(MarkupFormat.Ansi)`. The capture script applies rounded alpha masks and
transparent padding so the dark cards sit cleanly on both GitHub README themes.
The format pages place the same value's ANSI rendering beside its semantic HTML rendering. The
theme page places the same layout under a preset and a generated palette. ANSI is parsed back only
to make its terminal styling visible in the browser capture; its text and SGR styling come from
`Render(MarkupFormat.Ansi)`. The capture script applies rounded alpha masks and transparent padding
so the dark cards sit cleanly on both GitHub README themes, and omits volatile PNG timestamps so a
repeat capture is byte-for-byte identical.
4 changes: 3 additions & 1 deletion docs/showcase/capture.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ capture() {
"$raw"

magick composite -compose CopyOpacity "$mask" "$raw" "$clipped"
magick "$clipped" -bordercolor none -border 18 "$asset_dir/$asset"
magick "$clipped" -bordercolor none -border 18 \
-define png:exclude-chunk=date,time "$asset_dir/$asset"
}

capture box-drawing.html showcase-box-drawing.png
capture image-drawing.html showcase-image-drawing.png
capture theming.html showcase-theming.png
Loading