1. What is PicoDoc?
PicoDoc is a macro-based markup language that compiles to HTML. Everything in PicoDoc (headings, bold text, links, user-defined templates) is a macro call. This single abstraction keeps the language regular and predictable: once you learn the macro syntax, you know the entire language.
2. Installation - Python version
The PicoDoc Python project requires Python 3.14+ and uv.
git clone <repo-url> picodoc
cd picodoc
uv sync
Verify the installation:
uv run picodoc --help
3. Installation - C version
The C version requires a C compiler:
git clone <repo-url> picodoc-c
cd picodoc
make
4. Your First Document
Create a file called hello.pdoc:
#doc.title: Hello, PicoDoc
#-: Hello, PicoDoc
This is my first document.
Compile it to HTML (Python version):
uv run picodoc hello.pdoc -o hello.html
Or (C version):
path/picodoc hello.pdoc -o hello.html
Open hello.html in your browser. You should see a heading and a
paragraph. The #doc.title macro sets the browser tab title, while #-
creates a visible heading. The bare text was automatically wrapped
in a <p> tag because PicoDoc treats any text not inside a macro
call as an implicit paragraph.
5. Headings and Structure
PicoDoc uses #doc.title for the document's <title> (shown in the
browser tab) and separate heading macros for visible headings in the body:
#doc.title: Page Title (in browser tab only)
#-: Level 1 (visible heading)
#--: Level 2 (Section)
#---: Level 3 (Subsection)
#----: Level 4
#-----: Level 5
#------: Level 6
The named forms #h1 through #h6 can also be used:
#h1: Level 1 (same as #-)
#h2: Level 2 (same as #--)
#h3: Level 3 (same as #---)
#h4: Level 4 (same as #----)
#h5: Level 5 (same as #-----)
#h6: Level 6 (same as #------)
Use #hr for a horizontal rule and #// (or #comment)
for text that should not appear in the output:
#hr
#//: This is a note to myself — not rendered.
6. Paragraphs
Bare text is automatically wrapped in <p> tags. A blank line
separates paragraphs:
This is the first paragraph. It can span
multiple lines.
This is the second paragraph.
You can also use #p explicitly. The paragraph body form lets you
write the body on the following lines:
#p: An explicit single-line paragraph.
#p:
A multi-line paragraph using
the paragraph body form.
7. Inline Formatting
7.1. Bold and Italic
Use #** (or #b) for bold and #__ (or #i) for italic:
This is #**"bold" text.
This is #__"italic" text.
For longer spans, use the bracketed form:
This is [#b : bold text that spans several words].
This is [#i : italic text in brackets].
Bold and italic can be nested:
This is [#b : bold with [#i : italic inside]].
For combined bold+italic, use #*_ (strong wrapping em) or #_* (em wrapping strong):
This is #*_"bold italic" text.
This is [#_*: italic bold] text.
7.2. Links
Use #> (or #link) with a to argument
and optional body for the link text:
Visit [#> to="https://example.com" : Example Site] today.
[#link to="https://example.com" : Using the alternate form]
If no body is provided, the to value is used as the link text.
If to contains no :// and no /, it is treated as
a fragment reference (e.g. to=section1 produces href="#section1").
7.3. Mixed Inline Example
All inline formatting can appear together in a paragraph:
A paragraph with #**"bold", #__"italic", and
[#link to="https://example.com" : a link] all in one line.
8. Lists
8.1. Unordered Lists
Wrap list items in #ul. Each item uses #* (or #li):
[#ul :
#*: First item
#*: Second item
#*: Third item
]
8.2. Ordered Lists
Use #ol instead of #ul:
[#ol :
#*: Step one
#*: Step two
#*: Step three
]
8.3. Items with Formatting
List items can contain any inline macro:
[#ul :
#*: A #**"bold" item
#*: An #__"italic" item
#*: A [#link to="https://example.com" : linked] item
]
8.4. Nested Lists
Use the bracketed form of #* and nest a list inside:
[#ul :
#*: Top-level item
[#* : Item with sublist
[#ul :
#*: Nested A
#*: Nested B
]
]
#*: Another top-level item
]
9. Tables
The simplest way to make a table is with pipe-delimited rows. The first row becomes headers:
#table:
Name | Age | Status
Alice | 30 | Active
Bob | 25 | Inactive
Cells can contain inline macros:
#table:
Feature | Supported
Bold | [#**"Yes"]
Links | [#link to="https://example.com" : Yes]
For full control (colspan, custom structure), use the explicit form with #tr, #th, and #td:
[#table :
[#tr : [#th: Name] [#th: Age]]
[#tr : [#td: Alice] [#td: 30]]
[#tr : [#td span=2 : Total: 1 person]]
]
10. Code
For inline code, use #~:
Use the [#~ : print()] function to output text.
The [#~ language=python : def] keyword starts a function.
For code blocks, use #code with a raw string body and an optional
language parameter:
[#code language=python : """
def hello():
print("Hello, world!")
"""]
The indentation of the closing """ delimiter determines how much
leading whitespace is stripped from every line.
You can also use a paragraph body (colon followed by a newline). Body content is automatically dedented — the common leading whitespace is stripped:
#code language=python:
def hello():
print("Hello, world!")
To show PicoDoc syntax itself without it being interpreted, use #literal:
#literal """
The #b macro makes text bold.
Use [#link to="..." : text] for links.
"""
11. User-Defined Macros
11.1. Simple Variables
Define a variable with #set. A macro with no parameters is just a
named value:
[#set name=version : 1.0]
The current version is #version.
11.2. Macros with Arguments
Add parameters to create reusable templates. Mark required parameters with ? and provide defaults with =value:
[#set name=greeting target=? : Hello, #target!]
[#greeting target=World]
[#greeting target=Alice]
[#set name=box style=default body=? : [#p : (#style) #body]]
[#box : Content with default style.]
[#box style=fancy : Content with fancy style.]
11.3. Macros with Body
If your macro accepts a body, declare it as body=? (required) and
it must be the last parameter:
[#set name=greeting target=? body=? : Dear #target, #body Kind regards.]
[#greeting target=World : thank you for your support.]
11.4. Out-of-Order Use
Macros can be used before they are defined. PicoDoc collects all definitions first, then expands:
#p: The project is #project-name.
[#set name=project-name : PicoDoc]
12. Overriding Builtin Macros
You can redefine render-time builtins like #p, #b, #code, and #link to customise how standard elements are rendered. Use the #builtin.name prefix to call the original builtin from inside your override:
[#set name=p body=? : [#builtin.p : [#b : #body]]]
This paragraph will be bold.
Here the user-defined #p wraps every paragraph body in bold, delegating to the real #p via #builtin.p. This works for any render-time builtin:
[#set name=code body=? :
[#div class=code-block :
[#builtin.code : #body]
]
]
[#code : print("hello")]
Expansion-time builtins (#set, #ifeq, #ifne, #ifset, #include, #comment, #table) cannot be overridden.
The builtin.* namespace is reserved. You cannot define a macro whose
name starts with builtin..
13. Content Wrappers
PicoDoc provides wrapper macros (#div, #section, #span, #nav, #header,
#footer, #main, #article, #aside) for grouping content in HTML container
elements. They accept optional class and id parameters:
[#div class=card :
#--: Card Title
#p: Card body text here.
]
For page-level layout, use #doc.content to automatically wrap loose content (anything not inside an explicit wrapper) in a container element:
#doc.content type=main class=content
[#header : #-: My Site]
#p: This paragraph gets wrapped in <main class="content">.
[#footer : #p: Copyright]
The header and footer remain outside the <main> element, while loose
paragraphs and headings are collected inside it.
14. String Literals
14.1. Interpreted Strings
Delimited by single double quotes. Escape sequences are processed:
"A simple string."
"A tab:\there. A newline:\nSecond line."
"A literal quote: \" inside."
To embed macro calls inside a string, use \[...\] (code mode):
[#set name=version : 1.0]
#p"PicoDoc version \[#version] is ready."
14.2. Raw Strings
Delimited by three or more double quotes. Nothing inside is processed:
"""This is raw: \n is literal, #name is not expanded."""
14.3. Whitespace Stripping
When a string starts on the next line after the delimiter and the closing delimiter is on its own line, leading whitespace matching the closing delimiter's indentation is stripped from all lines:
[#code language=python : """
def hello():
print("world")
"""]
The four spaces before each line are stripped, producing clean output.
The same kind of dedenting applies to colon body content (paragraph and bracketed forms). The longest common leading whitespace across all non-blank lines is stripped automatically, so you can indent body content to match its macro without the indentation appearing in the output.
15. Conditionals
PicoDoc provides three conditional macros:
The #ifeq macro tests string equality:
[#set name=mode : draft]
[#ifeq lhs=#mode rhs=draft : This document is a draft.]
The #ifne macro tests string inequality:
[#ifne lhs=#mode rhs=production : Not yet published.]
The #ifset macro tests if a macro is defined:
[#ifset name=env.author : Written by [#env.author].]
A practical pattern is a draft watermark controlled by an environment variable:
[#ifeq lhs=[#env.mode] rhs=draft :
#p: DRAFT — not for distribution.
]
Then compile with uv run picodoc -e mode=draft doc.pdoc -o doc.html.
16. Including Other Files
The #include macro inserts another file at the call site. The
filename is provided as the body:
[#include : header.pdoc]
#--: Main Content
#p: The body of the document.
[#include : footer.pdoc]
This is the standard pattern for shared headers and footers across a set of documents.
To include a file as raw text without parsing, use the literal
parameter:
[#include literal=true : code.js]
17. Document Metadata
Use the doc.* namespace for HTML head elements:
#doc.lang: en
#doc.body class=dark-theme
#doc.meta name=viewport content="width=device-width, initial-scale=1"
#doc.link rel=stylesheet href="style.css"
#doc.script src="app.js"
#doc.author: Jane Doe
Use #doc.body to set class and id attributes on the
<body> element.
18. Table of Contents
Use #doc.toc to generate a table of contents from your document's
headings. It renders a <ul> tree, so wrap it in a container macro
for placement control:
[#nav id=toc :
#doc.toc
]
#-: Introduction
#--: Getting Started
#--: Installation
By default, headings up to level 3 are included. Use the level
parameter to control depth:
[#nav id=toc :
#doc.toc level=2
]
All headings automatically receive id attributes based on their text,
enabling deep linking even without a TOC.
19. Environment Variables
The env.* namespace provides global values. They can be set from
three sources:
CLI:
uv run picodoc -e mode=draft -e author="Alice" doc.pdoc -o doc.html
Config file (picodoc.toml):
[env]
mode = "draft"
author = "Alice"
Document-level #set:
[#set name=env.mode : draft]
Document-level definitions have the highest precedence, then CLI, then config.
Access environment values as zero-argument macros:
[#ifset name=env.author : Written by [#env.author].]
20. CLI Quick Reference
| Flag | Description |
|---|---|
-o FILE | Write output to FILE instead of stdout |
-e NAME=VALUE | Set an environment variable (repeatable) |
--css FILE | Inject a CSS file into the head (repeatable) |
--js FILE | Inject a JS file into the head (repeatable) |
--meta NAME=VALUE | Add a meta tag (repeatable) |
--filter-path DIR | Extra filter search directory (repeatable) |
--filter-timeout SECS | Filter execution timeout (default: 5.0) |
--config FILE | Config file path |
--watch | Watch for changes and recompile |
--debug | Dump the AST to stderr |
Watch mode recompiles automatically whenever the input file changes:
uv run picodoc --watch doc.pdoc -o doc.html
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Syntax error (lex or parse failure) |
| 2 | Evaluation error or invalid arguments |
21. Configuration File
Place a picodoc.toml next to your input file. CLI flags override
config values.
[env]
author = "Jane Doe"
[css]
files = ["style.css"]
[meta]
viewport = "width=device-width, initial-scale=1"
22. Next Steps
You now know enough to write real documents in PicoDoc. For a complete and
systematic description of every feature, see the Language Reference
in the file reference.pdoc.
Additional topics covered in the reference:
- External filters: extend PicoDoc with programs in any language
- The expansion model: how multi-pass evaluation works
- Editor support: Neovim syntax highlighting and LSP diagnostics