DocDraw PDF Renderer Profile v1 (DD-PDF-1)
This defines the deterministic rendering rules for producing a “standard business document” PDF from valid DocDraw v1.
Status: Implemented (defaults may still be tightened, but output is deterministic and covered by golden PDFs + hashes).
Note: DocDraw v1 (language) is pre-release and may evolve; DD‑PDF‑1 defaults may still be tightened, but changes are caught by the conformance suite (golden PDFs + pdf_sha256).
Reference compiler status (PHP)
There is now a reference PHP renderer accessible via:
./bin/docdraw render <file.docdraw> -o <out.pdf>
Byte-determinism is enforced by the harness:
- PASS examples have golden PDFs under
assets/examples/*.pdf pdf_sha256is recorded inexamples/golden-manifest.json- determinism is checked via
make examples-check
0) Goals
- Professional “standard document” look: clean paragraphs, readable lists, consistent spacing.
- Deterministic: same DocDraw input → same PDF output.
- No layout guessing: structure comes only from DocDraw tokens.
0.1 Output contract (normative intent)
This document is written as an implementation contract. If two implementations render the same valid DocDraw input differently in a way not permitted here, at least one is wrong.
0.2 Profile identifier
Implementations and examples refer to this rendering contract as:
DD-PDF-1
1) Page setup
Paper + margins
- Page size: Letter (8.5" × 11") by default
- Optional setting: A4
Margins:
- Top: 1.0"
- Bottom: 1.0"
- Left: 1.0"
- Right: 1.0"
Content width = page width − left margin − right margin
Pagination rule
Render blocks top-to-bottom.
If a block cannot fit fully and is “splittable” (paragraph/list), split across pages.
Headings should avoid being orphaned: if a heading would be the last line on a page, push it to next page (unless it fits with at least 1 following line of content).
Pagination edge cases (recommended behavior)
- Keep-with-next for headings: a heading should not appear as the last line on a page.
- List splitting: lists may split across pages, but indentation and numbering MUST remain consistent after the break.
- Continuation lines:
..:is part of the prior list item and MUST not introduce extra spacing.
2) Typography (default style)
Fonts (deterministic choice)
Pick a bundled, permissively licensed family and embed it.
Suggested families:
- Inter (sans) or Source Serif 4 (serif)
Baseline defaults:
- Body font: Source Serif 4 Regular
- Body size: 11pt
- Line height: 1.25× font size (≈ 13.75pt)
Color
- Text:
#111111(almost black) - No colored text in v1
Alignment
- Left aligned
- No full justification
3) Block layout rules
Define vertical spacing unit: U = 6pt
Default block spacing:
- After paragraph (
p:orp{}): U (6pt) - After heading: U (6pt)
- After list block: U (6pt)
- After divider (
---): 2U (12pt)
Paragraphs:
- First-line indent: 0 in v1
- Soft wrapping: words wrap to fit content width
- Hard breaks: only generated by
brinsidep{} p{ ... }: treat contents as a single paragraph; eachbrforces a new line within the same paragraph
4) Headings
Input:
#1:…#6:
Heading typography:
-
1 (H1): 18pt, Semibold
-
2 (H2): 14pt, Semibold
-
3 (H3): 12pt, Semibold
-
4 (H4): 11pt, Semibold
-
5 (H5): 11pt, Semibold
-
6 (H6): 11pt, Semibold
Spacing:
- Space before headings:
-
1: 3U (18pt) unless it is the first block on a page
-
2: 2U (12pt)
-
3–#6: 2U (12pt)
-
- Space after headings: 1U (6pt)
Keep-with-next:
- Heading must keep with at least one following line (either first line of paragraph or first list item). If not possible, push heading to next page.
5) Dividers
Input:
---
Rendering:
- Horizontal line across 100% of content width
- Stroke: 0.5pt
- Color:
#BBBBBB - Space before: 2U (12pt)
- Space after: 2U (12pt)
6) Lists (most important)
Input:
- Bullets:
-L: text(L=1..9) - Numbered:
1-L: text(L=1..9) - Continuations:
..: text(belongs to preceding list item)
List grouping:
- A “list block” is a contiguous run of list items and continuations.
- If a non-list block appears, the list block ends.
Indentation model (deterministic defaults)
Define:
listIndentStep = 18pt(per level)bulletColumnWidth = 18pt(space reserved for marker)hangingIndent = 12pt(text aligns after marker)
For a list item at level L:
- Base left indent = (L - 1) ×
listIndentStep - Marker X position = left margin + base left indent
- Text start X position = marker X +
bulletColumnWidth
Line wrapping within list items:
- Wrap at content width minus current level indent.
- Subsequent wrapped lines align to text start X (hanging indent style).
Vertical spacing in lists:
- Default: 2pt between list items
Bullet marker rendering (defaults)
For -L: bullets:
- Level 1:
• - Level 2:
◦ - Level 3+: pick one and keep it consistent (
▪recommended)
Marker font = body font, same size.
Numbered marker rendering (defaults)
Maintain a counter per list “scope.”
Scope rules:
- Each contiguous run of
1-*items is one ordered list block. - Within that block, numbering is tracked per level:
- Level 1 items count 1,2,3...
- Level 2 resets whenever a new level-1 item begins
- Level 3 resets whenever a new level-2 item begins
Marker format:
- Level 1:
1. 2. 3. - Simple v1 recommendation: always use numeric markers at every level, tracked independently per level
Ordered marker styles (DD-PDF-1 option)
DD-PDF-1 may be parameterized with a document-wide ordered marker style for ordered lists (1-L:).
Supported values:
decimal(default):1. 2. 3.lower-alpha-paren:(a) (b) (c)(continues as(z) (aa) (ab)...)
Alpha sequence (normative for lower-alpha-paren):
- (1 \to a)
- (26 \to z)
- (27 \to aa)
- (28 \to ab) and so on (base-26 letters, like spreadsheet columns, but lower-case).
Marker width handling:
- Right-align marker text within
bulletColumnWidthso multi-digit numbers don’t shift text.
Continuations (..:)
- Render as additional wrapped text lines for the prior list item
- No extra vertical spacing before a continuation
- Start at the same text start X
Long wrapping behavior (contract)
- Wrapped lines MUST align to the list item’s text start X (hanging indent).
- Wrapping MUST be word-based (no mid-word breaks unless unavoidable).
“Fail, don’t guess” (implementation philosophy)
Even though renderers can be defensive, correctness for DocDraw v1 relies on validation:
- Invalid structure SHOULD be rejected during validation rather than “repaired” at render time.
7) Quotes (q:) (optional)
Rendering defaults:
- Left indent: 18pt
- Vertical rule at left edge: 2pt thick,
#DDDDDD - Quote text: body font (italic optional; default regular)
- Space before: 1U
- Space after: 1U
8) Code blocks (code{}) (optional)
Rendering defaults:
- Monospace font (bundled): e.g., JetBrains Mono
- Size: 10pt
- Background fill:
#F6F6F6 - Border:
#E0E0E0, 0.5pt - Padding: 8pt
- Preserve whitespace; wrap long lines (wrapping is simpler)
9) Handling “bad” input
Even though validation should prevent this, renderer behavior should be deterministic:
- Unknown lines: error out with a clear message and line number (no silent interpretation).
10) Export consistency requirements
To keep output stable across machines:
- Embed fonts in the PDF
- Use a single rendering engine (Chromium print OR PDF library) and don’t mix
If using HTML→PDF:
- Use fixed CSS for margins, font metrics, list indentation
- Disable “smart” browser list styling; draw list markers yourself for consistency
11) Minimal HTML/CSS mapping (if using HTML→PDF)
Do not rely on <ul> default rendering. Instead, render a normalized structure:
<div class="p">...</div>
<div class="li level-2">
<span class="marker">•</span><span class="text">...</span>
</div>
Then CSS controls everything deterministically:
.li.level-3 { margin-left: calc((3 - 1) * 18pt); }
.marker { width: 18pt; display: inline-block; text-align: right; padding-right: 6pt; }
.text { display: inline; }