astro-lini
v0.2.1
Published
Astro integration that compiles ```lini fences in Markdown and MDX to inline SVG at build time.
Maintainers
Readme
An Astro integration that compiles ```lini blocks in your
Markdown and MDX to inline SVG at build time.
```lini
|chart| "Signups by channel" { categories: "Jan", "Feb", "Mar", "Apr", "May", "Jun" } [
|line| "organic" { data: 14, 19, 26, 33, 44, 58; curve: smooth; marker: dot; stroke: --teal }
|line| "referral" { data: 9, 13, 15, 22, 27, 36; curve: smooth; marker: dot; stroke: --purple }
]
```That is a chart, but the fence is not a chart fence. Lini is one engine for every figure family — flowcharts, charts, sequences, mindmaps, trees, schematics, and technical drawings are all layouts of the same language. So one fence covers all of them, and there is nothing else to install.
- No runtime. Figures are SVG in the HTML. No JavaScript, no CDN, no browser at build time — the pages work with scripts off.
- No second toolchain. The compiler is a WebAssembly module resolved from npm like any
other dependency.
npm installplus one line of config is the whole thing. - Dark mode for free. Colours are live CSS variables, so figures follow your theme toggle without a re-render.
- The source is one click away. Each figure carries a small
</>toggle that reveals the Lini that drew it, syntax-highlighted at build time — still no JavaScript. A fence word flips it, so a reference page can lead with the source instead. - Fast and deterministic. A typical diagram compiles in about a millisecond, byte-identically each run.
Install
npm install astro-liniSetup
One line in astro.config.mjs. The styling ships with the figures:
import { defineConfig } from 'astro/config';
import lini from 'astro-lini';
export default defineConfig({
integrations: [lini()],
});That's it — no stylesheet to link, no files to copy. Markdown and MDX are both covered, on
Astro's own Markdown processor and on unified() alike.
Writing a figure
Any Lini source works. The tour walks every family and the reference covers it in full; this is the shape of it:
```lini
{ layout: sequence; font-size: 13; }
|icon#reader| "user" { width: 60; stroke: --rose-deep; fill: --rose-wash }
|box#cdn| "CDN" { fill: --sky-wash; stroke: --sky-deep }
|box#origin| "Origin" { fill: --green-wash; stroke: --green-deep }
reader -> cdn "GET /guide"
cdn -> cdn "check edge cache"
|loop| "on miss" { fill: --amber-wash; stroke: --amber-ink } [
cdn -> origin "fetch"
origin --> cdn "200 + max-age"
]
cdn --> reader "HTML"
```A block that references a local image with |image| src: resolves the path against the
Markdown file's own directory.
Blocks in other languages pass through untouched, as does a ```lini block quoted
inside a wider fence — so you can document Lini in a Lini-powered site.
Showing the source
Every figure carries a small </> in its top-right corner. Clicking it reveals the Lini
that drew it, highlighted with the same vocabulary the VS Code and Zed grammars use — the
colouring comes from Lini's own ledger, so a property gets its colour here the moment the
language has it.
The toggle is a checkbox and its label — pure CSS, so it costs no JavaScript, takes keyboard focus, and works with scripts off. The listing is your block's own text, verbatim, not reformatted.
It is deliberately not a <details>. Your site's stylesheet is unlayered, so it outranks
ours: a <details> gives your theme a second element to frame — a box inside the code
block's box — and it carries a disclosure marker your theme can put back however we hide
it. With a label there is no marker, and the <pre> is the only element your theme
dresses. One frame, like every other code block on the page.
Choosing what a block shows
A block has two views — the figure, and the source that drew it. One word on the fence names them, in the order they appear:
| fence | shows |
| --- | --- |
| ```lini | the figure, its source folded behind the button |
| ```lini figure | the same, said out loud |
| ```lini code | the source, the figure folded behind the button |
| ```lini figure-code | the figure, then its source — both on the page |
| ```lini code-figure | the source, then the figure — both on the page |
| ```lini figure-only | the figure, nothing else |
| ```lini code-only | the source, nothing else |
A single word names the lead and leaves the other view folded — that is the default, and
```lini figure is simply the default spelled out. A compound names both, in order, and
drops the toggle: reach for it when the source is the lesson, since a reader meeting the
language for the first time should not have to discover a button to see what drew the
picture. code-figure is the one a tutorial usually wants — listing first, then the result.
code-only never reaches the compiler. It does not need a rule of its own: a block showing
the source and nothing else has no figure to draw, so a fragment, a counter-example or a
deliberately broken line stays a highlighted listing instead of becoming an error box.
```lini code-only
|box#hero| "…" // a shape, not a whole file
```Whitespace or a comma both separate, so ```lini,figure reads the same. The words are
alternatives, each naming a whole arrangement — write two and the last wins. A word we
don't recognise is reported on stderr and ignored, never fatal.
Theming
Each figure is a <div class="lini-figure"> wrapping the SVG. Lini emits every colour as a
light-dark() pair keyed on color-scheme, and binding that property is the entire
light/dark integration.
color-scheme is inherited, so a site that sets it on :root — the modern way — needs
nothing at all:
:root { color-scheme: light dark; }The two conventions that don't set it are bound for you: a .dark class and a
data-theme="dark" attribute on <html>. Any other theme adds one line:
html.my-dark-theme :is(.lini, .lini-source, .lini-error) { color-scheme: dark; }To hand Lini your own palette, alias its role variables. Everything shipped sits in
@layer, so any unlayered rule of yours wins without !important:
.lini {
--lini-bg: transparent;
--lini-fg: var(--fg);
--lini-accent: #4a7fd4;
--lini-font-family: var(--body-font);
}Its eleven-hue palette (--rose, --sky, --teal, … each in five tiers) is emitted only
where a figure references it.
[!NOTE] Alias
--lini-font-familyonly to a proportional sans close in metrics to the one Lini measured against at compile time. Lini bakes each label's position and sizes its box to fit, so a wider face — a monospace one especially — pushes the text past its border.
Sizing
The wrapper carries --lini-w, the diagram's natural width. A figure scales down to fit
the column but stops at 75% of that width — past there the labels stop reading, so the
wrapper scrolls horizontally instead.
The source listing is not bound by that floor: it fills the column and scrolls horizontally on its own when a line is long.
Owning the styling
astro-lini's own stylesheet — the wrapper above, the theme binding, the error box, and the
source listing's palette — rides along in a <style> block on each page that has a figure.
About 3 kB minified, and none on pages without one. It is not Lini's styling: that lives
inside each SVG and travels with it regardless.
To take it over instead, turn it off and ship the sheet yourself:
integrations: [lini({ bundledCss: false })]liniCss() returns exactly what the bundled block contains — this package's sheet, layered,
plus Lini's token palette straight from the compiler, so it cannot go stale:
import { liniCss } from 'astro-lini';astro-lini.css is the same sheet as a plain file, without the palette,
for forking.
Errors
A block that fails to compile becomes a visible <pre class="lini-error"> on the page and a
message on stderr; the rest of the build carries on, so one bad diagram never costs you the
site. Warnings — an unroutable link, say — only go to stderr. Diagnostics carry the file
path and the real line number in the Markdown, not the line within the fence:
astro-lini: src/pages/guide.md:41:1: warning: impossible (a -> b): no legal routeA Markdown page at the site root
Astro's minimal template ships src/pages/index.astro. Adding
src/pages/index.md beside it gives you two routes for /, and Astro keeps the
.astro one:
[WARN] [build] Could not render `` from route `/` as it conflicts with higher
priority route `/`.Delete the .astro file if the Markdown page is the one you want. Nothing to do
with this integration — it catches everyone whose first Lini page is the home
page.
Beyond Astro
This package is the Astro half. Everything a fence becomes — the compile, the figure, the listing, the error box — lives in remark-lini, which this depends on. Outside Astro, install that one:
npm install remark-lini-langimport { remarkLini } from 'remark-lini-lang';
unified().use(remarkParse).use(remarkLini).use(remarkRehype);It works in Docusaurus, Next and MDX, Gatsby, or a bare unified(). remarkLini and
liniCss are still re-exported from here, so nothing that reached for them breaks, but new
code outside Astro should name remark-lini-lang directly.
satteriLini() is the same work as a plugin for Astro's own Markdown processor; the
integration picks whichever of the two your site's processor takes, and you should not need
to reach for either by hand.
Links
Everything about the language itself lives with Lini, not here:
- lini.rs — language reference and gallery
- github.com/monfa-red/lini — the compiler, its
SPEC.md, andsamples/for every figure family - mdbook-lini — the same fences, for an mdBook
License
MIT
