@grovemotorco/livery-core
v1.0.0
Published
Deterministic SVG renderer for code snippet images
Readme
@grovemotorco/livery-core
The renderer behind Livery: turns a config object into an SVG image of syntax-highlighted source code, plus the layout metrics that produced it.
npm install @grovemotorco/livery-coreWhy SVG only
Tools in this space usually preview with DOM + CSS and export with a headless
browser, and the two drift — fonts substitute, sub-pixel rounding differs,
backdrop-filter silently disappears. Here SVG is the only render target,
and it is the same string whether you inline it in a page, rasterize it in a
Worker, or rasterize it on a laptop.
Because code is monospace, the renderer computes every glyph position itself rather than trusting each renderer's text shaping:
charWidth = fontSize × font.advance // 0.6 for JetBrains Mono
x = codeLeft + column × charWidth
baseline = lineTop + (lineHeight − (asc + desc) × fontSize) / 2 + asc × fontSizeThe metrics come out of the vendored TTFs, not from guesses.
Rendering an SVG
import { resolveConfig, renderSnippet } from "@grovemotorco/livery-core";
const config = resolveConfig({
code: "const answer = 42;\n",
language: "ts",
theme: "grove-dark",
});
const { svg, width, height, layout, themeBg } = await renderSnippet(config);resolveConfig validates and fills in defaults — it is a Zod schema, so
unknown or out-of-range values throw rather than rendering something surprising.
renderSnippet returns a RenderResult: the svg string, its width and
height, the full layout, and the theme's background and foreground colours
(handy for matching surrounding chrome).
Pass { fontFaceCss } when the SVG has to stand alone — for a canvas export, or
any context that has not already loaded the fonts. Pass { idPrefix } when
several SVGs share a page, so their generated element ids do not collide.
There is also renderSnippetSync(config, highlighted, options) if you already
hold a Highlighted result and want to stay off the event loop.
Rasterizing to PNG
Rasterizing is opt-in, because it costs a ~2.5 MB WASM binary. You supply the binary and the font bytes; the package never does I/O itself, which is what lets it run unchanged in a Worker, in Node, and in the browser.
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import {
FontRegistry,
initRasterizer,
rasterizeToPng,
renderSnippet,
resolveConfig,
} from "@grovemotorco/livery-core";
const require = createRequire(import.meta.url);
await initRasterizer(await readFile(require.resolve("@resvg/resvg-wasm/index_bg.wasm")));
// Fonts ship with this package and are reachable as subpath exports.
const fonts = new FontRegistry(async (path) => {
try {
return new Uint8Array(
await readFile(require.resolve(`@grovemotorco/livery-core/fonts/${path}`)),
);
} catch {
return null;
}
});
const config = resolveConfig({ code: "const answer = 42;\n", language: "ts" });
const { svg, width } = await renderSnippet(config);
const png = await rasterizeToPng(svg, {
width: width * 2, // scale
fontBuffers: await fonts.buffersFor(config.font.family),
fontFamily: config.font.family,
});initRasterizer is safe to call repeatedly — only the first call does work, and
a failure is not cached, so a transient fetch error can be retried.
Backgrounds
resvg performs no I/O, so any background image has to be embedded before rendering or it comes out blank:
import { inlineBackgroundImage, renderSnippet } from "@grovemotorco/livery-core";
const embedded = await inlineBackgroundImage(config, { loadAsset });
const { svg } = await renderSnippet(embedded);Remote URLs are restricted to https, 5 MB and a 5 s timeout. Bundled backdrops
resolve through the loadAsset loader you provide, the same shape as fonts.
What ships
dist/index.mjs the renderer (ESM), with dist/index.d.mts alongside
fonts/ six OFL monospace families, as TTF
backdrops/ the bundled wallpapersFonts and backdrops are reachable as subpath exports —
@grovemotorco/livery-core/fonts/jetbrains-mono/regular.ttf — so a bundler can
fingerprint them like any other asset.
TTF and not WOFF2 because resvg's font parser cannot decompress WOFF2. Using one file format for the browser and the rasterizer removes a whole class of "the export doesn't match the preview" bugs.
Shiki grammars and themes are listed explicitly and imported on demand, so a bundler emits chunks only for the languages you actually reach — not for all ~350 grammars.
Limits worth knowing
- No
backdrop-filter. SVG has no equivalent and neither does resvg; window translucency is a plain alpha fill over the backdrop. - No conic gradients. SVG has none. The
meshbackground type stacks radial gradients to cover the same ground.
Requirements
Node.js 20 or newer, or any modern browser or edge runtime. ESM only.
Related
@grovemotorco/livery-cli— theliverycommand, if you just want images- Source and the hosted editor
Licence
MIT. Bundled fonts are OFL 1.1 and ship their own LICENSE.txt beside the TTFs,
and the Grove Motor Co wallpapers are not covered by the MIT grant — see
LICENSE.
