krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
v0.20.2: docs/guide/05-org-support.org · raw
1#+TITLE: Org support
2#+DESCRIPTION: Exactly which org syntax is handled, which is not, and how the rest degrades.
3#+LEDE: A deliberate subset, with the boundary enforced by tests rather than by hope.
4
5orgo parses a defined slice of org. The boundary is not aspirational: every supported
6construct has a golden-file test, and every excluded one has a test asserting how it
7degrades. That is what stops the parser drifting toward all-of-org.
8
9* Supported
10
11** Headings
12
13Nesting by star count, with TODO keywords, priority cookies and tags:
14
15#+BEGIN_SRC org
16,* TODO [#A] Write the parser :work:rust:
17,:PROPERTIES:
18,:CUSTOM_ID: write-parser
19,:END:
20#+END_SRC
21
22The keyword set is Emacs' default — =TODO= and =DONE= — matched on a word boundary, so a
23heading beginning "TODOs are great" is a plain title. Keyword and priority markup uses
24Emacs' own export classes.
25
26Every heading gets an =id=: its =:CUSTOM_ID:= if it has one, else its =:ID:=, else a slug
27of its text.
28
29** Text and inline markup
30
31=*bold*=, =/italic/=, =_underline_=, =+strike+=, ~=verbatim=~ and =~code~=. Links in
32every org form — external, =[[*Heading]]=, =[[#custom-id]]=, =[[id:...]]=,
33=[[file:other.org]]= — plus bare URLs in running text.
34
35Timestamps, active and inactive, with times and ranges, render as =<time>= with a
36machine-readable =datetime=.
37
38*** Text conversions
39
40Org rewrites some prose on export, and so does orgo:
41
42| Written | Published |
43|---------+-----------|
44| =--= | – |
45| =---= | — |
46| =...= | … |
47| =x^2= | x superscript 2 |
48| =H_{2}O= | H subscript 2 O |
49| =\alpha=, =\rarr=, =20\deg= | α, →, 20° |
50
51The entity table is org's own, generated from Emacs' =org-entities= rather than
52transcribed, so all 412 names behave as they do in Emacs. A name org does not know —
53=\notanentity= — stays as the literal text you typed, and =\alphabet= is a word rather
54than α followed by "bet". =#+OPTIONS: e:nil= turns the whole table off.
55
56Neither reaches inside verbatim, code, a source block or a LaTeX fragment — =--verbose= in
57a shell transcript stays a flag, and =$x^2$= stays mathematics.
58
59*Braceless subscripts catch people out.* Org's default converts =a_b=, so =snake_case= in
60prose publishes as snake with a subscript. That is what Emacs does with the same file. Turn
61it off per document with =#+OPTIONS: ^:nil=, restrict it to the braced form with =^:{}=, or
62set =[html] sub_superscript= for the site. =#+OPTIONS: -:nil= turns off the dashes and
63ellipsis.
64
65*** Heading levels are relative
66
67A file whose shallowest heading is =**= is a file of top-level sections that happen to be
68indented, not a file of subsections — org exports levels relative to the document, so that
69subtree exports the same whether it was cut from a larger file or written on its own.
70
71** Lists
72
73Unordered, ordered and description lists, nested by indentation, with checkboxes and
74multi-paragraph items:
75
76#+BEGIN_SRC org
77- outer item
78 - inner item
79- [X] a checked item
80- [-] a partly-done item
81- term :: definition
821. [@4] an item numbered from 4
83#+END_SRC
84
85Checkboxes render as org writes them — =<code>[X]</code>= with the state as a class on the
86item — rather than as a disabled =<input>=, which has no way to say "partly done".
87
88** Blocks
89
90=SRC= (syntax highlighted), =EXAMPLE=, =QUOTE=, =CENTER=, =VERSE= and =EXPORT=. A source
91block inside a quote block works, because block ends match their own kind.
92
93An =html= export block passes through verbatim; every other backend is dropped, because
94emitting LaTeX into an HTML page is worse than emitting nothing.
95
96*Any other name is a special block*: =#+BEGIN_NOTE= becomes =<div class="note">= holding
97*parsed org*, which is what makes the convention usable without orgo knowing the word
98"note". A =COMMENT= block is not published.
99
100*** Which languages highlight
101
102Highlighting uses the syntax definitions [[https://docs.rs/syntect][syntect]] bundles.
103A language it does not know is not an error — the block renders as escaped
104=<pre><code class="language-…">= with its content intact, just uncoloured.
105
106Recognised, among others: =bash= / =sh=, =c=, =c++=, =css=, =clojure=, =diff=, =erlang=,
107=go=, =haskell=, =html=, =java=, =javascript=, =json=, =latex=, =lisp=, =lua=,
108=makefile=, =markdown=, =matlab=, =objective-c=, =ocaml=, =perl=, =php=, =python=, =r=,
109=ruby=, =rust=, =scala=, =sql=, =tcl=, =xml=, =yaml=.
110
111orgo adds two syntect does not ship: *TOML* and *Org*. Both are what this project's
112own documentation needed on its first page — every config example is TOML, and a tool for
113org users gets written about in org — so they are compiled into the binary and work with
114no setup.
115
116Still missing, and worth knowing before you write a page full of them: *INI* and *Emacs
117Lisp*. For those, drop a =.sublime-syntax= file into the directory named by
118=[highlight] syntaxes_dir= (default =syntaxes/=) and it is picked up. A file that fails
119to parse is reported and skipped rather than failing the build.
120
121*** The comma escape
122
123A line inside a block that would otherwise look like document structure is written with a
124leading comma — =,* heading=, =,#+KEYWORD:= — and orgo removes exactly one comma on
125output, as Emacs does. Every org example in this documentation relies on it.
126
127The escape is not optional politeness: an unescaped =*= at column zero *ends the block*,
128in Emacs as much as here. If a code block seems to stop early, that is why.
129
130** Tables and footnotes
131
132Pipe tables, with the rule row establishing a header band and an affiliated =#+CAPTION:=
133becoming a numbered =<caption>=. Org's *special column* is
134honoured: a first column holding only export markers (=/=, =#=, =!=, =^=, =_=, =$=) is
135dropped, and rows marked =/=, =!=, =^=, =_= or =$= are instructions to org rather than
136content, so they never reach the page.
137
138Footnotes in all three forms —
139=[fn:1]= references, =[fn:1]= definitions and =[fn:1:inline text]= — rendered as a
140numbered, back-linked notes section.
141
142** Images
143
144A description-less link to an image file renders as =<img>=. With an affiliated
145=#+CAPTION:= or =#+ATTR_HTML:= it becomes a =<figure>= with the caption as both
146=<figcaption>= and alt text:
147
148#+BEGIN_SRC org
149,#+CAPTION: The pipeline, end to end
150,#+ATTR_HTML: :width 640 :class diagram
151[[file:pipeline.svg]]
152#+END_SRC
153
154Links to non-=.org= files are understood as asset links: neither resolved nor reported as
155broken.
156
157* Keywords with meaning
158
159| Keyword | Effect |
160|---------+--------|
161| =#+TITLE:= | Page title. Falls back to the filename stem. |
162| =#+DATE:= | Sorts listings. Any org date syntax. |
163| =#+DESCRIPTION:= | The excerpt shown in listings. |
164| =#+FILETAGS:= | Tags, for grouping and =page.tags=. |
165| =#+SLUG:= | Sets the output filename. |
166| =#+DRAFT:= | Keeps the page out of the build. |
167| =#+TEMPLATE:= | The layout this page renders through. |
168| =#+OPTIONS:= | Per-file export switches. |
169| =#+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:=. |
170
171Every other =#+KEYWORD:= is available to templates as
172={{ page.keywords.that_keyword }}=, so metadata orgo has never heard of still reaches
173your layout.
174
175* Not supported, and what happens instead
176
177The contract is not that these work — it is that they degrade predictably and never crash
178a build.
179
180| Construct | What happens |
181|-----------+--------------|
182| Babel execution, =:results= | The source block renders as code. A checked-in =#+RESULTS:= block is *dropped*. |
183| =#+TBLFM:= | Inert. The table renders with the values as written. |
184| =#+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. |
185| LaTeX, MathJax | Survives as the literal text you typed. |
186| Macros ={{{name}}}=, radio targets | Literal text. |
187| Drawers other than =PROPERTIES= | Captured and dropped, including =LOGBOOK=. |
188| Non-HTML export blocks | Dropped entirely. |
189| Entities =\alpha= | Literal text. |
190| =#+TODO:= sequences | Not read; the default keyword set is used. |
191| Planning lines, =: = fixed-width | Render as ordinary paragraphs. |
192
193** Why #+RESULTS: is dropped rather than rendered
194
195Babel is never executed, so a checked-in results block is output from someone else's
196Emacs session at some other time. Emitting it would put unverifiable content on the page
197dressed as real content. The source block renders; its stale output does not.
198
199* Diagnostics
200
201Malformed input degrades rather than failing — but not *silently*, because the worst
202cases are severe. An unterminated =#+BEGIN_SRC= reads the rest of the file as block
203content, and an unterminated drawer does the same but renders to nothing, so one missing
204line can delete most of a page.
205
206#+BEGIN_EXAMPLE
207warning: post.org:42: unterminated `#+BEGIN_SRC` block (no `#+END_SRC`); everything to
208the end of the file was read as block content
209#+END_EXAMPLE
210
211Diagnostics carry exact line numbers through arbitrarily nested constructs, and
212=--strict= turns them into a non-zero exit.
213
214* Measured against Emacs
215
216=cargo test --test oracle= exports each fixture with org's own exporter via
217=emacs --batch= and snapshots the disagreement. Heading structure, list nesting and
218source-block text are asserted to match exactly.
219
220The rest differs deliberately:
221
222| | orgo | Emacs |
223|-+---------+-------|
224| emphasis | =<em>= / =<strong>= | =<i>= / =<b>= |
225| captioned image | =<figure>= / =<figcaption>= | =<p>= + "Figure 1: …" |
226| timestamp | =<time datetime="…">= | literal =<2024-01-15 Mon>= |
227| footnotes | =<section><ol>= | =<h2>Footnotes:</h2>= |
228| heading anchor | slug of the text | =org1a2b3c4= |
229| code | =<pre><code>= | =<pre>= |
230
231One genuine semantic difference: org treats a single blank line between a =1.= list and a
232following =-= list as *one* list, keeping the first item's bullet type. orgo starts a
233second list. That was kept on measurement — the pattern occurred zero times across a
234179-file reference corpus — rather than on taste.