
go-slides/slides

🌐 Website · 📚 Documentation
Marp-compatible slide decks in pure Go (no cgo, no Node, no browser), with
two additions:
- Go templates in slides.
{{ .title }}, {{ range }} loops that
generate slides, {{ page }} / {{ total }}, {{ include "part.md" }}.
- Slide kinds. A slide says what it is (
kind: title) instead of
repeating how it looks. A kind is a named preset of header, footer, page
numbering, class and background.
---
theme: gaia
paginate: true
footer: "{{ .title }} — {{ page }}/{{ total }}"
title: Quarterly review
kind: content
data:
topics: [Revenue, Hiring, Roadmap]
---
<!-- _kind: title -->
# {{ .title }}
{{ range .topics }}
---
## {{ . }}
{{ end }}
go install github.com/go-slides/slides/cmd/slides@latest
slides deck.md # writes deck.html
Marp compatibility
The deck format is Marp's: Marpit
directives and Marp Core's flavour of
CommonMark. Its behaviour was ported from the Marpit and Marp Core sources
rather than from their documentation. Every case below is pinned by a test
against output that marp-cli 4.5.1 (Marp Core 4.4.0) produced; see
testdata/oracle.
- Slides split at
---, or at the headings that headingDivider names.
- Global, local and spot (
_name) directives, from the front matter or from
HTML comments. Loose YAML is on, as in Marp Core: backgroundColor: #eee
is a colour, not a comment.
header, footer (inline Markdown), paginate (true, false, hold,
skip), class, color, and the background* directives.
- The image syntax:
w: / h:, CSS filters, ![bg] backgrounds. Several
backgrounds and split backgrounds (bg left:40%) render as layers, as in
Marp's inline SVG mode.
- Fragmented lists (
* and 1)), speaker notes (the comments that are not
directives), and <style> / <style scoped>.
- Marp Core's built-in
default, gaia and uncover themes, the size
directive (16:9, 4:3), custom themes (@theme, @import, @size).
- GFM tables, strikethrough, URL linkify, line breaks as
<br>, heading ids
slugged as Marp Core does them.
- Raw HTML filtered through Marp Core's allowlist;
--html allows it all.
slides expand writes a deck back out as plain Marp Markdown, with templates
expanded and kinds replaced by the directives they stand for, so marp-cli and
the Marp editor extensions render it the same way. The kinds test renders the
expanded deck with Marp and requires the same HTML as slides produces for
the original.
Templates
The deck body is one Go text/template,
executed before the Markdown is parsed. Its data (the dot) is the front
matter, overlaid by the front matter's data: mapping, then by data in
slides.yaml, --data files and --set key=value flags. A missing key is an
error that names the file and line, rather than printing <no value>.
|
|
{{ page }}, {{ total }} |
the page number and the last page number, as Marp counts them (skip and hold slides do not advance it) |
{{ include "file.md" }} |
another file, expanded with the same data |
now, date, env |
{{ now | date "2006-01-02" }}, {{ env "USER" }} |
| strings |
upper lower title trim replace split join contains hasPrefix hasSuffix repeat quote |
| values |
default seq add sub mul list dict merge |
Front-matter values and kind definitions are templates too, so
footer: "{{ .title }} — {{ page }}" numbers every slide.
Code is left alone. Text inside fenced code blocks and inline code spans is
never interpreted, so a slide about Go templates can show {{ .Name }} as it
is. Front matter template: code interpolates code too, and
template: false turns templates off. delims: ["<%", "%>"] changes the
delimiters.
Kinds
kinds:
title: # redefines a built-in kind
class: lead
paginate: false
header: ""
footer: ""
quote:
extends: blank # starts from another kind
class: invert
backgroundColor: "#111"
kind: name applies a kind like a local directive: to this slide and to the
ones after it. _kind: name applies it to one slide.
- Switching kinds first clears what the previous kind set, so a class or a
footer never leaks from one kind into the next.
- Directives written by hand always win over the kind. They carry over to the
following slides as Marp's local directives do, kind switches included.
- A kind accepts the local directives:
backgroundColor, backgroundImage,
backgroundPosition, backgroundRepeat, backgroundSize, class,
color, footer, header, paginate. It also accepts extends.
- Each slide carries
data-kind="<name>", so a theme can style a kind
directly.
Built-in kinds: title (class lead, no header, footer or page number),
section (class lead), content (the deck's defaults) and blank
(nothing around the content). Only the gaia theme defines lead; with the
other themes, give title and section a class your theme knows.
Kinds come from, in increasing priority: the built-ins, slides.yaml,
--kinds files, and the deck's front matter kinds: key. That key takes either
a mapping or the path of a file, or a list of paths, so several decks can share
one file. slides kinds deck.md lists what a deck can use.
HCL
Everywhere slides reads YAML, it also reads
HCL2's native
syntax, for its expressions and functions:
| where |
YAML |
HCL |
| front matter |
--- … --- |
---hcl … --- |
| configuration |
slides.yaml |
slides.hcl (looked up first) |
| kinds files |
--kinds kinds.yaml, kinds: kinds.yaml |
any file named *.hcl |
| data files |
--data data.yaml / .json |
any file named *.hcl |
---hcl
theme = "gaia"
paginate = true
title = "Quarterly review"
data = {
company = "ACME"
quarter = 3
}
header = format("%s — Q%d", data.company, data.quarter)
footer = "${upper(data.company)} · ${page()}/${total()}"
date = formatdate("D MMMM YYYY", now())
kind "chapter" {
extends = "section"
class = ["lead", "invert"]
}
---
# {{ .title }}
- Attributes are the YAML keys under the same names: Marp's directives
(
theme, paginate, header, footer, class, backgroundColor, …),
the metadata (title, author, …), template, delims, kinds (file
paths) and data, an object. kind "<name>" { … } blocks define kinds,
in a front matter, in slides.hcl and in kinds files. slides.hcl takes
themes, html, lang, data and kinds attributes besides.
- Values reach the directives in the string forms YAML gives them
(
true is "true", a list of classes is a list), so a directive behaves
the same whichever syntax set it. null leaves a directive unset. data
keeps its types: in templates an integral number is an int, others a
float64, and lists and objects stay lists and maps.
- Variables:
data is the template data — the front matter's own data
overlaid by slides.hcl's, --data files and --set — so
"${data.company}" works wherever {{ .company }} would. Front-matter
attributes may reference data.* but not each other; spot directives
(_class…) are not HCL identifiers, so write them in a comment.
- Functions:
page() and total() (the page number and the last page
number, as {{ page }} and {{ total }}), now() and timestamp()
(RFC 3339, local and UTC), env(name), and file(path), which reads a
UTF-8 text file of at most 1 MiB relative to the deck or configuration
file. From go-cty's standard library: abs ceil floor log max min parseint pow signum, chomp format formatlist join lower regex regexall regexreplace replace split strlen strrev substr title trim trimprefix trimspace trimsuffix upper, chunklist coalesce coalescelist compact concat contains distinct element flatten keys length lookup merge range reverse slice sort values zipmap, csvdecode jsondecode jsonencode,
formatdate timeadd. None reaches the network.
- An HCL value is final: Go templates do not run over it a second time. The
deck body stays a Go template.
- Errors carry HCL's diagnostics, positioned in the deck file itself.
footer = data.compnay on the deck's fourth line gives
deck.md:4:14: Unsupported attribute: This object does not have an attribute named "compnay".
- Marp reads
---hcl as the start of a front matter too (its rule opens
on any first line beginning with three dashes) and, the block not being a
YAML mapping, applies none of it:
testdata/oracle/hcl-frontmatter.md
pins this against Marp Core. slides expand writes the front matter back
as YAML, so the expanded deck is plain Marp.
- Directive comments (
<!-- class: invert -->) stay YAML, as in Marp.
Command line
slides [build] [flags] deck.md write the presentation as HTML
slides expand [flags] deck.md print plain Marp Markdown
slides notes [flags] deck.md print the speaker notes
slides kinds [flags] deck.md list the kinds and what they set
slides themes [--theme-set ...] list the themes
slides version print the version (also --version)
slides completion bash|zsh|fish|powershell
Flags may come before or after the deck. slides <command> --help lists a
command's flags.
| flag |
commands |
|
-o, --output file |
build, expand, notes |
output file; - for standard output (build defaults to deck.html next to the deck) |
--html |
build |
allow all raw HTML |
--lang code |
build |
the lang attribute when the deck sets none |
--bare |
build |
stack the slides, without the presenting script |
--pdf |
build |
write a PDF, one page per slide, with an outline (also chosen by -o file.pdf) |
--pptx |
build |
write a PowerPoint deck: a picture per slide with its speaker notes, at scale 2 by default (also chosen by -o file.pptx) |
--odp |
build |
write an OpenDocument presentation: a picture per slide with its speaker notes, at scale 2 by default (also chosen by -o file.odp) |
--images png|jpeg |
build |
write one image per slide: deck.001.png, … |
--image png|jpeg |
build |
write the first slide as an image (also chosen by -o file.png / .jpg) |
--jpeg-quality n |
build |
JPEG quality, 1 to 100 (default 85) |
--image-scale n |
build |
device pixels per CSS pixel for images and PDF (default 1) |
--allow-local-files |
build |
let images and PDF read local files, from under the deck's directory only |
--theme-set path |
all |
a theme CSS file or a directory of them (repeatable) |
--kinds file |
all |
a YAML or HCL (*.hcl) file of kinds (repeatable) |
--data file, --set k=v |
all |
template data: YAML, JSON or HCL (*.hcl) files (repeatable) |
--no-template, --strict-yaml |
all |
plain Marpit behaviour |
--config file, --no-config |
all |
slides.hcl, else slides.yaml, is read from the deck's directory by default |
# slides.yaml
kinds: kinds.yaml # or a mapping, or a list of files
themes: [themes/acme.css]
data: {company: ACME} # or file paths
html: false
lang: en
# slides.hcl, the same in HCL
kinds = "kinds.hcl"
themes = ["themes/acme.css"]
data = { company = "ACME", year = 2026 }
html = false
lang = "en"
kind "quote" {
extends = "blank"
footer = "${data.company} ${data.year}"
}
The HTML page is self-contained. Navigate with the arrow keys, Space and
PageUp/PageDown, f toggles full screen, a click advances, and the URL hash
names the slide. Fragments reveal one at a time. Printing gives one slide per
page.
Images and PDF without a browser
Marp CLI drives a headless Chrome for its PDF and image output. slides uses
the go-* libraries instead:
-
go-webengine lays out and paints
each slide. The slides go through the box frame (deck.FrameBox), which
needs neither <foreignObject> nor generated content: the theme's
section::after is lowered to a real element carrying its evaluated content.
-
go-pdfkit writes the PDF: one page
per slide at the slide's size, and an outline of the slides and their
headings.
-
The fonts the built-in themes ask for (Lato, Roboto Mono) come from
go-opentype/fonts, embedded, so no
render touches the network.
-
--image-scale 2 renders at two device pixels per CSS pixel (2560×1440 for
16:9). Text and vectors are drawn at that density, not enlarged.
-
Local images are refused unless --allow-local-files is given, as with
marp-cli. Even then, only files under the deck's directory are read: a deck
from elsewhere cannot reach ~/.ssh.
Several and split backgrounds (![bg left:40%]) render as Marp's layers do.
Slides render four at a time and are written as they come, so a long deck is
never held in memory as images. Against marp-cli 4.5.1 on the same machine, a
100-slide deck exports to PNG in 0.97 s against 5.4 s and to PDF in 1.1 s
against 1.9 s, in a sixth of the memory; the PDF is larger, its slides being
images. See BENCHMARKS.md.
Lato (Regular, Bold, Black) and Roboto Mono (Regular, Bold) are embedded
upright and italic, so gaia's headings, code and emphasis render with their
own faces.
Math and code
- Math:
$…$ inline and $$…$$ display, with Marp Core's delimiter
rules (a $ followed by a space does not open, one followed by a digit does
not close). Formulas are typeset by
go-tex/math (STIX Two Math) into inline
SVG sized in em, so they follow the text's size and sit on its baseline,
in the HTML as in images and PDF. A formula that does not parse stays visible
as written.
- Code: fenced blocks are coloured by
go-rouge in highlight.js class names,
which is what Marp's themes style. A language go-rouge does not know is
written as plain text, as Marp Core does.
PowerPoint and OpenDocument
--pptx writes the deck through
go-ooxml's WritePresentation, and
--odp through go-odf's: one full-slide
picture per slide, at the slide's size, with its speaker notes and the deck's
title, author, description and keywords. As with marp-cli's PPTX, the slides
are pictures, not editable text. Both render at scale 2 unless --image-scale
says otherwise, and LibreOffice opens and converts both.
Fitting headings
# <!-- fit --> Title scales the heading so its one line fills the slide's
width, up or down, as Marp Core does. The HTML page and the image, PDF and
PPTX outputs all run the same small script: it measures the heading's natural
width and scales its font size by the ratio. Only slides with a fitting heading
run a script when exporting.
Roadmap
Packages
|
|
deck |
parses a deck: directives, kinds, images, notes; renders slides |
interp |
the template stage |
theme |
Marp themes and the Marpit theme pipeline |
viewer |
the HTML page |
export |
images and PDF, through go-webengine and go-pdfkit |
slides |
wires them together |
License
BSD-3-Clause. The built-in themes in theme/marp are Marp
Core's, under its MIT license (theme/marp/LICENSE).
HCL support links hashicorp/hcl/v2
(MPL-2.0, used unmodified as a library) and
zclconf/go-cty (MIT).