Org support
A deliberate subset, with the boundary enforced by tests rather than by hope.
orgo parses a defined slice of org. The boundary is not aspirational: every supported construct has a golden-file test, and every excluded one has a test asserting how it degrades. That is what stops the parser drifting toward all-of-org.
Supported
Headings
Nesting by star count, with TODO keywords, priority cookies and tags:
* TODO [#A] Write the parser :work:rust:
,:PROPERTIES:
,:CUSTOM_ID: write-parser
,:END:
The keyword set is Emacs' default — TODO and DONE — matched on a word boundary, so a heading beginning "TODOs are great" is a plain title. Keyword and priority markup uses Emacs' own export classes.
Every heading gets an id: its :CUSTOM_ID: if it has one, else its :ID:, else a slug of its text.
Text and inline markup
*bold*, /italic/, _underline_, +strike+, =verbatim= and ~code~. Links in every org form — external, [[*Heading]], [[#custom-id]], [[id:...]], [[file:other.org]] — plus bare URLs in running text.
Timestamps, active and inactive, with times and ranges, render as <time> with a machine-readable datetime.
Text conversions
Org rewrites some prose on export, and so does orgo:
| Written | Published |
|---|---|
-- | – |
--- | — |
... | … |
x^2 | x superscript 2 |
H_{2}O | H subscript 2 O |
\alpha, \rarr, 20\deg | α, →, 20° |
The entity table is org's own, generated from Emacs' org-entities rather than transcribed, so all 412 names behave as they do in Emacs. A name org does not know — \notanentity — stays as the literal text you typed, and \alphabet is a word rather than α followed by "bet". #+OPTIONS: e:nil turns the whole table off.
Neither reaches inside verbatim, code, a source block or a LaTeX fragment — --verbose in a shell transcript stays a flag, and $x^2$ stays mathematics.
Braceless subscripts catch people out. Org's default converts a_b, so snake_case in prose publishes as snake with a subscript. That is what Emacs does with the same file. Turn it off per document with #+OPTIONS: ^:nil, restrict it to the braced form with ^:{}, or set [html] sub_superscript for the site. #+OPTIONS: -:nil turns off the dashes and ellipsis.
Heading levels are relative
A file whose shallowest heading is ** is a file of top-level sections that happen to be indented, not a file of subsections — org exports levels relative to the document, so that subtree exports the same whether it was cut from a larger file or written on its own.
Lists
Unordered, ordered and description lists, nested by indentation, with checkboxes and multi-paragraph items:
- outer item
- inner item
- [X] a checked item
- [-] a partly-done item
- term :: definition
1. [@4] an item numbered from 4
Checkboxes render as org writes them — <code>[X]</code> with the state as a class on the item — rather than as a disabled <input>, which has no way to say "partly done".
Blocks
SRC (syntax highlighted), EXAMPLE, QUOTE, CENTER, VERSE and EXPORT. A source block inside a quote block works, because block ends match their own kind.
An html export block passes through verbatim; every other backend is dropped, because emitting LaTeX into an HTML page is worse than emitting nothing.
Any other name is a special block: #+BEGIN_NOTE becomes <div class"note">= holding parsed org, which is what makes the convention usable without orgo knowing the word "note". A COMMENT block is not published.
Which languages highlight
Highlighting uses the syntax definitions syntect bundles. A language it does not know is not an error — the block renders as escaped <pre><code class"language-…">= with its content intact, just uncoloured.
Recognised, among others: bash / sh, c, c++, css, clojure, diff, erlang, go, haskell, html, java, javascript, json, latex, lisp, lua, makefile, markdown, matlab, objective-c, ocaml, perl, php, python, r, ruby, rust, scala, sql, tcl, xml, yaml.
orgo adds two syntect does not ship: TOML and Org. Both are what this project's own documentation needed on its first page — every config example is TOML, and a tool for org users gets written about in org — so they are compiled into the binary and work with no setup.
Still missing, and worth knowing before you write a page full of them: INI and Emacs Lisp. For those, drop a .sublime-syntax file into the directory named by [highlight] syntaxes_dir (default syntaxes/) and it is picked up. A file that fails to parse is reported and skipped rather than failing the build.
The comma escape
A line inside a block that would otherwise look like document structure is written with a leading comma — ,* heading, ,#+KEYWORD: — and orgo removes exactly one comma on output, as Emacs does. Every org example in this documentation relies on it.
The escape is not optional politeness: an unescaped * at column zero ends the block, in Emacs as much as here. If a code block seems to stop early, that is why.
Tables and footnotes
Pipe tables, with the rule row establishing a header band and an affiliated #+CAPTION: becoming a numbered <caption>. Org's special column is honoured: a first column holding only export markers (/, #, !, ^, _, $) is dropped, and rows marked /, !, ^, _ or $ are instructions to org rather than content, so they never reach the page.
Footnotes in all three forms — [fn:1] references, [fn:1] definitions and [fn:1:inline text] — rendered as a numbered, back-linked notes section.
Images
A description-less link to an image file renders as <img>. With an affiliated #+CAPTION: or #+ATTR_HTML: it becomes a <figure> with the caption as both <figcaption> and alt text:
#+CAPTION: The pipeline, end to end
#+ATTR_HTML: :width 640 :class diagram
[[file:pipeline.svg]]
Links to non-.org files are understood as asset links: neither resolved nor reported as broken.
Keywords with meaning
| Keyword | Effect |
|---|---|
#+TITLE: | Page title. Falls back to the filename stem. |
#+DATE: | Sorts listings. Any org date syntax. |
#+DESCRIPTION: | The excerpt shown in listings. |
#+FILETAGS: | Tags, for grouping and page.tags. |
#+SLUG: | Sets the output filename. |
#+DRAFT: | Keeps the page out of the build. |
#+TEMPLATE: | The layout this page renders through. |
#+OPTIONS: | Per-file export switches. |
#+CAPTION:, #+ATTR_HTML: | Attach to the image directly below them — a blank line in between attaches to nothing, as in org. A captioned image is numbered Figure N:. |
#+TBLFM: | Kept inert, and that matches org: the HTML exporter does not recalculate formulas either, so both emit the cells as written. Recalculate in Emacs (C-c C-c) to change them. |
Every other #+KEYWORD: is available to templates as {{ page.keywords.that_keyword }}, so metadata orgo has never heard of still reaches your layout.
Not supported, and what happens instead
The contract is not that these work — it is that they degrade predictably and never crash a build.
| Construct | What happens |
|---|---|
Babel execution, :results | The source block renders as code. A checked-in #+RESULTS: block is dropped. |
#+INCLUDE: | Never expanded, and reported: the build prints file:line: `#+INCLUDE: …` is not expanded, so a page is never quietly missing content. --strict makes it a failure. |
| LaTeX, MathJax | Survives as the literal text you typed. |
Macros {{{name}}}, radio targets | Literal text. |
Drawers other than PROPERTIES | Captured and dropped, including LOGBOOK. |
| Non-HTML export blocks | Dropped entirely. |
#+TODO: sequences | Not read; the default keyword set is used. |
| Planning lines, =: = fixed-width | Render as ordinary paragraphs. |
Why #+RESULTS: is dropped rather than rendered
Babel is never executed, so a checked-in results block is output from someone else's Emacs session at some other time. Emitting it would put unverifiable content on the page dressed as real content. The source block renders; its stale output does not.
Diagnostics
Malformed input degrades rather than failing — but not silently, because the worst cases are severe. An unterminated #+BEGIN_SRC reads the rest of the file as block content, and an unterminated drawer does the same but renders to nothing, so one missing line can delete most of a page.
warning: post.org:42: unterminated `#+BEGIN_SRC` block (no `#+END_SRC`); everything to the end of the file was read as block content
Diagnostics carry exact line numbers through arbitrarily nested constructs, and --strict turns them into a non-zero exit.
Measured against Emacs
cargo test --test oracle exports each fixture with org's own exporter via emacs --batch and snapshots the disagreement. Heading structure, list nesting and source-block text are asserted to match exactly.
The rest differs deliberately:
| orgo | Emacs | |
|---|---|---|
| emphasis | <em> / <strong> | <i> / <b> |
| captioned image | <figure> / <figcaption> | <p> + "Figure 1: …" |
| timestamp | <time datetime"…">= | literal <2024-01-15 Mon> |
| footnotes | <section><ol> | <h2>Footnotes:</h2> |
| heading anchor | slug of the text | org1a2b3c4 |
| code | <pre><code> | <pre> |
One genuine semantic difference: org treats a single blank line between a 1. list and a following - list as one list, keeping the first item's bullet type. orgo starts a second list. That was kept on measurement — the pattern occurred zero times across a 179-file reference corpus — rather than on taste.