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
188 changes: 118 additions & 70 deletions docs/Beck.Docs/Content/docs/guides/pennington.md
Original file line number Diff line number Diff line change
@@ -1,95 +1,119 @@
---
title: Add Beck to a Pennington site
description: Render fenced beck diagrams to static SVG at build time with a code-block preprocessor, measure with your own fonts, and adopt your MonorailCSS palette.
description: Register the Pennington.Beck package so fenced beck diagrams render to static SVG at build time, pick styles per fence or site-wide, measure with your own fonts, and adopt your MonorailCSS palette.
order: 21
sectionLabel: Setup
uid: docs.guide.pennington
---

This guide shows you how to add Beck to a [Pennington](https://usepennington.github.io/pennington/)
site so a fenced ` ```beck ` block renders to a static, self-animating `<svg>` at **build time** — no
client JavaScript. For a generic (non-Pennington) ASP.NET host, see [Add Beck to your
site](/docs/guides/install) instead.
client JavaScript. Pennington ships a first-class integration package, **`Pennington.Beck`**, so the
whole wiring is one package reference and one service registration. For a generic (non-Pennington)
ASP.NET host, see [Add Beck to your site](/docs/guides/install) instead.

## Add the package

Beck renders with **`Beck`**, a pure-C# package. Add **`Beck.Skia`** too — the
preprocessor below measures text with it so cards match your fonts exactly (strongly recommended):

```bash
dotnet add package Beck
dotnet add package Beck.Skia
dotnet add package Pennington.Beck
```

## Render fenced diagrams

Pennington hands every fenced code block to any `ICodeBlockPreprocessor` you register. Add one that
renders the `beck` language through `BeckSvg.Render` and returns finished HTML:
Then register it in `Program.cs`:

```csharp
using Beck.Rendering;
using Beck.Skia;
using Beck.Rendering.Text;
using Pennington.Markdown.Extensions;

// Turns a ```beck (inline YAML) or ```beck:symbol (a .beck.yaml file path) fence into a static SVG.
// Priority 500 runs it before the source-embed preprocessor, so a `beck` fence is never treated as
// a plain source listing. Add `,static` to a fence for a still frame.
public sealed class BeckFencePreprocessor : ICodeBlockPreprocessor
{
public int Priority => 500;
builder.Services.AddPenningtonBeck();
```

private readonly BeckFontSpec _font = new()
{
Family = "IBM Plex Sans",
MonoFamily = "IBM Plex Mono",
Files = new Dictionary<int, string>
{
[400] = "wwwroot/fonts/IBMPlexSans-Regular.ttf",
[600] = "wwwroot/fonts/IBMPlexSans-SemiBold.ttf",
[700] = "wwwroot/fonts/IBMPlexSans-Bold.ttf",
},
MonoFiles = new Dictionary<int, string> { [400] = "wwwroot/fonts/IBMPlexMono-Regular.ttf" },
};
That is the whole integration. `Pennington.Beck` hooks the shared code-block pipeline (it works the
same on `AddPennington`, `AddDocSite`, and `AddBlogSite` hosts), so the next ` ```beck ` fence in any
Markdown page renders as an inline SVG — on the live dev server and in the static build. It brings
the `Beck` engine with it; a malformed diagram fails loud with a visible error box and a diagnostics
entry rather than silently vanishing.

private readonly SkiaTextMeasurer _measurer;
public BeckFencePreprocessor() => _measurer = new SkiaTextMeasurer(_font);
## Tune a fence with flags

public CodeBlockPreprocessResult? TryProcess(string code, string languageId)
{
var id = languageId.Trim();
if (id != "beck" && !id.StartsWith("beck:") && !id.StartsWith("beck,")) return null;

bool isFile = id.Contains(":symbol");
var animation = id.Contains(",static") ? AnimationMode.Static : AnimationMode.Full;
string yaml = isFile ? File.ReadAllText(Path.GetFullPath(code.Trim())) : code;

string svg = BeckSvg.Render(yaml, new SvgRenderOptions
{
Measurer = _measurer, // exact card sizing over your fonts (see below)
Font = _font,
Animation = animation,
});
// SkipTransform: this is finished HTML — the highlighter must not reprocess it.
return new CodeBlockPreprocessResult($"<div class=\"beck-embed\">{svg}</div>", "beck", SkipTransform: true);
}
}
A comma-separated tail after the language adjusts one fence without touching its YAML, and flags
combine (`beck:symbol,static` works):

| flag | effect |
|---|---|
| `static` | Renders the fully-revealed final frame with no motion. |
| `scrub` | Drives the choreography from scroll position instead of a looping timeline. |
| `style=<name>` | Overrides the document's `meta.style` — render one YAML in any built-in look. |

## Pick a style

Beck ships [nine built-in styles](/docs/guides/styles) — `classic`, `minimal`, `terminal`,
`blueprint`, `glow`, `brutalist`, `sketch`, `extrude`, and `circuit`. There are three places to set
one, from narrowest to widest scope:

**Per document**, in the YAML itself:

```yaml
type: architecture
meta:
style: sketch
```

**Per fence**, with the `style=` flag — a last-word override that beats the document's own
`meta.style`, handy when one shared `.beck.yaml` should appear in different looks:

````markdown
```beck,style=sketch
type: architecture
nodes: [ ... ]
```
````

Register it in `Program.cs`:
**Site-wide**, as the default for every fence via `BeckOptions.RenderOptions` (an individual
document's `meta.style` still opts back out):

```csharp
builder.Services.AddSingleton<ICodeBlockPreprocessor, BeckFencePreprocessor>();
builder.Services.AddPenningtonBeck(beck =>
{
beck.RenderOptions = new SvgRenderOptions { Style = BeckStyles.ByName["sketch"] };
});
```

Custom styles you register in `SvgRenderOptions.Styles` are addressable the same three ways — see
[Author a custom style](/docs/guides/custom-styles) and the
[style system reference](/docs/reference/styles) for resolution and precedence.

## Measure with your site's own fonts

By default Beck measures text against embedded Inter/IBM Plex Mono metrics, and every label carries
a `textLength` guard so a font mismatch squeezes glyphs slightly instead of breaking layout. If your
site renders diagrams in different fonts, cards drift a little tight or roomy — for anything you
publish, add the optional **`Beck.Skia`** package and point a `SkiaTextMeasurer` at the `.ttf` files
your CSS actually serves:

```bash
dotnet add package Beck.Skia
```

Now any ` ```beck ` fence in your Markdown renders to an inline SVG at build time.
```csharp
var font = new BeckFontSpec
{
Family = "IBM Plex Sans",
MonoFamily = "IBM Plex Mono",
Files = new Dictionary<int, string>
{
[400] = "wwwroot/fonts/IBMPlexSans-Regular.ttf",
[600] = "wwwroot/fonts/IBMPlexSans-SemiBold.ttf",
[700] = "wwwroot/fonts/IBMPlexSans-Bold.ttf",
},
MonoFiles = new Dictionary<int, string> { [400] = "wwwroot/fonts/IBMPlexMono-Regular.ttf" },
};

> [!IMPORTANT]
> **Measure with your site's own fonts.** The preprocessor above wires a `SkiaTextMeasurer` at the
> `.ttf` files your CSS serves, so Beck sizes each card to the text the browser will actually draw.
> `BeckSvg.Render(yaml)` also works with no measurer at all — a built-in default measures against
> Inter/IBM Plex Mono metrics — but if your site renders diagrams in different fonts, cards drift a
> little tight or roomy. Skia is the recommended setup for anything you publish.
builder.Services.AddPenningtonBeck(beck =>
{
beck.RenderOptions = new SvgRenderOptions
{
Font = font,
Measurer = new SkiaTextMeasurer(font),
};
});
```

## Adopt your MonorailCSS palette

Expand Down Expand Up @@ -130,9 +154,9 @@ render time. See [Match your theme and colours](/docs/guides/theme) for the full

Because Pennington ships TreeSitter `:symbol` source embeds, you can keep a diagram's YAML in one
`.beck.yaml` file and show both its source and its render from that single file — no duplication.
Pull the source with a `yaml:symbol` fence, then render the same file with a `beck:symbol` fence
the preprocessor turns it into static SVG at build time. Add `,static` (`beck:symbol,static`) for a
still frame:
Pull the source with a `yaml:symbol` fence, then render the same file with a `beck:symbol` fence
(the body is one file path per line; each file renders independently). All the fence flags apply
(`beck:symbol,static`, `beck:symbol,style=sketch`):

````markdown
```yaml:symbol
Expand All @@ -150,9 +174,33 @@ wwwroot/examples/guides/pennington-platform.beck.yaml
wwwroot/examples/guides/pennington-platform.beck.yaml
```

Paths resolve against `BeckOptions.ContentRoot`, which defaults to the working directory — set it to
match your TreeSitter `ContentRoot` so both `:symbol` forms address files the same way:

```csharp
builder.Services.AddPenningtonBeck(beck =>
{
beck.ContentRoot = "../..";
});
```

This is the convention these docs use throughout, showing each diagram's source beside the diagram
itself.

Next, learn the language in [Your first diagram](/docs/tutorials/first-diagram), generate diagrams
from your model in [Generate diagrams from your code](/docs/guides/generate), or fine-tune colours in
## Fullscreen zoom

Each rendered embed carries a zoom button that opens the diagram in a full-screen lightbox — the
package's one piece of client JavaScript; rendering stays server-side. Turn it off to emit bare SVG
with no client behavior:

```csharp
builder.Services.AddPenningtonBeck(beck =>
{
beck.Zoom = false;
});
```

Next, learn the language in [Your first diagram](/docs/tutorials/first-diagram), browse the looks in
[Pick a built-in style](/docs/guides/styles), generate diagrams from your model in
[Generate diagrams from your code](/docs/guides/generate), or fine-tune colours in
[Match your theme and colours](/docs/guides/theme).
1 change: 1 addition & 0 deletions docs/Beck.Docs/Content/docs/reference/yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ the layered types — architecture, state, class.)
| `subtitle` | string | — | Muted line under the title. |
| `direction` | `TB` `BT` `LR` `RL` | `TB` | Primary layout axis: top-to-bottom, bottom-to-top, left-to-right, right-to-left. |
| `theme` | `auto` `light` `dark` | `auto` | `auto` follows the host page. |
| `style` | string | `classic` | Visual style token — one of the nine built-ins (`classic`, `minimal`, `terminal`, `blueprint`, `glow`, `brutalist`, `sketch`, `extrude`, `circuit`) or a registered custom style. See [Pick a built-in style](/docs/guides/styles) and the [style system reference](/docs/reference/styles). |
| `animate` | bool | `true` | `false` renders a static frame and never loads the motion runtime. |
| `loop` | bool | `true` | `false` plays the flow once (forces `flow.repeat: 0`). |
| `fit` | `shrink` `scroll` | `shrink` | What a diagram wider than its container does: `shrink` scales it down to fit; `scroll` keeps it at natural size and scrolls horizontally. Vertical size is never constrained. |
Expand Down
9 changes: 6 additions & 3 deletions docs/Beck.Docs/Content/for-ai.llms.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ field tables and defaults, see the [YAML schema reference](/docs/reference/yaml)
| `title`, `subtitle` | string | — |
| `direction` | `TB` `BT` `LR` `RL` | `TB` |
| `theme` | `auto` `light` `dark` | `auto` (follows host page) |
| `style` | `classic` `minimal` `terminal` `blueprint` `glow` `brutalist` `sketch` `extrude` `circuit` (or a registered custom name) | `classic` |
| `animate` | bool | `true` |
| `loop` | bool | `true` (`false` plays once) |
| `fit` | `shrink` `scroll` | `shrink` |
Expand Down Expand Up @@ -352,9 +353,11 @@ Beck renders diagrams with the pure-C# engine — there is no client JS, no npm
Two rendering paths:

- **Fenced block (main path):** write a ` ```beck ` block (inline YAML) or a ` ```beck:symbol ` block
(pointing at a `.beck.yaml` file) in any Markdown page. The Pennington preprocessor runs the engine
at build time and inlines a static, self-animating `<svg>` — no script tag, no client runtime. Flag
variants force the static frame: ` ```beck,static ` and ` ```beck:symbol,static `.
(pointing at a `.beck.yaml` file) in any Markdown page. The `Pennington.Beck` package's
preprocessor runs the engine at build time and inlines a static, self-animating `<svg>` — no
script tag, no client runtime. Comma flags tune one fence: `,static` forces the still frame,
`,scrub` drives playback from scroll, and `,style=<name>` overrides the document's `meta.style`
(they combine, e.g. ` ```beck:symbol,style=sketch,static `).
- **C# / ASP.NET:** `BeckSvg.Render(yaml)` from the `Beck` package returns a self-contained
`<svg>` string you write into a page (server-side or at build time).

Expand Down
2 changes: 1 addition & 1 deletion docs/Beck.Docs/wwwroot/js/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@
dialog.setAttribute('aria-label', 'Diagram, full screen');

var clone = svg.cloneNode(true);
// Drop the engine's inline sizing (max-width cap + height:auto) so the lightbox
// Drop the engine's inline sizing (max-width:100% + height:auto) so the lightbox
// CSS controls the box: natural size from the width/height attributes, shrunk
// proportionally only when it exceeds the viewport.
clone.removeAttribute('style');
Expand Down
6 changes: 5 additions & 1 deletion src/Beck/Svg/SvgRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,13 @@ public static string Render(DiagramModel model, ITextMeasurer measurer, string h
animCss = "@media (prefers-reduced-motion:no-preference){" + motionCss + "}";
}

// meta.fit: `shrink` (default) lets the SVG scale down inside a narrow container
// (the width attribute already caps it at natural size in wide ones); `scroll`
// pins natural size so the host's overflow container scrolls instead.
var maxWidth = model.Meta.Fit == FitMode.Scroll ? $"{N(w)}px" : "100%";
var svg = new StringBuilder();
svg.Append($"<svg class=\"beck-svg b-{hash}\" viewBox=\"0 0 {N(w)} {N(totalH)}\" width=\"{N(w)}\" height=\"{N(totalH)}\" ")
.Append($"style=\"max-width:{N(w)}px;height:auto\" font-family=\"var(--beck-font)\" role=\"img\" aria-label=\"{SvgWriter.Attr(model.Meta.Title ?? "diagram")}\">");
.Append($"style=\"max-width:{maxWidth};height:auto\" font-family=\"var(--beck-font)\" role=\"img\" aria-label=\"{SvgWriter.Attr(model.Meta.Title ?? "diagram")}\">");
svg.Append("<style>").Append(Stylesheet.Emit(hash, font, mono, theme, style)).Append(animCss).Append("</style>");
svg.Append("<defs>").Append(markers.Defs).Append(extraDefs).Append(animDefs).Append(edgeDefs).Append(Stylesheet.StyleDefs(hash, style)).Append("</defs>");
svg.Append(TitleBlock(model, w, style));
Expand Down
2 changes: 1 addition & 1 deletion tests/Beck.Tests/Goldens/svg/class.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/Beck.Tests/Goldens/svg/seq-kitchen.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions tests/Beck.Tests/RenderSmokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,27 @@ public void RenderCorpusToFiles()
}
}

[Fact]
public void FitModeControlsInlineMaxWidth()
{
const string nodes = """
nodes:
- { id: a, title: Node A }
- { id: b, title: Node B }
edges:
- { from: a, to: b }
""";

// Default (shrink): responsive — scales down inside a narrow container.
var shrink = BeckSvg.Render(nodes);
Assert.Contains("style=\"max-width:100%;height:auto\"", shrink);

// fit: scroll pins natural size (the width attribute's value) so the host scrolls.
var scroll = BeckSvg.Render("meta:\n fit: scroll\n" + nodes);
Assert.DoesNotContain("max-width:100%", scroll);
Assert.Matches("style=\"max-width:[0-9.]+px;height:auto\"", scroll);
}

[Fact]
public void ScrubModeDrivesAnimationsOffTheViewTimeline()
{
Expand Down
2 changes: 1 addition & 1 deletion tools/oracle/rendered/arch-flow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tools/oracle/rendered/arch-grouped.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading