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.
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.
| Rule | Source | Status | |
|---|---|---|---|
| Shared by every type | |||
| 1 | A 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. | §3 | Specified |
| 2 | Nesting 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.1 | Specified |
| 3 | Attributes 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.1 | Specified |
| 4 | An 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 |
| 5 | An unknown attribute key on a known type is an unknown-attribute warning; the key is preserved. | §4 | Specified |
| 6 | caption 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.2 | Specified |
| 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. | §4 | Specified |
| 8 | Prose between two blocks has a derived address: P-between-N, C-before-N, C-after-P. Addresses are matched, never parsed back. | §4 | Specified |
| meta | |||
| 9 | Key–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 · §4 | Specified |
| 10 | Several 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. | §4 | Specified |
| 11 | With two or more meta blocks, another block declaring {#meta} is a reserved-id error. | §4 · A.2 | Specified |
| 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.3 | Specified |
| 13 | profile is a reserved key declaring application-layer vocabularies; the title belongs in meta, not in an H1 (a style note). | §4 · §8.6 | Specified |
| 14 | #meta["version"] reads one merged value by coordinate. | GEP-0011 | GEP draft |
| math | |||
| 15 | A raw-bodied display formula, rendered as \[…\]; inline math is $…$. The body goes to a math renderer; the processor never interprets it. | §3 · §5.1 | Specified |
| note | |||
| 16 | Flow 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 · §4 | Specified |
| 17 | A footnote [^id] may point at any block with an id; a note is the usual target. | §5.2 | Specified |
| text | |||
| 18 | A 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-0004 | Specified |
| 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.2 | Specified |
How fences open and close, how unknown things degrade, which attributes anyone may use. With these settled, each type's own section is short.
=== #id spares you counting =, but a bare run of the opening length inside the body still closes the block early. The processor says so.==== 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
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
==== around ===); labeled closes are recommended for long blocks, against miscounting, not against truncation.#fences-after-early: inside container #fences, after #early, with no following block. That is what "prose has an address" means.=== 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
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
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.%% SpecifiedFour kinds of "the processor does not know this or should not show it", each with its own degradation.%% 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 ===
[[#hiddennote]] resolves, and nothing appears on the page.Unknown attribute on a known type.
warning · unknown attribute `foo` for block type `note`See hiddennote.
**not parsed** are literal. An unknown attribute is likewise kept, with a warning.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: a key–value body that merges into one namespaceIt does not render. It supplies three things: the document title, the values {{key}} interpolates, and the profile declaration.
{{key}} is single-pass Specified=== 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. ===
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
Version 3, owner docs, escaped {{version}}, code {{version}}.
Attribute values are not interpolated.
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.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.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.=== 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. ===
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
#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.#meta a coordinate root (#meta["title"]), so one address with two readings would also answer with two different values.math: a display formulaThe simplest type. Raw body, handed to a math renderer; the processor's only duty is to leave it alone.
=== 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]].
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>
$…$, equally verbatim. {{key}} is not interpolated inside inline math (§5.3 phase 1). [[#euler]] takes its text from the caption, "Euler".format=: math has no competing DSLs the way diagrams do; the body is TeX-style text and the renderer picks its engine.note: a callout with chromeFlow body, may nest blocks. The whole difference from text is the chrome.
.warning joins the class list Specified==== note {#warn .warning caption="Careful"} A callout with **inline** markup and a nested block: === code {lang=sh} geml check doc.geml === ==== #warn Reference: [[#warn]].
A callout with inline markup and a nested block:
Reference: Careful.
**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.text. For a footnote, [^id] pointing at a note is the usual shape (§5.2).===, so the outer fence must be longer: ====. The labeled close ==== #warn is used at the same time; the two do not conflict.text: an id for a run of prose, nothing moreAdded by GEP-0004. A neutral container with no chrome; it exists for geml get/set #intro, [[#intro]] and versioned rollback.
![[#id]] projecting only a single paragraph SpecifiedOne paragraph projects; two is an error, and only that error.=== text {#intro} One paragraph of addressable prose. === === text {#two} First paragraph. Second paragraph. === Projection of one paragraph: ![[#intro]]. Projection of two: ![[#two]]. Reference: [[#intro]].
error: `![[#two]]` projects inline content, but the target is not a
single-paragraph `text` block; for block content use
`=== embed {src=#two}` (line 28)
One paragraph of addressable prose.
First paragraph.
Second paragraph.
Projection of one paragraph: One paragraph of addressable prose. Reference: intro.
![[#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}.[[#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.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.
| Probe | Covers | check result | Where on this page |
|---|---|---|---|
| p1-fences.geml | nested fences, labeled close, same-length bare run closing early | 1 warning stray-labeled-fence | Shared · fig. 1 |
| p1b-fencelike.geml | unbraced attributes, unterminated block | 1 error 1 warning | Shared · fig. 2 |
| p2-meta.geml | meta merge, interpolation, escape, no interpolation in attributes, unknown key | 1 error 1 warning | meta · 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 clean | meta · fig. 2, board 11 |
| p4-blocks.geml | math, note with nested block, text projection, hidden, unknown attribute, unknown type, %%, caption as link text | 1 error 2 warning | Shared · fig. 3, math, note, text, board 19 |