PDX Markup
PDX markup is an XML-based DSL for authoring document and email templates. It was designed to be familiar to anyone who can read HTML, but constrained to the subset that maps cleanly to DocumentModel and EmailModel.
Two parsers live in Pragmatic.Documents.Markup:
PdxDocParser.Parse(markup)→DocumentTemplatePdxEmailParser.Parse(markup)→EmailTemplate
Both also expose ParseFile(path) convenience methods.
Why markup?
Section titled “Why markup?”Authoring a DocumentBuilder in C# is great when the author is the developer. When a designer, content team, or localisation team owns the layout, a text file is the right surface:
- no C# toolchain required
- git-friendly diffs
- easy to version per-customer variants
- parseable by tools outside the runtime (snippet validators, linters, preview tools)
The markup is declarative — expressions live in {{ ... }} blocks, so a designer can author the structure and a developer provides the data.
PDX-Doc — document markup
Section titled “PDX-Doc — document markup”Skeleton
Section titled “Skeleton”<document title="Invoice {{invoice.number}}" author="{{company.name}}" page-size="A4" orientation="portrait" margin="normal" lang="en"> <page> <!-- content --> </page> <page> <!-- another page --> </page></document>Root attributes:
| Attribute | Values | Maps to |
|---|---|---|
title | text or {{expr}} | DocumentTemplate.Title |
author | text or {{expr}} | DocumentTemplate.Author |
lang | ISO code (en, it-IT) | DocumentTemplate.Language |
page-size | A4, Letter, Legal, A5 | DocumentTemplate.PageSize |
orientation | portrait, landscape | DocumentTemplate.Orientation |
margin | normal, narrow, wide | DocumentTemplate.Margins |
page-data-source | data-source name | one page per item |
Content elements
Section titled “Content elements”Inside <page> you place nodes. Each maps to a model node:
| Element | Maps to | Attributes |
|---|---|---|
<heading level="N"> | HeadingNode | level (1-6) |
<text> | TextNode | — |
<paragraph> | ParagraphNode | — |
<spacer /> | SpacerNode | height (points) |
<hr /> | HorizontalRuleNode | — |
<image src="..." alt="..." /> | ImageNode | src, alt, width, height |
<barcode value="..."> | BarcodeNode | value, type (qr, code128, …) |
<hyperlink href="...">text</hyperlink> | HyperlinkNode | href |
<toc /> | TocNode | max-level, title |
<page-break /> | PageBreakNode | — |
<field type="PageNumber" /> | FieldNode | type, format |
<bookmark name="..."> | BookmarkNode | name |
<footnote>text</footnote> | FootnoteNode | — |
<header> / <footer> | Page header / footer nodes | — |
<table> | TableNode with rows | see below |
<import src="partial.pdxdoc" /> | Include a partial template | src |
Tables
Section titled “Tables”Plain table:
<table> <row><cell>Item</cell><cell>Qty</cell><cell>Price</cell></row> <row><cell>Design review</cell><cell>1</cell><cell>€250</cell></row></table>Data-bound table (repeats <row-template> once per item):
<table data-source="invoice.items"> <column width="90">Description</column> <column width="20" align="center">Qty</column> <column width="25" align="right">Price</column> <row-template> <cell>{{item.description}}</cell> <cell>{{item.qty}}</cell> <cell>{{item.price | currency:"EUR"}}</cell> </row-template></table>Inside row-template, the loop variable is the singular form of the data source key (invoice.items → item). For custom names:
<row-template as="line"> <cell>{{line.description}}</cell></row-template>Expressions
Section titled “Expressions”Any attribute or text content can include {{ expression }} interpolation. See templating.md for the expression syntax.
Headers and footers
Section titled “Headers and footers”<page> <header> <text align="right">Page <field type="PageNumber"/> of <field type="PageCount"/></text> </header> <footer> <text align="center">Confidential — {{company.name}}</text> </footer>
<heading level="1">{{invoice.number}}</heading> <!-- ... --></page>Full data-bound example
Section titled “Full data-bound example”<document title="Invoice {{invoice.number}}" author="{{company.name}}"> <page> <heading level="1">{{company.name}}</heading> <text>{{company.address}}</text> <spacer /> <heading level="2">Invoice {{invoice.number}}</heading> <text>Date: {{invoice.date | date:"dd MMMM yyyy"}}</text> <text>Customer: {{customer.name}} — VAT {{customer.vatId}}</text> <hr /> <table data-source="invoice.items"> <column width="90">Description</column> <column width="20" align="center">Qty</column> <column width="25" align="right">Price</column> <column width="25" align="right">Total</column> <row-template> <cell>{{item.description}}</cell> <cell>{{item.qty}}</cell> <cell>{{item.price | currency:"EUR"}}</cell> <cell>{{item.total | currency:"EUR"}}</cell> </row-template> </table> <hr /> <text>Subtotal: {{invoice.subtotal | currency:"EUR"}}</text> <text>VAT 22%: {{invoice.vat | currency:"EUR"}}</text> <heading level="3">TOTAL: {{invoice.total | currency:"EUR"}}</heading> </page></document>Resolve it with a data context (see next section).
PDX-Email
Section titled “PDX-Email”PDX-Email is the email counterpart. The root is <email>, children are <section> / <hero> / <two-columns> / <article> / <footer> / <social-bar>.
<email subject="Welcome to {{company.name}}" preheader="Your account is ready." width="600" lang="en"> <hero background-color="#1F497D"> <heading>Welcome, {{user.firstName}}!</heading> <text>Thanks for signing up.</text> </hero> <section padding="24"> <column> <paragraph>You're all set. Click the button below to get started:</paragraph> <button href="{{verifyLink}}">Verify your email</button> </column> </section> <footer background-color="#eeeeee"> <column> <text align="center" color="#666">© {{year}} {{company.name}}</text> </column> </footer></email>Element mapping:
| Element | Maps to EmailBuilder |
|---|---|
<section> + <column> | .Section(s => s.Column(...)) |
<hero> | .Hero(...) |
<two-columns> / <three-columns> | .TwoColumns(...) / .ThreeColumns(...) |
<article> | .Article(...) |
<footer> | .Footer(...) |
<social-bar> | .SocialBar(...) |
<heading> / <text> / <button> / <image> / <spacer> / <divider> | Column nodes |
See email-rendering.md for what each section is and when to use it.
Loading and parsing
Section titled “Loading and parsing”using Pragmatic.Documents.Markup;
var template = PdxDocParser.Parse(xmlString);// orvar template = PdxDocParser.ParseFile("invoice.pdxdoc");Parser errors throw MarkupParseException with the offending element name and line number:
MarkupParseException: Root element must be <document>, got <documentz> (line 1)Partials
Section titled “Partials”Large templates split into reusable fragments:
<!-- invoice.pdxdoc --><document title="Invoice {{invoice.number}}"> <page> <import src="header.pdxdoc" /> <heading level="1">Invoice {{invoice.number}}</heading> <!-- ... --> <import src="footer.pdxdoc" /> </page></document>var resolver = new DocumentTemplateResolver() .WithPartial("header.pdxdoc", PdxDocParser.ParseAsPartialFile("header.pdxdoc")) .WithPartial("footer.pdxdoc", PdxDocParser.ParseAsPartialFile("footer.pdxdoc"));Partials receive the same data context as the parent template.
Resolving the template
Section titled “Resolving the template”Parsing gives you a template. To get a renderable DocumentModel / EmailModel, resolve it with data:
using Pragmatic.Documents.Templating.Data;using Pragmatic.Documents.Templating.Pipes;
var resolver = new DocumentTemplateResolver(PipeRegistry.Default);var data = new TemplateDataContext() .AddSource("invoice", invoice) .AddSource("company", company) .AddSource("customer", customer);
var model = await resolver.ResolveAsync(template, data);PdfRenderer.RenderTo(File.Create("invoice.pdf"), model);See templating.md for the data context, pipes, and custom pipe registration.
Editor support
Section titled “Editor support”A VS Code / Rider language server for PDX-Doc is on the roadmap. For now use XML IntelliSense — the schema validates structure, though expression syntax is intentionally free-form.
Related
Section titled “Related”- templating.md — expressions, pipes, data context
- pdf-rendering.md — render resolved document templates to PDF
- email-rendering.md — render resolved email templates to HTML
Pragmatic.Documents.Markup.Samples— simple, data-bound, and batch markup examples