krz/orgo

Lightning fast org-mode static site generator.

clone: git clone https://gitbay.org/krz/orgo.git

v0.21.0: 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.