GEML block types, illustrated · page 1 of 5 · spec 1.0 · 中文

GEML Blocks Illustrated · The Simple Four

This page covers meta, math, note and text — the four smallest types — plus the fence, id and attribute rules every type shares. Each rule names its source: a section of the specification, or a GEP. The right-hand side is not an imagined rendering but what the processor actually does: the diagnostics geml check prints, the addresses geml list gives, the tags --to html emits. Two places where the implementation disagrees with the specification are flagged on the board.

Board

19 rules, each with its source and status

Specified written into the specification. GEP draft defined by a proposal, implemented on this branch, not yet in the spec. Impl. gap the spec says one thing, 1.9.2 does another.

Specified 18 GEP draft 1
RuleSourceStatus
Shared by every type
1A fence is a run of three or more =. A block is closed by whichever closer comes first: a bare run of exactly the opening length, or, when the block has an id, the labeled fence === #id.§3Specified
2Nesting is safe only through fence length. A labeled fence spares you counting = but does not protect against a same-length bare run inside the body closing the block early; when that happens, stray-labeled-fence is a warning.§3 · A.1Specified
3Attributes without braces turn the whole line into a paragraph, with a fence-like-line warning; a block still open at end of file is an unterminated-block error.§3.1 · A.1Specified
4An unregistered type is a warning; its body is kept raw and rendered as a <figure><pre> labelled with the type name.§3 · §8.2(6)Specified
5An unknown attribute key on a known type is an unknown-attribute warning; the key is preserved.§4Specified
6caption and hidden are valid on every type. [[#id]] takes its link text from the target's caption or heading, falling back to the id. A hidden block is in the model and reference-checked, but not rendered.§4 · §5.2Specified
7%% is a comment only in block position (top level, or inside a flow body); inside a raw body it is body text, kept verbatim.§4Specified
8Prose between two blocks has a derived address: P-between-N, C-before-N, C-after-P. Addresses are matched, never parsed back.§4Specified
meta
9Key–value body, one pair per line. Value types: quoted string, true/false, number, any other bare word is a string. No arrays, dates or nesting.§3 · §4Specified
10Several meta blocks merge; for a repeated key the first definition wins and later ones are duplicate-meta-key warnings. #meta names the merged result and is the one reserved id.§4Specified
11With two or more meta blocks, another block declaring {#meta} is a reserved-id error.§4 · A.2Specified
12{{key}} is a single-pass interpolation; an unknown key is an error; no interpolation inside code spans, inline math or attribute values; \{{key}} gives the literal text.§4 · §5.3Specified
13profile is a reserved key declaring application-layer vocabularies; the title belongs in meta, not in an H1 (a style note).§4 · §8.6Specified
14#meta["version"] reads one merged value by coordinate.GEP-0011GEP draft
math
15A raw-bodied display formula, rendered as \[…\]; inline math is $…$. The body goes to a math renderer; the processor never interprets it.§3 · §5.1Specified
note
16Flow body: nested blocks and inline markup allowed. Renders as <aside class="callout note …">, with .class tokens joining the class list. It is a callout with chrome, not a neutral container.§3 · §4Specified
17A footnote [^id] may point at any block with an id; a note is the usual target.§5.2Specified
text
18A neutral flow container whose only job is to give a run of prose an id and attributes. Renders as <div class="text"> with no chrome; wrap only prose you actually need to address.§3 · GEP-0004Specified
19![[#id]] may project only a single-paragraph text block; anything else is an inline-transclusion-not-inline error, and that error alone.§5.2 · A.2Specified
Shared

Rules every type shares

How fences open and close, how unknown things degrade, which attributes anyone may use. With these settled, each type's own section is short.

Fences: bare run or labeled close, whichever comes first SpecifiedA labeled fence === #id spares you counting =, but a bare run of the opening length inside the body still closes the block early. The processor says so.
GEML · p1-fences.geml
==== note {#outer}
Outer holds an inner block.
=== code {lang=sh}
echo hi
===
====                    %% longer outside, shorter inside: safe

=== note {#labeled}
A long block closed by a labeled fence.
=== #labeled            %% labeled close: no counting

=== note {#early}
This body contains a bare run of the opening length:
===                     ← same-length bare run: the block closes here
which closed the block above at that line.
=== #early              ← closes a block that is already closed
geml check · geml list
warning: labeled fence for `#early` at line 22, but block `#early`
  was already closed by a bare fence at line 20 — body may be
  silently truncated (line 22)
0 error(s), 1 warning(s)

$ geml list p1-fences.geml
#outer               note           L7-12
=== code             code     anon  L9-11
#labeled             note           L14-16
#early               note           L18-20   ← ends at L20
#fences-after-early  prose          L21-22   ← the two lines that fell out became prose
Rule
§3: a block is closed by whichever closer appears first; an id does not disable the same-length bare close. The recommendation is an outer fence longer than any fence-like line in the body (==== around ===); labeled closes are recommended for long blocks, against miscounting, not against truncation.
Address
The two lines that fell out get the §4-derived address #fences-after-early: inside container #fences, after #early, with no following block. That is what "prose has an address" means.
Missing braces, and a block never closed SpecifiedThe two most common slips: one a warning, one an error.
GEML · p1b-fencelike.geml
=== embed src=#f          ← attributes without {}: the whole line is a paragraph
This line follows a fence-like line that had no braces.

=== note {#open}
This block is never closed.  %% end of file
geml check
warning: line looks like an open fence for `embed` but is not one —
  attributes must be braced (`=== embed {…}`); the line reads as
  plain paragraph text (line 5)
error: unterminated `note` block (no matching === or `=== #open`) (line 8)
1 error(s), 1 warning(s)   exit 1
Why
A.1: fence-like-line is a warning because the line did parse, as a paragraph — the document still stands, only any reference on that line goes unchecked. unterminated-block is an error because the body swallowed everything to end of file and the author's intent is gone.
Unknown type, unknown attribute, hidden, %% SpecifiedFour kinds of "the processor does not know this or should not show it", each with its own degradation.
GEML · from p4-blocks.geml
%% a top-level comment: kept in the file, never rendered

=== note {#hiddennote hidden}
Not rendered, but referenced: still checked.
===
=== note {#badattr foo=bar}
Unknown attribute on a known type.
===
=== fancy {#unknown}
An unknown type keeps its body raw: **not parsed**.
===
See [[#hiddennote]].

=== code {lang=ts}
export const x = 1;
%% this percent line is body text inside a raw block
===
the matching --to html output
#hiddennote: no output. The block is in the model, [[#hiddennote]] resolves, and nothing appears on the page.
aside.callout.note

Unknown attribute on a known type.

warning · unknown attribute `foo` for block type `note`
figure › pre.diagram-src data-type="fancy"
An unknown type keeps its body raw: **not parsed**.
warning · unknown block type `fancy`; body kept as raw

See hiddennote.

tsexport const x = 1; %% this percent line is body text inside a raw block
Degradation
§8.2(6): an unrecognized type keeps its body verbatim and never loses content; the asterisks in **not parsed** are literal. An unknown attribute is likewise kept, with a warning.
hidden vs %%
§4's division of labour: hidden is for structured content that belongs in the model but should not show (a chart's data source, a reusable fragment); %% is for throwaway notes that never enter the model. Inside a raw body %% is body text — the line above went into the <code> as written.
meta

meta: a key–value body that merges into one namespace

It does not render. It supplies three things: the document title, the values {{key}} interpolates, and the profile declaration.

Two meta blocks merge, first definition wins; {{key}} is single-pass Specified
GEML · p2-meta.geml
=== meta
title = "Meta probe"
version = 3
draft = true
===
=== meta
title = "Second title"       ← same key: the later one is ignored
owner = "docs"
===

# Interpolation {#interp}

Version {{version}}, owner {{owner}}, escaped \{{version}}, code `{{version}}`.

Unknown {{nope}} key.

=== note {#cap caption="{{title}}"}
Attribute values are not interpolated.
===
geml check · geml get
warning: meta key `title` already defined at line 1; later
  definition at line 6 is ignored (line 6)
error: unknown metadata reference `{{nope}}` (line 15)

$ geml get p2-meta.geml '#meta'      ← the merged namespace
title = "Meta probe"
version = 3
draft = true
owner = "docs"

$ geml get p2-meta.geml '#meta["version"]'   GEP-0011
3
Meta probetitle → page title

Version 3, owner docs, escaped {{version}}, code {{version}}.

caption="{{title}}"

Attribute values are not interpolated.

Value types
§4: a quoted string, true/false, a bare word matching number syntax is a number, any other bare word is a string. version = 3 is a number, draft = true a boolean. No arrays, dates or nested tables.
Interpolation
Single pass: a substituted value is never rescanned, so a = "{{b}}" with b = "{{a}}" cannot loop. Three places it does not happen: inside code spans and inline math, inside attribute values, inside raw bodies. \{{version}} yields the six literal characters.
Advice
Put the title in title = rather than an H1, so that every heading denotes a genuine section (§4 style note).
#meta is the one reserved id: with two meta blocks no other block may claim it SpecifiedThe spec says error, and check reports it.
GEML · p2b-reserved.geml
=== meta
title = "a"
===
=== meta
owner = "b"
===
=== note {#meta}          ← §4: with two or more meta blocks this is a reserved-id error
Declared #meta with two meta blocks.
===
geml check · geml list
error: `#meta` is reserved for this document's merged meta namespace, and this
  document has more than one `meta` block — give the block another id (line 7)

$ geml list p2b-reserved.geml
=== meta@61f06c79  meta  anon  L1-3
=== meta@0e8db6c1  meta  anon  L4-6
#meta              note        L7-9   ← one address: a reader sees a note, the processor sees the merge

$ geml check p2c-single.geml     a single meta carrying {#meta}: legal
ok: no diagnostics
Spec
§4 and A.2: #meta names the merged result, not any one block. With a single meta the block and the merge are the same thing, so it may carry {#meta}; with two or more, one address has two readings, which is an error.
Why it matters
GEP-0011 makes #meta a coordinate root (#meta["title"]), so one address with two readings would also answer with two different values.
math

math: a display formula

The simplest type. Raw body, handed to a math renderer; the processor's only duty is to leave it alone.

Display and inline Specified
GEML · from p4-blocks.geml
=== math {#euler caption="Euler"}
e^{i\pi} + 1 = 0
===
Inline math $a^2+b^2=c^2$ stays inline; block math is the typed block.
Reference: [[#euler]].
--to html
eiπ + 1 = 0

Inline math a²+b²=c² stays inline; block math is the typed block. Reference: Euler.

<div class="math-block" id="euler">\[e^{i\pi} + 1 = 0\]</div>
Rule
§3 registers it as raw; §5.1's inline form is $…$, equally verbatim. {{key}} is not interpolated inside inline math (§5.3 phase 1). [[#euler]] takes its text from the caption, "Euler".
Absent
No format=: math has no competing DSLs the way diagrams do; the body is TeX-style text and the renderer picks its engine.
note

note: a callout with chrome

Flow body, may nest blocks. The whole difference from text is the chrome.

A callout with a nested code block; .warning joins the class list Specified
GEML · from p4-blocks.geml
==== note {#warn .warning caption="Careful"}
A callout with **inline** markup and a nested block:
=== code {lang=sh}
geml check doc.geml
===
==== #warn
Reference: [[#warn]].
--to html
aside.callout.note.warning #warn

A callout with inline markup and a nested block:

shgeml check doc.geml

Reference: Careful.

Rule
§3 registers it as flow: the body is parsed for inlines and blocks, so **inline** is real strong and the nested code is a real block. §4's .class is a semantic class with no styling implied; the renderer puts it on class.
vs text
For addressable prose without callout chrome, use text. For a footnote, [^id] pointing at a note is the usual shape (§5.2).
Fences
The body contains ===, so the outer fence must be longer: ====. The labeled close ==== #warn is used at the same time; the two do not conflict.
text

text: an id for a run of prose, nothing more

Added by GEP-0004. A neutral container with no chrome; it exists for geml get/set #intro, [[#intro]] and versioned rollback.

Addressable prose, and ![[#id]] projecting only a single paragraph SpecifiedOne paragraph projects; two is an error, and only that error.
GEML · from p4-blocks.geml
=== text {#intro}
One paragraph of addressable prose.
===
=== text {#two}
First paragraph.

Second paragraph.
===
Projection of one paragraph: ![[#intro]]. Projection of two: ![[#two]].
Reference: [[#intro]].
geml check · --to html
error: `![[#two]]` projects inline content, but the target is not a
  single-paragraph `text` block; for block content use
  `=== embed {src=#two}` (line 28)
div.text #intro

One paragraph of addressable prose.

div.text #two

First paragraph.

Second paragraph.

Projection of one paragraph: One paragraph of addressable prose. Reference: intro.

Rule
§5.2: ![[#id]] is an inline projection that inserts the target's inlines into the sentence, so the target must be a text block of exactly one paragraph. Several paragraphs, a heading or any other type is an inline-transclusion-not-inline error; for a whole block use === embed {src=#two}.
No caption
[[#intro]] falls back to the id itself, "intro", because a text block has neither caption nor heading. Give it caption= for a nicer link text.
Evidence

Probe files and measured output

All run with the repository's current build, node geml-parser/dist/geml.js. The probe files live in the session scratch directory; their full text appears on the left of each figure above.

ProbeCoverscheck resultWhere on this page
p1-fences.gemlnested fences, labeled close, same-length bare run closing early1 warning stray-labeled-fenceShared · fig. 1
p1b-fencelike.gemlunbraced attributes, unterminated block1 error 1 warningShared · fig. 2
p2-meta.gemlmeta merge, interpolation, escape, no interpolation in attributes, unknown key1 error 1 warningmeta · fig. 1
p2b-reserved.geml · p2c-single.geml{#meta} under two meta blocks; a single meta carrying {#meta}1 error reserved-id on the former; the latter is cleanmeta · fig. 2, board 11
p4-blocks.gemlmath, note with nested block, text projection, hidden, unknown attribute, unknown type, %%, caption as link text1 error 2 warningShared · fig. 3, math, note, text, board 19