GEML vs. other markup formats
September 4, 2026 · View on GitHub
English | 中文
How GEML compares to Markdown (GitHub-flavored), HTML, CommonMark (strict core), AsciiDoc, Org-mode, and Pandoc Markdown.
Note on Pandoc: it is really a converter plus its own Pandoc Markdown —
the most feature-complete Markdown dialect. Its attribute syntax
{#id .class key=val} is in fact the ancestor of GEML's. Pandoc's own
super-powers — multi-format conversion and programmable Lua filters — sit on
a different axis than the per-element comparison below.
A note on framing: this is not a checkbox race. AsciiDoc, in particular, ships more built-in elements out of the box than GEML does. GEML's case rests on three things no other format here offers together — and the comparison is meant to make those visible, not to win a feature count:
- One primitive for every structured block — lowest syntax surface to learn, parse, or generate (which is why it's friendly to AI).
- Build-time reference checking — a broken cross-reference is an error, not a
silent dead link — and it covers content, not just links: a transcluded
block, an external CSV, a
.jsonllog, a code line range. - Self-contained version history (
.gemlhistory) — without git or any online service.
Legend: ✓ native · ◐ via extension/convention · ✗ none · (H) needs raw HTML.
For a construct-by-construct walk through CommonMark 0.31.2 alone — every leaf block, container block and inline, with what GEML does to each — see GEML vs. CommonMark.
Capability matrix
| Element / capability | GEML | Markdown (GFM) | HTML | CommonMark | AsciiDoc | Org-mode | Pandoc Markdown |
|---|---|---|---|---|---|---|---|
| Headings | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Bold / italic | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Inline code | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Code block (with language) | ✓ | ✓ | ◐ | ✓ | ✓ | ✓ | ✓ |
| Lists | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Links / images | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Audio / video embed | ✓ | ✗ (H) | ✓ | ✗ | ✓ | ◐ | ✗ (H) |
| Tables | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| Data / computed-column tables | ✓ | ✗ | ✗ | ✗ | ◐ csv | ◐ formulas | ✗ |
| Structured data as a verified block | ✓ json/jsonl | ✗ | ◐ unchecked | ✗ | ◐ unchecked | ◐ babel | ✗ |
| Body pulled from an external file | ✓ checked | ✗ | ✗ | ✗ | ✓ unchecked | ✓ unchecked | ◐ filter |
| Admonitions / callouts | ✓ | ◐ alerts | ◐ | ✗ | ✓ | ◐ | ◐ fenced div |
| Footnotes | ✓ | ✓ | ◐ | ✗ | ✓ | ✓ | ✓ |
| Definition lists | ✗ | ✗ | ✓ | ✗ | ✓ | ✓ | ✓ |
| Super/subscript, inline spans | ✗ | ✗ | ✓ | ✗ | ✓ | ◐ | ✓ |
| Math (inline / block) | ✓ | ◐ | ◐ | ✗ | ✓ | ✓ | ✓ |
| Diagrams (hosted DSL) | ✓ | ◐ mermaid | ✗ | ✗ | ✓ | ✓ | ◐ filter |
| Chart bound to a table or data block | ✓ | ✗ | ✗ | ✗ | ◐ | ◐ | ✗ |
| Citations / bibliography | ✗ | ✗ | ✗ | ✗ | ◐ | ✓ | ✓ |
| Document metadata | ✓ native block | ◐ frontmatter | ✓ | ✗ | ✓ | ✓ | ✓ |
| Block id + cross-reference | ✓ | ◐ headings only | ✓ | ◐ | ✓ | ✓ | ✓ |
| Transclusion (another document's block, in place) | ✓ checked | ✗ | ◐ iframe | ✗ | ✓ preprocessor | ✓ | ◐ filter |
| Build-time reference checking | ✓ error | ✗ | ✗ | ✗ | ✓ warns | ◐ | ✗ |
| Raw-HTML escape hatch | ✗ (by design) | ✓ | — | ✓ | ✓ | ✓ | ✓ |
| Plain-text legible (no rendering) | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ |
| Programmable filters / macros | ✗ (by design) | ✗ | ✗ | ✗ | ◐ | ✓ | ✓ Lua |
| One primitive for all blocks | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Self-contained version history | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
The three bold rows are GEML's real differentiators. "Raw HTML = ✗" is a feature, not a gap: it keeps semantics portable and tied to no backend. Pandoc Markdown is the broadest column here — it adds citations, definition lists, and Lua filters — yet it still lacks the one primitive, build-time checking, and self-contained history.
Side-by-side syntax
Code block
GEML === code {#hello lang=python}
print("hi")
===
Markdown ```python
print("hi")
```
HTML <pre><code class="language-python">print("hi")</code></pre>
CommonMark ```python
print("hi")
```
AsciiDoc [source,python]
----
print("hi")
----
Org-mode #+begin_src python
print("hi")
#+end_src
Pandoc ```{.python}
print("hi")
```
Document metadata
GEML === meta
title = "Budget plan"
===
Markdown --- (YAML frontmatter — convention, not spec)
title: Budget plan
---
HTML <meta name="title" content="Budget plan">
CommonMark (no mechanism)
AsciiDoc = Budget plan
:version: 0.1
Org-mode #+TITLE: Budget plan
Pandoc --- (YAML metadata block — first-class)
title: Budget plan
---
Admonition / callout
GEML === note {#risks}
Vendor lock-in is the main risk.
===
Markdown > [!NOTE] (GitHub extension)
> Vendor lock-in is the main risk.
HTML <div class="note">Vendor lock-in is the main risk.</div>
CommonMark (no mechanism — plain blockquote only)
AsciiDoc [NOTE]
====
Vendor lock-in is the main risk.
====
Org-mode (no standard — special block, export-dependent)
Pandoc ::: {.note}
Vendor lock-in is the main risk.
:::
Cross-reference, and whether it is checked
GEML See [[#budget]] → #budget missing ⇒ build ERROR
Markdown See [budget](#budget) → broken link passes silently
HTML See <a href="#budget">… → not checked
CommonMark See [budget](#budget) → not checked
AsciiDoc See <<budget>> → processor WARNS on unresolved xref
Org-mode See [[budget]] → partially checked on export
Pandoc See [budget](#budget) → not checked (xref via pandoc-crossref filter)
Transclusion — content that lives in another document
GEML === embed {src=handbook.geml#refund}
=== → renders that block here. src= is
reference-checked, a transclusion cycle
is an error, and handbook.geml is PARSED
AS ITS OWN DOCUMENT — its metadata and
its relative paths resolve against
itself, not against this file.
Inline form: ![[handbook.geml#refund]] (inside a sentence)
Markdown (no mechanism — Obsidian's ![[note]] is an app extension)
HTML <iframe src="handbook.html#refund"> (a nested browsing context,
not content in this document)
CommonMark (no mechanism)
AsciiDoc include::handbook.adoc[tag=refund] (preprocessor: the target's
TEXT is spliced in, and then
resolves against THIS file)
Org-mode #+INCLUDE: "handbook.org::#refund" :only-contents t
Pandoc (no native form — a filter, e.g. pandoc-include)
Table with computed columns (GEML-specific)
GEML === table {#fy25 format=csv header=1}
Segment, Q1, Q2, Q3, Q4
Cloud, 1, 2, 3, 4
===
=== view {#fy25-report src=#fy25
compute="FY [%.1f] = Q1 + Q2 + Q3 + Q4"
summary="Segment = 'Total'; FY = sum(FY)"}
=== → per-row FY column + a Total summary row,
FY shown to 1 decimal
Org-mode | Segment | Q1 | Q2 | Q3 | Q4 | FY |
|---------+----+----+----+----+----|
#+TBLFM: \$6=\$2+\$3+\$4+\$5 (the inspiration — but a full spreadsheet:
cell refs, remote(), Emacs Lisp. GEML
keeps a restricted column-formula subset)
others static tables only — no computation
Structured data, verified (GEML-specific)
GEML === data {#limits format=json}
{ "retries": 3, "timeout_ms": 500 }
=== → parsed and VERIFIED at build time; a
malformed body is an error naming the
offending line. #limits is addressable,
referenceable and versioned like any
other block.
=== data {#events format=jsonl src=events.jsonl}
=== → the log stays a plain .jsonl file every
existing tool can append to and tail;
the document is its verified, chartable
view. A body AND src= is an error —
one source, always.
others a ```json fence — text the processor never reads, never checks, and
cannot address, reference or chart. HTML's
<script type="application/json"> and Org's #+NAME'd src block come
closest, and neither verifies the payload.
Code that names its source, instead of copying it
GEML === code {#parse lang=ts src=src/parser.ts#L14-24}
=== → the block IS those lines. If the file no
longer has them the build FAILS
(bad-source-range); a body kept
alongside src= is a snapshot, and warns
when it has drifted from the source.
AsciiDoc [source,ts]
----
include::src/parser.ts[lines=14..24]
---- → whatever sits at those lines today,
silently — a drifted range is not an error
Org-mode #+INCLUDE: "src/parser.ts" src ts :lines "14-25" (same, at export)
Pandoc ```{.ts include=src/parser.ts startLine=14 endLine=24} (filter)
MD/HTML copy-paste, and it rots
Diagram (hosting an external DSL)
GEML === diagram {#flow format=mermaid}
graph LR
A --> B
===
Markdown ```mermaid (GitHub renders it; no id/caption/check)
graph LR
A --> B
```
AsciiDoc [mermaid]
----
graph LR
A --> B
----
Org-mode #+begin_src plantuml :file out.png
...
#+end_src
Pandoc ```{.mermaid} (rendered by a filter, e.g. mermaid-filter)
graph LR
A --> B
```
HTML/CMark no native diagram hosting
Chart bound to a table or a data block (GEML-specific)
GEML === diagram {#rev format=geml-chart data=#fy25 type=bar x=Segment y=FY}
=== → renders table #fy25 as a chart; column
refs checked. data= takes the same three
targets a table's src= does: a block in
this document, doc.geml#id in another,
or a file. A data block feeds a chart
when its value is a record array.
others hand-copy data into a chart lib, or a spreadsheet app — no link
What only GEML does
Every format above can render a heading and a code block. The difference is what happens to a whole document under change and automation:
- A single typed block carries code, tables, diagrams, math, callouts, and metadata — so there is one grammar to learn and one grammar for a tool (or an LLM) to emit correctly, instead of a different syntax per feature plus an HTML fallback.
- References are validated at build time. An
#idthat doesn't resolve fails the build, instead of slipping through as a dead link the way it does in Markdown/HTML. - Content may live elsewhere and still be checked. An
embedblock (or an inline![[…]]) renders another document's block in place;src=gives acodeblock a line range in a real source file, atableits CSV, adatablock its.jsonllog. Every named target is resolved at build time — a dangling reference, a transclusion cycle, and a line range that has drifted since the file changed all fail the build. AsciiDoc and Org-mode can include too, but they splice text: the target's own relative paths then resolve against whoever included it, and a stale line range yields the wrong lines quietly. In GEML the named document is parsed as a document in its own right, so it resolves against itself at every depth of the chain. - Data is verified, not quoted. A
datablock carries JSON's value domain — scalars, sequences, maps — as data the build parses and rejects when malformed, where a```jsonfence in every other format here is text nobody reads. It takes an#id, so it can be referenced, block-edited, versioned and charted; and withsrc=events.jsonlthe records stay a plain append-and-tail file while the document becomes their verified view. - History is self-contained. A sibling
.gemlhistoryfile reconstructs any past revision and rolls the document back — offline, with no git and no online service. See the history extension.
Pandoc plays a different game — it is the universal converter, and the most
practical way to reach docx/latex/epub. A natural future for GEML is to
join that ecosystem (a Pandoc reader/writer) rather than compete with it.
See the core specification for the full format, and the README for a quick tour.