Skip to main content

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.py render entrypoint
  • pages.py, layout.py, theme.py
  • content.py rendered from content.py.j2 with sensible Jinja defaults
  • assets/.gitkeep and .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 components
  • PathBuilder / 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()
  • tokens
  • markup() 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 entrypoint
  • src/folio/commands/ — CLI command modules
  • src/folio/dsl/ — canonical document model, DSL loader, and SVG renderer
  • src/folio/render/ — generic SVG primitives and design tokens
  • src/folio/reconcile/ — SVG parse/diff/report
  • src/folio/layout/ — reusable layout helpers (Columns, Grid, cols(), grid(), flow_cols())
  • config/folio.py — starter Python DSL spec

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 entrypoint
  • my-doc/content.py — content constants and copy blocks
  • my-doc/layout.py — reusable helpers, components, defs, and layout utilities
  • my-doc/pages.py — page-level assembly helpers that return page(...) nodes when a spec grows large
  • my-doc/assets/ — source images referenced by the spec
  • my-doc/.cache/ — build/reconcile/raster cache written beside the spec
  • my-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 rasterize prefers Playwright, then falls back to CairoSVG, rsvg-convert, or Inkscape when available.

Metadata

Release files for folio-dsl 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for folio-dsl 0.2.0
File Size Uploaded
folio_dsl-0.2.0.tar.gz 470.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for folio-dsl 0.2.0
File Interpreter ABI Platform
folio_dsl-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 957.0 kB

Release files / folio_dsl-0.2.0.tar.gz

Download URL folio_dsl-0.2.0.tar.gz
Size 470.5 kB
Tags Source
SHA-256 checksum
How to use checksums
0044da80c9fd3a2c57725f44cba9cea87e86970662d61744f981fd09d87f4b26
BLAKE2b-256 checksum
How to use checksums
a362e1022a8e0784fe7d649377471bc0b91a9a9ae1771460273fbaed7469543d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 2, 2026.

Transparency log

Release files / folio_dsl-0.2.0-py3-none-any.whl

Download URL folio_dsl-0.2.0-py3-none-any.whl
Size 486.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c8c13788672e9d938f375e8960532b3e3f49c2eee6e6c7c6a1d41a4290be0a41
BLAKE2b-256 checksum
How to use checksums
8cd1be46ef45ffe312c3090a64a1ce7efe441bede25a2211b903fdf2d34d1cbb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page