Builds upon the Ddot.it Parse Specification.
Syntax Layers
The ddot.it syntax happens in roughly three layers:
-
Finding snippets via
!!on,!!off, and!!block. -
Parsing snippets to triples (s, p,o ) and meta (p, o) via
..and,,.Syntaxaaa ..bbb..ccc ,, ..ppp.. qqq ;; ..rrr.. sss
-
Processing commands within s, p, o via
!!.
The resulting regions in layer 1 are
-
excluded — between
!!offand!!on -
verbatim — a
!!blockbody -
plain — everything else, refined by layer 2
-
plus the pre-parse command markers themselves (
!!off,!!on,!!block), which are not regions but tokens, and are styled like any other command.
Within plain, on layer 2, a line is one of:
WS* subject WS* DT2 WS* relation WS* DT2 WS* object WS* Meta? NEWLINE
DT4 or DT2 WS+ DT2, no relation slotWS* subject WS* ( DT4 | DT2 WS+ DT2 ) WS* object WS* Meta? NEWLINE
Either form may omit the subject (a continuation line), and Meta? is one of:
SC2CM2 WS* DT2 WS* meta-relation WS* DT2 WS* meta-object ( WS* SC2 WS* … )*
DT2 after the CM2CM2 WS* meta-text
CM2 at end of line or alone on the next lineCM2 NEWLINE ( meta-triple-line | meta-text-line )* WS* CM2
The meta-relation slot is likewise absent in the untyped (DT4) meta form.
And in layer 3 we refine each of subject, relation, object, meta-relation, meta-object and meta-text into:
-
text*
-
command
-
text*
Highlighting
Regions
Highlighting happens, as far as TextMate grammars are used, in two layers.
| Region | Visually | Extent |
|---|---|---|
excluded | block comment |
|
verbatim | string literal | a |
plain | normal text | everything else, refined by the Default Layer below |
pre-parse command | keyword-style, including the | the |
| Default Layer | Class | Visually |
|---|---|---|
WS | Whitespace | Default text; whitespace outside an entity is NOT highlighted |
subject | Entity | Consistent across all five slots, see D3 |
relation | Entity | |
object | Entity | |
meta-relation | Entity | |
meta-object | Entity | |
meta-text | Entity | Free text after a |
DT2 / DT4 / | Marker | Signature syntax of ddot.it, important to highlight |
CM2 ( | Marker | Opens and closes the meta part |
SC2 ( | Marker | Only inside the inline meta part |
EM2 + URI-path | In-Text-Command | keyword-style, including the |
The concrete role and scope names for every row are in Scope Name Mapping, which is normative; this table only fixes the visual classes.
TextMate Grammar Encoding
This section records how the ddot.it syntax is encoded as a TextMate grammar, and the design decisions taken.
TextMate grammars have one hard constraint that shapes everything below: the parser looks at one line at a time and matches its regexes greedily, left to right. It cannot look ahead to a later line, and it cannot first decide "is this whole line a valid triple?" before styling its tokens. Multi-line constructs are expressed only through paired begin/end rules.
Design Decisions
D1 — Deployment: standalone core + injection wrapper
Two grammars are shipped, sharing one rule set:
-
A standalone grammar,
scopeName: source.ddot, bound to the.ddotfile extension. The whole file is ddot.it. -
An injection grammar that `include`s the same repository so ddot.it lights up inside host languages (Markdown, AsciiDoc, …).
The core rules live in the repository and are written once; each grammar is a thin shell around them.
D2 — Format-agnostic specification
This document describes the grammar abstractly — rules, regexes and scope names — rather than committing to a concrete .tmLanguage plist or tmLanguage.json. Either serialization is generated from the tables below. The property-list vs. JSON choice is left to the toolchain (see the TextMate bundle guide).
D3 — Distinct scopes, uniform default appearance
subject, relation, object, meta-relation and meta-object each get their own scope name, but all share the entity.name.* prefix. Under an off-the-shelf theme they therefore render identically (as the highlight layer intends — all are class Entity), yet a theme can single out, say, subjects without any grammar change.
D4 — Standard scopes with a .ddot suffix
Scope names reuse the conventional TextMate roots (keyword, comment, string, entity, punctuation) so existing themes colour ddot.it with zero extra configuration. The trailing .ddot component lets a dedicated theme target ddot.it specifically. No bundled theme is required.
D5 — Line-shape gated, progressive
A line is treated as ddot.it only when it matches the triple skeleton; ordinary prose stays unstyled. To keep live editing responsive (D8), the gate fires on the leading skeleton — a subject (possibly empty) followed by the first ../…. marker — rather than the complete line. Everything after the first marker (relation, closing marker, object, meta) is optional in the match, so a half-typed Dirk ..works at.. already highlights.
The practical effect: what "looks like ddot.it" is a line carrying a complete operator skeleton — either two DT2 markers, or one DT4 / .. ... Each marker must be a run of exactly two (or four) dots, not part of a longer run.
That gate is considerably tighter than "contains ..`", and prose survives it well: a lone `.., an ellipsis …, Node.js, Mr. Smith and U.S.A. all stay plain, because none of them completes the skeleton (see corpus case 23-not-a-triple). The only place the gate is suppressed outright is an excluded region.
D5b — !!off / !!on are recognised anywhere on a line
An excluded region opens on any line containing !!off, not only on a line that holds nothing else, because the command is normally written inside a host-language comment (<!-- ddot.it/off -->, # !!off). On such a line only the command token itself is coloured; the surrounding comment markers are left unscoped, exactly as they are on any other line. The region’s content begins with the next line. !!on closes it by the same rule.
The command name is matched exactly — the character after it must be whitespace, ?, # or end-of-line — so !!office does not open a region. See corpus case 31-off-in-comment.
D6 — All four command spellings recognised
!!name, ddot.it/name, http://ddot.it/name and https://ddot.it/name are all highlighted as commands, per the regex in Token Building Blocks. A bare ddot.it/foo inside object text lights up too — command shape is recognised wherever it occurs, see Command syntax vs. vocabulary.
The slash is required and the name must be non-empty: a bare ddot.it — in a URL, or as a word in prose — is not a command and is not highlighted as one. That is deliberate, and is what keeps every prose mention of the project from lighting up. It is also independent of the file-selection marker, which is satisfied by the bare string but is a substring test, not command recognition.
D7 — Full multi-line fidelity
The ,, meta block and the !!block verbatim region are modelled as real begin/end regions with correct inner structure, not flattened. The custom !!block?end=MARKER terminator is handled with an Oniguruma back-reference from end to a group captured in begin (the same technique heredocs use). Because one rule can carry only one end, the default blank-line block and the custom-marker block are two separate rules (see Repository Rule Structure).
D8 — Progressive highlighting while typing
Consequence of D5: an incomplete triple highlights its valid prefix as it is typed, instead of staying dark until the object exists. This dovetails with the autocomplete spec.
D9 — No host trigger; gate self-selects
The injection grammar carries no magic-marker or fenced-block precondition. It is offered to host text broadly (via a low-priority injectionSelector, e.g. L:text, L:source, minus host comment/string scopes) and the line-shape gate (D5) decides, line by line, what is ddot.it. "Whenever it looks like ddot.it, highlight it — except inside an excluded region."
| In injection mode the ddot.it rules are additive over the host grammar. An excluded span stops ddot.it’s own rules from firing but does not silence the host’s tokeniser, which keeps colouring its own syntax underneath. |
D10 — Role vocabulary named after the language
The canonical role names asserted by the golden corpus were revised to match this spec’s own vocabulary:
| Was | Now | Rationale |
|---|---|---|
|
| Named after the language’s defining mark, not a generic category. Covers |
|
| Follows |
|
| The Parse Spec and the Syntax Layers section already say excluded. |
(untokenized) |
| The |
Two roles are new, covering syntax the corpus did not previously exercise: meta-separator (;;) and block-end (the MARKER in !!block?end=MARKER).
Scope Name Mapping
The abstract classes of the Regions tables map to concrete TextMate scopes as follows. The middle column is the canonical role name asserted by test-data/cases/*/expected.tokens.json; test-data/tokens.md is normative for it.
| Element | Role | Scope | Notes |
|---|---|---|---|
Excluded region ( |
|
|
|
|
|
| Via |
|
|
|
|
|
|
| Fills a whole field (see block-as-field): Subject, Object or MetaObject |
|
|
| Captured, back-referenced by |
|
|
| One role for all three spellings (D10) |
|
|
| Opening and closing alike |
|
|
| Only inside the |
|
|
|
| Field | Role | Scope |
|---|---|---|
subject |
|
|
relation |
|
|
object |
|
|
meta-relation |
|
|
meta-object |
|
|
meta-text |
|
|
Metadata deliberately shares the entity.name. / keyword.operator. roots with the main triple, so it renders as first-class content rather than as a comment. The trailing .meta component exists only so a theme can single metadata out later — the same reasoning as D3.
| Part | Role | Scope |
|---|---|---|
Whole command (prefix + name) |
|
|
Query / fragment ( |
|
|
One command splits its query further. In !!block?end=MARKER the query key ?end= is a command-param like any other, but the marker value is broken out as its own block-end role, because the identical marker also appears as the block’s terminator line and the two are meant to read as a pair. So !!block?end=END yields three tokens — command, command-param, block-end — and the closing END line yields a fourth, also block-end. See The ?end= Parameter for the full semantics.
The command rule is nested inside every field capture. A field that is entirely a command (ddot.it/this as a subject, ddot.it/block as an object) therefore carries both scopes, and the deepest one wins — which is exactly the corpus rule "a slot whose text is a command is emitted as command, not `subject`".
The rule matches command shape, not a list of names, so a retired or unknown command still highlights as one — corpus case 15-label keeps !!label in a relation slot for exactly this reason, even though label is now an ordinary relation. See Command syntax vs. vocabulary.
Whitespace outside an entity is left unscoped (D4 note in the Default Layer - Visually table). Every slot is whitespace-trimmed — without exception, inline and block alike. Free-form meta text after a ,, therefore starts at its first non-whitespace character, matching Meta := CM2 WS* MetaInline WS* in the parse spec: the WS between the ,, and the text belongs to neither token (corpus case 13-inline-meta-text).
Token Building Blocks
The counted tokens from the Parse Spec become these regexes. The lookarounds enforce "a run of exactly N, neither preceded nor followed by the same character", so a lone ., a run of three dots, etc. stay plain text (TX).
| Token | Regex (Oniguruma) |
|---|---|
| (?<!\.)\.{2}(?!\.) |
| (?<!\.)\.{4}(?!\.) |
| (?<!,),,(?!,) |
| (?<!;);;(?!;) |
| (?<!!)!!(?!!) |
command | (?:(?<!!)!!(?!!)|(?:https?://)?ddot\.it/)([^\s?#]*)(\?[^\s#]*)?(#\S*)? |
Note DT2’s trailing `(?!\.) makes it decline to match the first two dots of ...., so DT4 wins there; the leading (?<!\.) keeps it from matching the middle of ....
Repository Rule Structure
Top-level patterns, in order (order matters — first match wins, so most specific first):
-
#excluded—begin= theoffcommand,end= theoncommand;contentName=comment.block.excluded.ddot, with no inner patterns. Listed first so nothing inside is parsed.
This is the only construct that suppresses the gate (D5, D9). -
#block-verbatim-marked— a gated triple line whose object is!!block?end=MARKER.beginmatches the line through that opener and captures the marker;end=^[ \t]*\8[ \t]*$(back-reference to that capture, D7);contentName=string.unquoted.block.ddot; no inner patterns. A blank line does not end this form. -
#block-verbatim— a gated triple line whose object is a plain!!block.end=^[ \t]*$(first blank line). Otherwise as above. Placed after the marked variant. -
#meta-block-after-triple— a gated triple line that ends in,,.beginstyles the triple + opening,,;end=^[ \t]*,,[ \t]*$(closing,,); the content holds nested#meta-block-triple-line/#meta-block-text-linepatterns (the block form, part 3 of the Parse automaton). -
#meta-block-standalone— a line consisting solely of,,also opens a meta block, after an already-complete triple line. Required by golden-corpus case09-standalone-comma-block, which theTriple := … Object WS* Meta?production does not by itself allow; the corpus has the last word. -
#triple-line— the common case: one physical line carrying a triple and any inline meta (,, ..mr.. mo ;; ..mr2.. mo2or,, text). A single-linematchwith captures; each field capture carries a nestedpatternslist of#command. Progressive (D8): relation, closing marker, object and meta are all optional.
Anything matching none of these is a NotATriple line and stays plain.
Each of subject / relation / object / meta-* is captured as the text between its bounding markers (lazily, respecting its TextExcept{…} set from the grammar), assigned its entity scope, and given a nested patterns list containing #command so an in-text command inside the field is highlighted on top of the entity colour.
-
Operator alternation, not rule ordering.
QuadDotandDoubleDotare alternatives within one regex (QUADlisted first), not two rules in priority order. Leftmost-first then does the right thing in both directions:Alpha .. .. Betayields onedoubledotspanning.. .., whilea ..b.. c .. .. dstill parses as a typed triple, because its first marker sits further left. -
The object is lazy with an optional trailing meta group. That makes it stop at a
,,when one is present, yet otherwise run to end of line — so an object may itself contain..(corpus case20-object-dotdot:http://h/x..y,../../b.adoc).
Verification
The canonical grammar lives in the ddot.it-syntax-tools repository, at textmate/ddot.tmLanguage.json. It is checked against the golden corpus by tools/conformance-textmate.mjs, which tokenises each input.ddot with the real vscode-textmate + vscode-oniguruma engine, maps the emitted scopes back to the canonical role names (deepest scope wins) and diffs against expected.tokens.json.
The same corpus is the contract for every other implementation, so each has its own harness in that repository and they all assert the same expected tokens:
cd ../ddot.it-syntax-tools
npm run conformance:textmate # the canonical grammar
npm run conformance # all implementations
npm run regenerate:tokens # refresh expected.tokens.json after a grammar changeAll cases in test-data/cases/ pass. The corpus is authoritative for the count; this document deliberately does not repeat it, so it cannot go stale.
Behaviours once verified only by hand now have cases of their own: 21-block-end-marker (the ?end=MARKER back-reference, and that a blank line does not end that form), 22-inline-meta-separator (;;), and 23-not-a-triple (prose that must stay plain — …, a lone .., Node.js, U.S.A., and non-ddot.it https:// URLs). 24--28 cover the remaining grammar productions: QuadDot in meta, the empty ,, block, a DT4 continuation line, multi-line meta text, and ;; as content inside a ,, block.
Known Limitations
-
Loose over-matching (accepted, D5/D9). A line of prose that happens to complete the operator skeleton — two
..markers, or a….— is styled as a triple. The gate is a shape test, not a semantic one. -
Host bleed-through in excluded spans (D9 NOTE). In injection mode the host keeps colouring its own syntax inside an
!!off…!!onregion; ddot.it only adds the comment scope. -
A block’s continuation line is not styled as a continuation.
!!blockmay open in the Subject, Object or MetaObject position, and all three are implemented. What TextMate cannot do is carry the logical line across the block: after the terminator, the continuation line is tokenised from scratch with no memory of which field was interrupted. In practice this reads correctly anyway —..knows.. Bobafter a block subject is already a valid omitted-subject line and highlights as relation
object (corpus case29-block-subject) — but a,,or;;continuation after a block object or meta object is not recognised as resuming the triple and stays plain. -
begin/endnever backtracks. If a!!block/,,region’s terminator is never typed, the region runs to end of document — standard TextMate behaviour.
A line whose subject is omitted (..ppp.. ooo) is highlighted completely: the markers, the relation and the object all get their normal scopes — exactly as on a line that spells its subject out (corpus case 07-leading-separator-continuation). Nothing is lost for highlighting purposes.
What a TextMate grammar cannot do is show the inherited subject, because the value comes from an earlier line and TextMate carries no cross-line state. But that is a matter of displaying a value that was never typed on this line — an inlay-hint or code-lens feature of an editor integration, not a gap in the highlighting. Rendering the same character twice is not something a highlighter does at all.
Rules
-
Quoted strings (
"…") are NOT a separate scope. Single and double quotes are legal, uninterpreted characters and belong to whichever slot encloses them (subject / relation / object / meta-*). -
Whitespace between tokens is not scoped. Every slot is whitespace-trimmed, inline and block alike — including free-form meta text after a
,,, which starts at its first non-whitespace character. This followsMeta := CM2 WS* MetaInline WS*: the space between the,,and the text belongs to neither token. -
The
.. ..form is onedoubledotregion spanning the full literal.. ..(inner whitespace included). It is semantically equivalent to….— both denote an untyped link, and all three spellings share the singledoubledotrole (D10). -
Command spelling is not a scope distinction. All four spellings produce one
commandregion covering prefix and name together —!!,ddot.it/,http://ddot.it/orhttps://ddot.it/are not split from the name that follows. A theme must not be able to tell!!offfromhttps://ddot.it/offby colour. Note that the prefix is 2 characters in one spelling and up to 17 in another, so an implementation matching a fixed-width!!will get the URL spellings wrong.A command’s query or fragment is the one part that is scoped separately, as
command-param— see Scope Name Mapping, which is normative for all role names.