orgo

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:

WrittenPublished
--
---
...
x^2x superscript 2
H_{2}OH 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

KeywordEffect
#+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.

ConstructWhat happens
Babel execution, :resultsThe 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, MathJaxSurvives as the literal text you typed.
Macros {{{name}}}, radio targetsLiteral text.
Drawers other than PROPERTIESCaptured and dropped, including LOGBOOK.
Non-HTML export blocksDropped entirely.
#+TODO: sequencesNot read; the default keyword set is used.
Planning lines, =: = fixed-widthRender 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:

orgoEmacs
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 anchorslug of the textorg1a2b3c4
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.