Python DSL SVG page builder and reconciliation CLI
Project description
folio
CLI for creating, building, rasterizing, validating, and reconciling SVG pages from a Python DSL.
Quick start
Install the published CLI package and run the folio command:
pipx install folio-dsl
# or: pip install folio-dsl
folio --version
For local development from this checkout:
python -m venv .venv
source .venv/bin/activate
pip install -e .
# bundled starter spec
folio validate config/folio.py
folio build config/folio.py
folio build config/folio.py idml # writes one .idml per document
folio reconcile out/cover.svg --spec config/folio.py
folio rasterize out/cover.svg --output out/cover.png
# scaffold from the built-in starter template
folio create my-doc \
--var brand_name="ACME" \
--var document_title="Ship layouts from Python." \
--var document_kicker="STARTER TEMPLATE"
# self-contained project dir (preferred for real specs)
cd my-doc
folio validate # resolves ./build.py by default
folio check # validate + lint + typecheck (add --format or --fix as needed)
folio build
folio reconcile out/page1.svg --spec .
folio rasterize --spec .
Template scaffolding
folio create TARGET_DIR scaffolds Folio's built-in starter template into a new target directory.
Use repeated --var key=value pairs to fill variables.
The starter produces a complete two-page project with:
build.pyrender entrypointpages.py,layout.py,theme.pycontent.pyrendered fromcontent.py.j2with sensible Jinja defaultsassets/.gitkeepand.gitignore
Only template-marked files are rendered (.j2, .jinja, .jinja2); other files are copied verbatim.
Defaults live in the template itself, so folio create my-doc works without any --var flags.
Starter metadata lives in src/folio/templates/starter/template.yaml; keep that file descriptive and let content.py.j2 remain the source of truth for default values.
DSL reference (folio docs)
The full DSL surface is documented inside the installed package. Agents and humans look symbols up through the CLI instead of grepping the source:
folio docs search text # find primitives related to text
folio docs show page # signature, params, examples, source
folio docs show folio.dsl.rect # fully-qualified lookup
folio docs list --kind=token # every design token
folio docs list --kind=style # every preset TextStyle
folio docs show page --format=json # machine-readable view for agents
The index ships inside the installed wheel, so folio docs works from
any working directory regardless of where the Folio source lives.
Regenerate the committed index with python -m folio.docs.generate
after adding or modifying public DSL symbols.
Agent skill
Agent-driven work on Folio projects is supported by a bundled skill
shipped inside the installed package. It installs into the
agent-neutral .agents/skills/folio/ directory so it is not tied to any
one agent tool. Clients that read skills from a different path (for
example Claude Code's .claude/skills/) can symlink it.
folio create installs the skill into new projects by default at
.agents/skills/folio/; you can also install it on demand:
folio skill install # project scope (./.agents/skills/folio)
folio skill install --scope=user # ~/.agents/skills/folio
folio skill install --force # overwrite divergent installs
# if your agent tool reads from a client-specific path:
ln -s ~/.agents/skills/folio ~/.claude/skills/folio
The skill tells the agent to use folio docs show / folio docs search
for DSL lookups, and to follow the standard
check → build → rasterize → reconcile pipeline.
Build outputs
folio build writes one SVG file per page by default. Specs can return a multi-document
collection so document-oriented formats can emit one artifact per publication:
from folio.dsl import collection, document
def build():
return collection(
document(
"brochure",
pages=[build_page1(), build_page2()],
filename="TM42_brochure",
title="TM42 Brochure",
),
document(
"tv_16x9",
pages=[build_page3()],
filename="TM42_tv_16x9",
title="TM42 TV 16:9",
),
)
Declare export presets in the DSL and build them by target name:
from folio.dsl import collection, document, idml, page, pdf, png, svg
def build():
return collection(
document(
"brochure",
pages=[build_page1(), build_page2(), build_page3(extra_exports=["1080p", "4k"])],
filename="TM42_brochure",
title="TM42 Brochure",
default_exports=["svg"],
export_presets=[
svg(),
png("1080p", viewport=(1920, 1080)),
png("4k", viewport=(3840, 2160)),
pdf(source="1080p"),
idml(),
],
)
)
folio build writes default exports, folio build 1080p pdf writes only those
named targets, and folio build all writes every declared preset. IDML and PDF
are document-scoped outputs; SVG and PNG are page-scoped outputs. Export targets
can depend on other presets: pdf(source="1080p") builds a PDF from the named
PNG preset without requiring users to build 1080p first or write unrequested
PNG files to out/. PDF exports are currently raster-backed for visual fidelity;
true SVG-to-vector-PDF conversion is planned separately. The IDML exporter maps common Folio
primitives such as rectangles, text frames, lines, ovals, polygons, and
polylines. This is an editable-structure MVP, not a full
SVG compatibility layer: filters, gradients, defs, arbitrary SVG path commands,
and image assets need additional mapping work.
DSL entrypoint
A starter spec lives at config/folio.py.
For real projects, the preferred layout is a self-contained directory with build.py at the top level.
Implicit CLI lookup resolves ./build.py; other spec files still work when passed explicitly.
Larger or private specs can live in any other Python module and still use the same public DSL from folio.dsl:
page()rect(),circle(),ellipse(),polygon(),polyline(),text(),multiline(),tspan()/span(),image(),group(),path(),line(),rule(),triangle(),qr()block()for scoped IDs and local coordinates inside reusable componentsPathBuilder/path_builder()for mm-based path construction- defs helpers like
linear_gradient(),linear_gradient_stops(),radial_gradient(),filter_(),drop_shadow(),gaussian_blur(),offset(),component_transfer(),func_a(),merge(),merge_node(),clip_path(),mask() render()tokensmarkup()for trusted non-escaped text fragments
page(), group(), and defs helpers accept either the existing list/tuple style or slimmer variadic children.
qr() renders as ordinary SVG paths/rects, so it works without adding a renderer-specific element kind.
text() accepts either a plain string or a sequence of strings / tspan() fragments.
Use markup() instead of the old raw=True escape hatch when you need trusted literal markup.
page() also accepts width_mm= / height_mm= so you can use presets like tokens.A5, tokens.DL, tokens.US_LETTER, or tokens.ROLLUP_850x2000.
tokens.extend(...) lets a spec register additional named hex colors so validation treats them like first-class palette tokens.
Text presets live under tokens.STYLES.*, and they are callable: tokens.STYLES.hero(...), tokens.STYLES.kicker.span(...), and tokens.STYLES.body.multiline(...) all build the corresponding nodes while still allowing keyword overrides.
from folio.dsl import block, page, rect, render, tokens
T = tokens.STYLES
card = block("card", at=(20, 40))
document = render(
page(
rect("cover_bg", 0, 0, tokens.A4_WIDTH_MM, tokens.A4_HEIGHT_MM, fill=tokens.INK),
T.hero("headline", 16, 24, "Hello DSL", size_pt=22),
card.layer(
"Card",
card.rect("panel", 0, 0, 64, 24, fill=tokens.SOFT, rx_mm=2.0, ry_mm=2.0),
card.text("body", 4, 8, "Shorter, still explicit.", style=T.body),
card.rule("sep", 0, 18, 64, fill=tokens.LINE, opacity=0.4),
),
page_id="cover",
filename="cover.svg",
page_number=1,
)
)
Layout
src/folio/cli.py— Typer entrypointsrc/folio/commands/— CLI command modulessrc/folio/dsl/— canonical document model, DSL loader, and SVG renderersrc/folio/render/— generic SVG primitives and design tokenssrc/folio/reconcile/— SVG parse/diff/reportsrc/folio/layout/— reusable layout helpers (Columns,Grid,cols(),grid(),flow_cols())config/folio.py— starter Python DSL spec
Recommended spec split pattern
For larger specs, prefer a self-contained project directory.
Because the loader inserts the spec directory on sys.path, build.py can import sibling modules directly.
Passing either the file path or the project directory works: folio build my-doc/build.py and folio build my-doc/ resolve the same spec.
Recommended structure:
my-doc/build.py— main spec entrypointmy-doc/content.py— content constants and copy blocksmy-doc/layout.py— reusable helpers, components, defs, and layout utilitiesmy-doc/pages.py— page-level assembly helpers that returnpage(...)nodes when a spec grows largemy-doc/assets/— source images referenced by the specmy-doc/.cache/— build/reconcile/raster cache written beside the specmy-doc/out/— rendered SVG, PNG, PDF, or IDML output (default when building that spec, even if the CLI is invoked elsewhere)
Layout helpers are available both as classes and convenience builders:
cols(n, inside=(left, right), gap=...)grid(cols, rows, inside=(x, y, width, height), col_gap=..., row_gap=...)flow_cols(n=3, inside=(x, y, width), gap=..., arrow_w=...)
Charts
chart() turns a matplotlib figure into a folio Element (kind IMAGE) that participates in the normal layout, reconcile, and SVG-embedding pipeline.
It is an optional feature — install with pip install 'folio-dsl[charts]' (or add matplotlib directly).
It accepts any library that writes into a matplotlib Axes (seaborn, pandas, matplotlib itself) or hands back a matplotlib Figure. Libraries that produce their own PNGs directly (Plotly static export, Altair) should keep using image().
Three shapes, one code path:
from folio.dsl import chart
# 1. Decorator — function takes an Axes. Most concise.
@chart("revenue", x_mm=20, y_mm=120, width_mm=80, height_mm=40)
def revenue(ax):
ax.plot(months, values)
# `revenue` is now an Element
# 2. Context manager — when you want the Figure object alongside the Axes.
handle = chart("trends", x_mm=20, y_mm=120, width_mm=80, height_mm=40)
with handle as ax:
ax.plot(...)
page(handle.element, ...)
# 3. Pre-built Figure — for libraries that hand back a Figure.
import seaborn as sns
fig = sns.lineplot(data=df).figure
element = chart("revenue", x_mm=20, y_mm=120, width_mm=80, height_mm=40).from_figure(fig)
Rendered PNGs are content-addressed by SHA-256 of the PNG bytes, so builds that produce identical figures reuse the cache. The cache lives at <spec>/.folio-cache/charts/ by default; override with FOLIO_CHART_CACHE_DIR or the cache_dir= kwarg.
dpi=300 is the default (print-ready); drop to 150 for drafts. transparent=True is the default so page tokens show through chart backgrounds.
Notes
- Images are embedded as base64 data URIs.
- Cache lives beside the spec at
.cache/folio/<spec-cache-key>/so multiple specs in one directory do not collide. folio rasterize <svg>rasterizes an arbitrary SVG to PNG; without an SVG argument it rasterizes cached last-build pages for a spec.folio rasterizeprefers Playwright, then falls back to CairoSVG,rsvg-convert, or Inkscape when available.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file folio_dsl-0.1.0.tar.gz.
File metadata
- Download URL: folio_dsl-0.1.0.tar.gz
- Upload date:
- Size: 466.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
955188499334fca5c20eedd93de5bbf0a1b924661097bf2a035e07a1c3330a08
|
|
| MD5 |
fba134e40841a7af4ebb2ea35a90667d
|
|
| BLAKE2b-256 |
5fb3cbed15d9aea04abd02f5178426ae78134e5cdfa77286b04e7be39c377f1a
|
Provenance
The following attestation bundles were made for folio_dsl-0.1.0.tar.gz:
Publisher:
release.yml on barisgit/folio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
folio_dsl-0.1.0.tar.gz -
Subject digest:
955188499334fca5c20eedd93de5bbf0a1b924661097bf2a035e07a1c3330a08 - Sigstore transparency entry: 1748836105
- Sigstore integration time:
-
Permalink:
barisgit/folio@ee5073193fe29caf300dc07076bf9184e92e8989 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/barisgit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ee5073193fe29caf300dc07076bf9184e92e8989 -
Trigger Event:
push
-
Statement type:
File details
Details for the file folio_dsl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: folio_dsl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 482.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e725acdb4f8c5968a96d66c7f287e475b9aea7445beefb180e7235a6a47195d4
|
|
| MD5 |
d2548e4adb0384998db5c8bc2131f7e4
|
|
| BLAKE2b-256 |
b2e53227976f6e03dbbe2abdb6cabf4190e997d8e77807077c1f68d86f242430
|
Provenance
The following attestation bundles were made for folio_dsl-0.1.0-py3-none-any.whl:
Publisher:
release.yml on barisgit/folio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
folio_dsl-0.1.0-py3-none-any.whl -
Subject digest:
e725acdb4f8c5968a96d66c7f287e475b9aea7445beefb180e7235a6a47195d4 - Sigstore transparency entry: 1748836337
- Sigstore integration time:
-
Permalink:
barisgit/folio@ee5073193fe29caf300dc07076bf9184e92e8989 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/barisgit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ee5073193fe29caf300dc07076bf9184e92e8989 -
Trigger Event:
push
-
Statement type: