badpixxel / md2pdf
Build & Render Pdf from Markdown or Twig Templates
Requires
- php: ^8.1
- ext-dom: *
- ext-gd: *
- ext-mbstring: *
- dompdf/dompdf: ^3.0
- league/commonmark: ^2.5
- scrivo/highlight.php: ^9.18
- symfony/console: ^6.4|^7.4|^8.0
- symfony/options-resolver: ^6.4|^7.4|^8.0
- symfony/process: ^6.4|^7.4|^8.0
- symfony/yaml: ^6.4|^7.4|^8.0
- twig/extra-bundle: ^3.0
- twig/markdown-extra: ^3.0
- twig/twig: ^3.0
- webmozart/assert: ^1.10|^2.0
Requires (Dev)
- badpixxel/php-sdk: 3.0.x-dev
- fakerphp/faker: ^1.23
- phpunit/phpunit: ^10.0|^11.0
- symfony/asset: ^6.4|^7.4|^8.0
- symfony/asset-mapper: ^6.4|^7.4|^8.0
- symfony/debug-bundle: ^6.4|^7.4|^8.0
- symfony/flex: ^2.0
- symfony/http-client: ^6.4|^7.4|^8.0
- symfony/http-kernel: ^6.4|^7.4|^8.0
- symfony/runtime: ^6.4|^7.4|^8.0
- symfony/stopwatch: ^6.4|^7.4|^8.0
- symfony/translation: ^6.4|^7.4|^8.0
- symfony/web-profiler-bundle: ^6.4|^7.4|^8.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Build & render Pdf documents from Markdown or Twig templates, with DomPdf.
Features
- Markdown or Twig contents, assembled as cover, header, footer & pages
- Blocks: covers, columns, headings, landscape pages, page breaks, table of contents
- Styles: five of them, each drawing its own covers & page bands from a primary color
- Styled charts: bar, line & pie, drawn by the package as Svg with the colors of the style,
written as
```chartfences in Markdown, or as blocks - Mermaid diagrams, drawn by a Kroki server with colors of their own, then kept on disk
- GitHub alerts:
> [!NOTE],[!TIP],[!IMPORTANT],[!WARNING],[!CAUTION] - Color emojis, sequences included: skin tones, families, professions, flags & keycaps
- Tables, code highlighting, table of contents, heading permalinks & Fontawesome icons
- Extensible Markdown: add your own CommonMark extensions to any document
Documentation
| Page | Contents |
|---|---|
| Getting started | Install, a first document, how to render it |
| Blocks | Every block & its options, charts, landscape pages |
| Styles | The five styles, colors, section pages, drawings |
| Markdown | Everything the converter renders, attributes, Mermaid |
| Templates | Namespaces, Twig extensions, the limits of DomPdf |
llms.txt holds the same map, written for the coding agents. Both are shipped with the
package, so they are read from vendor/badpixxel/md2pdf/ as well. The package also carries a
Claude skill, linked into an application with:
ln -s ../../vendor/badpixxel/md2pdf/.claude/skills/md2pdf .claude/skills/md2pdf
Requirements
Php 8.1+, Symfony 6.4 LTS, 7.4 LTS or 8.0.
Demo
The repository holds a demo application, one document per feature, in its own bundle under
demo/. It is not shipped with the package:
make up # start containers, demo is served by the dev container
make demo # render the demo docs folder in every theme, into samples/
| Path | Contents |
|---|---|
demo/src/Samples | Sample documents, one class per feature or document |
demo/templates/Samples | Their Markdown & Twig contents |
demo/templates | The demo pages themselves |
docs/demo | The demo docs folder, rendered by the console |
Docs folders & the console
A directory of Markdown pages, an index.md carrying the options, an assets/ directory:
the md2pdf console renders it as one Pdf per language, with the theme of your choice.
vendor/bin/md2pdf themes
vendor/bin/md2pdf build docs/ --lang all --theme ribbons --out build/
vendor/bin/md2pdf build docs/ --watch
vendor/bin/md2pdf demo --theme all --lang all --out build/demo/
The format of the folder, its options and how to add a theme are in docs/docs-folder.md.
Samples
The demo folder, rendered in every theme, is kept in samples/ — one Pdf per theme and
language, none of them shipped with the package:
make demo # render them all again
bin/md2pdf demo --theme curves --lang fr --out samples/ # one of them
Tests
make test # all suites, locally
make test SUITE=Operational # one suite: Core, Integration or Operational
make verify # update vendors, then quality checks & tests in every
# container, one per supported Symfony version
Operational renders every sample document to a complete Pdf, without any Symfony kernel.
Blocks
A document is composed of blocks: each one knows its template, its options, and what it requires. Options are resolved when the document is rendered, against its own contents.
use BadPixxel\Md2Pdf\Blocks\{Cover, Heading, TableOfContents, TwoColumns};
$document
->setCover(new Cover(introduction: $intro, version: "V1.2"))
->addContent(new TableOfContents("Contents"))
->addContent(new Heading(2, "Parties"))
->addContent(new TwoColumns(
leftTemplate: "Invoices/seller.md.twig",
rightTemplate: "Invoices/buyer.md.twig",
))
->addContent("Invoices/lines.html.twig", array("lines" => $lines))
;
- Options are given by named arguments, typed by the constructor, or as an
optionsarray. Both are validated by the block resolver: a required option that is missing, an unknown one or an invalid value stops the rendering. - What the document already carries fills the blocks: the title of a cover, the logo of a header, the licence of a footer.
- A block asked to render
onlyits own options ignores the document, ie: a table of contents. - Every block takes two common options, given as arguments or set fluently:
only()renders it without the contents of the document,shift()moves its headings, ie:TemplateBlock::from("Pdf/syntax.md")->shift(1)renders a document written on its own as a chapter of another one. Their names are in theBlockOptionsdictionary. - A template named by a string is a block too, with free options: an application template knows its own variables, and nothing here could validate them.
Writing a block of your own is a class, a template and its options:
class ReleaseCover extends AbstractBlock
{
protected const TEMPLATE = "@my-theme/Blocks/release-cover.md.twig";
public function __construct(?string $version = null, ?string $released = null)
{
parent::__construct(self::filter(array("version" => $version, "released" => $released)));
}
protected function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setRequired(array("version", "released"))
->setAllowedTypes("version", "string")
->setAllowedTypes("released", "string")
;
}
}
Styles
A style gives a document its colors, its page bands & its covers. Five are shipped:
Standard, Ribbons, Curves, Circles and BadPixxel.
use BadPixxel\Md2Pdf\Blocks\Ribbons\{BackCover, Cover, Section};
use BadPixxel\Md2Pdf\Models\Styles\Ribbons;
Ribbons::setupStyles($document, "Northwind Industries"); // or a primary color of your own
$document
->setCover(new Cover(company: "Northwind Industries", kicker: "Annual", title: "Report"))
->setBackCover(new BackCover(kicker: "Thank you", contact: $contact))
->addContent(new Section(kicker: "Appendix", title: "Annexes"))
;
Drawings are Svg templates, inlined by the svg() Twig function: DomPdf ignores inline
<svg> elements, but draws a Svg data Uri as vectors. Colors of the document are available
to them, so the same style follows any primary color.
A full page drawing hides the page bands, on the cover & on the back cover: it must be a positioned block written at the root of the page, never nested in a block of its own.
Templates
Three ways to name a template, and one rule: an application template always wins.
| Name | Where it lives | Resolution |
|---|---|---|
@Md2Pdf/Layouts/2-columns.html.twig | This package | Its own namespace, never shadowed |
Invoices/lines.html.twig | Your application | Main namespace, searched before the package |
@my-theme/Styles/default.html.twig | A theme, a bundle… | Its own namespace, asked for explicitly |
$renderer
->addTemplatePath(__DIR__.'/../templates/pdf') // application contents
->addTemplateNamespace('my-theme', $themeDir) // a provider of its own
;
In a Symfony application, twig.paths entries are already in the main namespace: they win over
the package templates too, whether the document is rendered by a controller or by a command.
The package directory stays registered in the main namespace as a fallback, so
Layouts/2-columns.html.twig keeps working — but an application file of that name replaces it.
Extending the Markdown converter
$document
->addMarkdownExtension(new MyExtension())
->setMdAlertsOptions(array('labels' => array('note' => 'Note', 'tip' => 'Astuce')))
;
Emojis
Emojis are rendered as images, from a pack of Twemoji SVG files compiled into a single binary file: DomPdf has no emoji glyph, and no font could render sequences. Nothing is downloaded at render time.
Rebuild the pack when Twemoji publishes a new version:
php bin/build-emoji-pack.php
The pack is in resources/emoji/twemoji.pack.
Licences
This package is released under the MIT licence.
Emoji graphics come from jdecked/twemoji: graphics
© Twitter, Inc and other contributors, licensed under CC-BY 4.0, see
resources/emoji/LICENSE-GRAPHICS.