AgentStack
SKILL verified MIT Self-run

Markdown To Pdf

skill-vlebert-markdown-to-pdf-agent-skill-markdown-to-pdf · by vlebert

>-

No reviews yet
0 installs
3 views
0.0% view→install

Install

$ agentstack add skill-vlebert-markdown-to-pdf-agent-skill-markdown-to-pdf

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Markdown To Pdf? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

markdown-to-pdf

LaTeX-free PDF generation pipeline:

note.md ──[Pandoc + Lua filters]──▶ HTML ──[WeasyPrint + CSS]──▶ PDF

Pandoc converts the content (Markdown → HTML) and injects the frontmatter into the template. WeasyPrint handles layout exclusively: pagination, headers/footers, cover page, styles. Everything is bundled in this skill (template, Lua filters, CSS, Docker image): no external dependency.

The skill stays agnostic: standard Markdown converts cleanly, and the Obsidian transformations are only applied when the corresponding syntax is present.

1. Prerequisite: the Docker image

Before generating, make sure the image exists — do not rely on session memory (each session starts cold). The source of truth is Docker:

bash scripts/build-image.sh

This script is idempotent: it runs docker image inspect markdown-to-pdf:latest and returns immediately if the image already exists (it does not rebuild). It only builds (a few minutes) if the image is missing. Running it systematically before a generation is therefore safe and nearly free.

Manual equivalent to decide: docker image inspect markdown-to-pdf:latest (exit 0 = present, nothing to do; exit ≠ 0 = needs building).

When to rebuild with --force: only if the Dockerfile changes (WeasyPrint version, fonts). Templates, CSS and Lua filters are mounted at runtime (-v …:/skill), so modifying them requires no rebuild.

The image produces markdown-to-pdf:latest (Pandoc + WeasyPrint + fonts: system Arial/Verdana/… + Google/open-source Inter, Roboto, Montserrat, etc.).

2. Prepare the frontmatter BEFORE generating

Inspect the note's YAML frontmatter. The keys feed the template; an incomplete frontmatter yields a degraded PDF (no cover page, empty headers).

If useful keys are missing, add them to the note (without overwriting what's there):

  • at minimum title;
  • as needed: subtitle, author, date, lang;
  • for a cover page / table of contents: titlepage, titlepage-logo,

logo-width, titlepage-rule-color, toc, toc-depth, toc-title.

Infer values from the content and context when obvious (e.g. title = H1 heading or file name, date = today). Ask the user for confirmation when in doubt rather than guessing silently: whether a cover page is wanted, which logo to use, author, subtitle. See the full key table in references/weasyprint-notes.md.

3. Choose the Lua filters based on content

Only add --lua-filter for syntax actually present in the note (preserves agnosticity — pure Markdown undergoes no Obsidian transformation):

| Filter | Add if the note contains… | |---|---| | assets/lua/callouts.lua | callouts > [!TYPE] | | assets/lua/embeds.lua | image embeds ![[image.png]], ![[image.png\|200]], ![[image.png\|Caption\|200]] | | assets/lua/mdlinks.lua | text wikilinks [[note]] / [[note\|alias]] | | assets/lua/images.lua | standard Markdown images with width in the caption ` | | assets/lua/headers_shift.lua` | the "H1 = title" convention to shift (H2→H1, etc.) — otherwise do NOT add it |

Detect quickly via grep on the note (e.g. grep -E '> \[!' note.md for callouts, grep -E '!\[\[' note.md for image embeds, grep -E '\[\[' note.md for wikilinks).

Order matters: load embeds.lua BEFORE mdlinks.lua. embeds.lua converts ![[image]] into images; otherwise mdlinks.lua would see the inner [[…]] and treat it as a text link.

embeds.lua resolves the image path itself by searching for it by name under the mounted folder (see step 5), including in any subfolder: it reproduces Obsidian's behavior (finding an attachment wherever it is) and makes --resource-path unnecessary for these embeds, regardless of the attachments convention. The segments after | are classified automatically: number → width (px), WxH → width+height, text → caption. An image alone in its paragraph with a caption becomes a ` with (styled in the template's CSS). mdlinks.lua` renders text wikilinks as a plain label (alias if present, otherwise the note name) — no dead link in the PDF.

4. Choose the PDF location

  • Default (agnostic): next to the source .md file.
  • Obsidian vault detected (a .obsidian/ folder exists when walking up the

tree from the note): place the PDF in the attachments folder used by the note — infer it from the note's existing embeds (e.g. _attachments, attachments/). If relevant, insert a wikilink embed/link to the PDF in the source note (![[my-doc.pdf]]). "If relevant": judge by context, ask when in doubt.

5. Generate the PDF

Mount two volumes: the root folder (for the note AND its attachments) and the skill folder (template/CSS/filters). Choose the mounted root based on step 4:

  • Obsidian vault: mount the vault root on /data and reference the note

by its relative path (e.g. projects//quote.md). This way embeds.lua can find any attachment in the vault, regardless of its storage convention;

  • outside a vault: mount the folder containing the note (and its images).
SKILL_DIR="/path/to/this-skill"     # this skill's folder (markdown-to-pdf)
ROOT_DIR="/path/to/root"            # vault root, or the note's folder
NOTE="projects/.../note.md"         # note path RELATIVE to ROOT_DIR

docker run --rm \
  -v "$ROOT_DIR:/data" \
  -v "$SKILL_DIR:/skill:ro" \
  --workdir /data \
  --user "$(id -u):$(id -g)" \
  markdown-to-pdf:latest \
  "$NOTE" \
  --from markdown+lists_without_preceding_blankline \
  --pdf-engine=weasyprint \
  --template=/skill/assets/templates/document/template.html \
  --css=/skill/assets/templates/document/style.css \
  --lua-filter=/skill/assets/lua/callouts.lua \
  --lua-filter=/skill/assets/lua/embeds.lua \
  --lua-filter=/skill/assets/lua/mdlinks.lua \
  --number-sections \
  --toc --toc-depth=3 \
  --embed-resources \
  -o "projects/.../note.pdf"

Notes:

  • Only include the --lua-filter flags selected in step 3, in order (callouts,

embeds, mdlinks, images, headers_shift).

  • No need for --resource-path for ![[…]] embeds: embeds.lua resolves the

path by searching. Only add it for standard Markdown images `` whose path is not resolved otherwise.

  • Heading numbering: --number-sections is enabled by default (headings 1,

2, 2.1…). Pandoc does not read this from the frontmatter; it's a CLI flag. Remove it for unnumbered headings (see step 6 for adjusting).

  • Table of contents: add --toc (+ --toc-depth=N) if the note wants a

table of contents; omit it otherwise. The title/depth can also come from the frontmatter (toc-title, toc-depth) via the template.

  • --embed-resources inlines images and CSS into the intermediate HTML

(self-contained PDF).

  • The warning Ticker: poll failed: Interrupted system call is benign.

6. After generation: offer adjustments

Once the PDF is produced, don't stop there: present the result then ask the user whether anything needs adjusting, explicitly listing the available levers (they may not know them). If an adjustment is requested, change the relevant flag/key and regenerate.

Levers to offer:

  • Heading numbering — enabled by default; remove --number-sections for

headings without numbers.

  • Table of contents — presence (--toc), depth (--toc-depth=N), title

(toc-title in the frontmatter).

  • Cover page — enable/disable (titlepage), logo (titlepage-logo,

logo-width), accent color (titlepage-rule-color).

  • Template / visual identity — switch template (--template + --css), hence

font, colors, margins (see step 7).

  • Header/footer metadatatitle, author, date, lang (frontmatter).

For frontmatter-driven levers, edit the note; for CLI flags (numbering, toc), adjust the command and regenerate.

7. Create a new template

A template = a visual identity, i.e. a pair template.html + style.css. The Lua filters are shared (generic Obsidian transformations) and need not be duplicated.

For a new template:

  1. Copy assets/templates/document/ to assets/templates//.
  2. Adapt template.html: cover page structure, running elements

(headers/footers), exposed $key$ frontmatter variables.

  1. Adapt style.css: colors (--accent…), fonts, @page margins,

callout/table styles. Many Google/open-source fonts are bundled in the image (Inter, Roboto, Montserrat, Merriweather, JetBrains Mono…): usable directly by name in font-family. Full list and how to add new fonts: references/weasyprint-notes.md.

  1. Point --template and --css to the new folder at generation time.

Remember the template for this project. A custom template only has value if it gets reused. Since each session starts cold, after creating it write the association in the CLAUDE.md (or AGENTS.md) at the root of the user's vault/site — not in this repo: absolute path of the template folder, and the associated options (cover page, toc, logo). Example: "PDF export: template …/assets/templates/acme/, titlepage enabled, logo assets/logo.png". If it doesn't exist, create it. This way a future session reuses the right template without asking again.

The separation of content (Pandoc + Lua) and layout (template + CSS) is the guiding principle: appearance is changed without touching conversion. For layout pitfalls (running elements, cover page, tables, WeasyPrint 63 limitations), see references/weasyprint-notes.md.

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.