Ddot.it is a small language for knowledge graphs. It is meant to be used within existing files, which have their own structure and syntax. We call it the host language, where ddot.it is the guest language. Ddot.it has a generic triple model and a simple syntax. A gentle introduction can be found at ddot.it.

About this document
  • Last Modified: 2026-07-28

  • Version: 1.2.0

Introduction

  1. What is the Purpose of ddot.it?

  2. Where can I use it? Guest and Host Language

  3. What can I express structurally with ddot.it? Information Model

  4. How do I type ddot.it syntax? Syntax Concepts

Purpose

Ddot.it is meant to be spread over many files. Together, all these files should be parsed by a ddot.it parser, creating one large knowledge base. This knowledge base can interlink and describe concepts, and it can annotate the documents in which the ddot.it syntax has been typed. Crucially, ddot.it is meant to be typed alongside other content in another syntax. In fact, it has been designed specifically so that it coexists with other syntaxes: Markdown and AsciiDoc syntax have no meaning in ddot.it and also the other way round.

Guest and Host Language

  • Ddot.it can be used as a custom text format for representing knowledge graphs.

Spec

If ddot.it is used stand-alone, the following is defined:

  • File extension: .ddot

  • MIME type: text/ddot.it

  • Ddot.it can be used as a guest language inside another host language or even a more complex host system. For example, ddot.it can be used within Markdown and AsciiDoc. In general, a host language SHOULD be parsed by a host-language aware parser. That parser SHOULD strip away host syntax for encoding host structures. For example, ## Foo defines a headline in Markdown, so only Foo should be given to the ddot.it parser.

Information Model

Ddot.it has a simple, triple-based information model.

  • Subject (S, about what are we stating facts),

  • Relation (P, named after RDF property/predicate), and

  • Object (O, another objects name or a data value).

Additionally, a triple can have any number of attached Meta Relation (MR) and Meta Object (MO) pairs. This resembles RDF 1.2 / RDF-Star and provides a way to state facts about the triple.

Data Model
  • A snippet has any number of triples. Its source URI is that of the chunk it comes from; positions within the snippet are given as line numbers relative to that chunk.

  • A triple consists of subject, relation, and object.

  • Additionally, a triple can have any number of (meta relation, meta object) pairs.

  • Each of subject, relation, object, meta relation, and meta object is a string value.

Diagram
Figure 1. The ddot.it Information Model
In the information model a Triple always has all three of Subject, Relation and Object. The AST shows Subject and Relation as 0..1 because the syntax may leave them out — an omitted subject is inherited from the previous line and an omitted relation is the implicit links to. The subject is filled in by the time a triple reaches this model; the relation is filled in by the consumer, because the serialised form omits it rather than writing links to out.

Triple Events

The information model above says what a triple is. This section says how a parser reports one. There is exactly one wire format, the triple event, and it is normative: an implementation conforms by emitting these bytes.

Example Snippet as ddot.it Syntax
Dirk ..works at.. SAP ,, ..since.. 2016
The same snippet as triple events
{"from":"Dirk","type":"works at","to":"SAP","meta":[{"type":"since","to":"2016"}],"kind":"asciidoc","source":"file:///Users/maxvolkel/Desktop/example.adoc","location":1}
Spec: event fields

A parser emits one JSON object per recognised triple. An unrecognised line (NotATriple) emits nothing — it is not an error and has no representation.

Field Req. Value
 from

yes

The subject, as a string. When the syntax omitted it, the inherited subject is already substituted here.

 type

no

The relation, as a string. Omitted entirely when the syntax used the untyped …​. / .. .. form — it is not written out as links to. A consumer defaults a missing type to links to.

 to

yes

The object, as a string. Never empty: an empty object is not a Triple.

 meta

no

The triple’s meta pairs, in source order. Omitted when the triple has none — never emitted as an empty array.

 kind

yes

The host format the reader read, as a short lowercase name: ddot, markdown, asciidoc, html, xml, yaml, java, pptx, … Fixed per reader, identical on every event that reader emits.

 source

yes

The chunk’s source URI, verbatim. This is the value !!this stands for.

 location

yes

The 1-based line number of the triple’s first line, relative to its chunk. A triple whose subject or object is a !!block is located at its opening line.

Meta pairs use the same two keys as the event itself. Each element of meta is an object with type (optional) and to (required) — deliberately the same names as on the triple, because a meta pair is a (relation, object) pair. Three cases arise:

Syntax Meta pair

,, ..since.. 2016 — a typed pair

{"type":"since","to":"2016"}

,, .... 2025 — the untyped form

{"to":"2025"} — type omitted, meaning links to, exactly as on a triple

,, a random note — free meta text

{"type":"text","to":"a random note"} — the built-in text relation, not links to

Values are emitted verbatim. No unescaping, no HTML escaping, no quote stripping: "a sample document" keeps its quotes and <https://example.com/x?a=1&b=2> keeps its angle brackets and &. A field filled by a !!block carries the body’s real newlines, which become \n in the JSON string.

Commands are not resolved at this layer. !!this is emitted as the literal from value ddot.it/this (or !!this, whichever was written); substituting source for it is the collector’s job. !!off, !!on and !!block are pre-parse commands and never appear as events at all — they have already decided which text was parsed and what the block-filled field contains.

Spec: serialisation

Events are serialised as JSON Lines (JSONL): one event per line, \n-separated, UTF-8, no enclosing array and no separators between objects.

Conformance is asserted byte for byte against the golden corpus, so serialisation is fixed, not merely semantic:

  • Key order on an event is from, type, to, meta, kind, source, location, with absent optional keys simply left out. Within a meta pair it is type, to.

  • String escaping covers \", \\, \n, \r, \t, \b, \f, and \uXXXX for any other control character. Nothing else is escaped — in particular /, <, > and & are written as-is.

  • No insignificant whitespace: no spaces after : or ,, no indentation, no trailing newline beyond the line separators themselves.

See the golden corpus at test-data/cases/*/expected.events.jsonl.

Ddot.it Triple Event JSON Schema

The schema of one event. A JSONL stream is a sequence of documents each matching it.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "ddot.it triple event",
  "type": "object",
  "properties": {
    "from":     { "type": "string", "description": "Subject" },
    "type":     { "type": "string", "description": "Relation; omitted means 'links to'" },
    "to":       { "type": "string", "description": "Object" },
    "meta": {
      "type": "array",
      "description": "Meta pairs, in source order. Omitted when empty.",
      "minItems": 1,
      "items": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "description": "Meta relation; omitted means 'links to'" },
          "to":   { "type": "string", "description": "Meta object" }
        },
        "required": [ "to" ],
        "additionalProperties": false
      }
    },
    "kind":     { "type": "string", "description": "Host format of the source" },
    "source":   { "type": "string", "description": "Source URI of the chunk" },
    "location": { "type": "integer", "minimum": 1, "description": "1-based line number" }
  },
  "required": [ "from", "to", "kind", "source", "location" ],
  "additionalProperties": false
}
Two layers, one key vocabulary

Ddot.it splits reading from collecting (Developer Guide → Architecture):

  1. A reader pulls ddot.it text out of one host format and emits the triple events defined above.

  2. A collector merges the events from many readers into one knowledge base and does the resolution a single reader cannot — substituting source for !!this, defaulting an absent type to links to, applying prefix, merging duplicate triples into one entry set.

The two layers are genuinely different — one is a stream keyed to source positions, the other a merged graph — but they use one key vocabulary. A triple is from / type / to on both sides of the pipe, and the collector’s document is the concatenation of the events it received.

Earlier drafts of this specification gave the collector document its own key names — sourceUri plus triples of {s, p, o} with {p, o} meta pairs — so that the same triple had two spellings depending on which side of the pipe it sat on. That served no purpose beyond forcing every tool to translate, and it is retired. Any document or implementation still emitting s/p/o is stale.

Syntax Concepts

Ddot.it tries to use few syntax elements, to make learning, reading and writing easy. The following syntax elements are used:

Double dot (..)

It’s used for writing triples. This most fundamental syntax element, the double dot (ddot) is what gave ddot.it its name.

Example
Claudia ..owns.. cat
Whitespace

Space and tab are significant but never their exact amount. So ddot.it has a middle ground between 'whitespace is optional' (its not) and 'indentation level matters' (it doesn’t).

Double comma (,,)

For appending meta data to a triple.

Example
Claudia ..owns.. cat ,, ..since.. X-mas 2026
Double exclamation mark (!!)

For commands.

Newlines

These are significant as they terminate a triple. A triple is recognised by its ../.. markers, so ddot.it does not add a way to abort or continue a triple across a newline. The one place a newline is otherwise required — stating several (relation, object) pairs about one triple — has an inline substitute, the double semicolon.

Double semicolon (;;)

Used only inside a triple’s meta part (introduced by ,,), where it separates several (relation, object) assertions about that triple on a single line — the inline form of writing one per line in a ,, block. In a subject, relation, or object, ;; is ordinary text with no special meaning.

Syntax Shorthands

Subject Optional

To avoid repeating the same subject over and over, ddot.it allows to omit it after the first line. So instead of

sss ..ppp1.. ooo1
sss ..ppp2.. ooo2
sss ..ppp3.. ooo3

one can write

sss ..ppp1.. ooo1
..ppp2.. ooo2
..ppp3.. ooo3
There is no shorthand for expressing multiple objects for the same subject-relation combination.

Commands

Commands extend ddot.it capabilities in various ways while keeping the extra syntax small.

All commands can be typed in several ways, which are interpreted identically:

The !!COMMAND_NAME is easy to type, whereas the https://ddot.it/COMMAND_NAME is the most self-documenting version. In practice, ddot.it/COMMAND_NAME provides the best balance between readability and self-documenting. The expansion from !! could be done by editor plugins at typing time or ddot.it CLI tools.
All commands are designed to be resolvable HTTPS URI (in their ddot.it/COMMAND_NAME form). Each URI resolves to a documentation page for the exact command. In the future, that page could even explain the interpretation of parameters.
RFC 3986 defines the URI syntax:
The following are two example URIs and their component parts:

         foo://example.com:8042/over/there?name=ferret#nose
         \_/   \______________/\_________/ \_________/ \__/
          |           |            |            |        |
       scheme     authority       path        query   fragment
          |   _____________________|__
         / \ /                        \
         urn:example:animal:ferret:nose

A ddot.it command thus consists of

  • command-begin := ( https://ddot.it/ | http://ddot.it/ | ddot.it/ | !! )

  • command := command-begin command-name uri-query? uri-fragment?

  • command-name := one or more characters that are not WS, NL, ? or
    (the name stops at ?/; the command does not — see below)

  • uri-query := ? then any characters up to WS, NL or #

  • uri-fragment := # then any characters up to WS or NL

Spec

The slash is part of command-begin, and the name is required. In the three URL spellings the / that separates the host from the path belongs to command-begin, not to the name; in the !! spelling there is no / at all. Consequently:

  • ddot.it/this, http://ddot.it/this, https://ddot.it/this and !!this are the four spellings of one command, and are interpreted identically.

  • A bare ddot.it — no slash, no name — is not a command. Prose may mention the project by name, and a URL may point at the site, without either becoming a command. (It may still act as the optional file-selection marker, which is a plain substring test and has nothing to do with command syntax.)

  • ddot.it/ with an empty name is likewise not a command.

A command ends at the first WS or NL — and at nothing else. WS is TAB or any Unicode space separator, so a tab- or NBSP-terminated command is recognised exactly like a space-terminated one; do not implement this as "up to the next space".

? and # do not terminate a command: they introduce its query and fragment, which are part of the command. They end only the name. So in !!block?end=END the whole token is one command — name block, query ?end=END — and in ddot.it/foo?a=1#frag the command runs to the end of #frag.

Two Kinds of Commands

Commands fall into two groups by when they act:

Special Pre-Parse Commands

These three commands must be respected at the ddot.it parser level: !!off, !!on, and !!block.

Regular In-Text Commands

!!this, and any other ddot.it/<name>.

!!label was an in-text command in earlier versions of ddot.it. It is retired: label is now an ordinary relation (foo ..label.. bar), defined in the Vocabulary Specification. This changes nothing at the syntax level — see Spec.

Pre-Parse Commands

They are recognised while a chunk is split into snippets, before the snippet grammar runs. They decide which text is parsed (!!off/!!on) or taken verbatim (!!block), and so never appear as nodes in the AST.

Spec

!!off and !!on are recognised anywhere on a line, not only at line start and not only alone on a line. The pre-parser scans for the command token itself; whatever precedes it on the line is irrelevant to recognition.

  • !!off and !!on take effect from the point where they occur. Text before an !!off on the same line is still parsed; text after an !!on on the same line is parsed again.

  • This is what lets a host format’s own comment syntax carry the command. The Vocabulary Specification steers its own parsing with // https://ddot.it/off — AsciiDoc comment lines that keep the commands out of the rendered document while still reaching a plain ddot.it parser.

!!block is recognised only when it fills a whole field — see !!block fills a field. A !!block inside running text is not an opener: it stays ordinary command-shaped text with no pre-parse effect. Allowing openers mid-text would force the spec to define how the text around the opener and the multi-line body glue together — which whitespace survives, where the newlines go — and that is exactly the kind of rule ddot.it avoids.

Because !!off/!!on recognition is position-independent, those two are recognised inside running prose too. Writing "use !!off to disable parsing" in a document will disable parsing from that point. Documents that need to discuss these commands without triggering them must keep them out of the raw text — for instance behind the host format’s own escaping, or by turning parsing off around the discussion. A !!block in the middle of prose is inert; only one that lexically fills a field slot opens (see the recognition rule in Spec: !!block fills a field).
A host-aware parser is a different matter. If an AsciiDoc-aware ddot.it parser strips // comment lines before handing text over, the commands inside them never arrive. Steering that relies on comments therefore works with a plain ddot.it parser reading the raw file, and a host-aware parser SHOULD preserve pre-parse commands it finds in host comments rather than discarding them.

Command !!off

Ddot.it has no comments in its syntax. But only conforming lines are interpreted as triples. To explicitly exclude certain lines from interpretation — effectively like a block comment — ddot.it provides ddot.it/off and ddot.it/on commands.

Spec

The !!off command excludes the following text from ddot.it parsing. Until parsing resumes with an !!on command.

!!off is recognised anywhere on a line, not only when it stands alone on one. The overwhelmingly common way to write it is inside a host-language comment — <!-- ddot.it/off -->, # !!off, // !!off — and requiring a bare line would make the command unusable in exactly the documents ddot.it is designed to annotate. A host-aware reader strips comment syntax before parsing, but ddot.it must also work when no such reader is available.

The line carrying the command is a directive line: it yields no triple, only the command token, and the excluded region begins with the following line. Whatever else the line contains — comment markers, indentation — is neither parsed nor coloured.

The command name is still matched exactly, so !!office is not !!off: the character after the name must be whitespace, ?, #, or the end of the line.

This is a deliberate trade. Any line mentioning !!off switches parsing off — including prose about the command. Documents that discuss ddot.it syntax should keep such mentions inside a !!block (where commands are inert) or accept that the rest of the file is excluded.
This command, like all commands, is not active when inside a Command !!block.

Command !!on

Spec

This command resumes ddot.it parsing. If it was already on, nothing changes.

Like !!off, !!on is recognised anywhere on a line and its line is a directive line: no triple, only the command token, and parsing resumes with the following line.

This command, like all commands, is not active when inside a Command !!block.

See also: The !!off command.

Command !!block

JSON, for example, allows to type foo\"bar to effectively have a single double quote in the string between foo and bar and get foo"bar as a value. In ddot.it, quotes don’t need to be escaped, as they carry no semantics in the syntax. Other character sequences, such as .., ,,, ;;, !! or newlines cannot be used everywhere.

Ddot.it has no escape syntax. Instead, ddot.it provides a !!block command. The block command allows almost any character to be put verbatim into ddot.it, without interpreting it. By default, a !!block ends with the first blank line, but a custom end marker line can be defined.

Spec: !!block fills a field

!!block (in any command spelling, with or without ?end=) opens a verbatim region only when it is the entire content of one of these four positions: Subject, Object, MetaObject, or the free meta text after ,,. The block’s body — the lines from the line after the opener down to its terminator — becomes that position’s complete value, verbatim.

Only those four. A !!block in a Relation or a MetaRelation is not an opener; it is ordinary in-text command-shaped text. The four supported positions are the ones that hold values, which is where a multi-line verbatim value is actually wanted. Relation and meta-relation hold names, and a multi-line relation name has no use.

The meta text position is redundant but allowed: the multi-line ,,,, block form already expresses a verbatim multi-line meta text, so … ,, !!block says nothing that cannot be said without it. It is permitted anyway because excluding it would be the odd rule — meta text is a value like the other three, and an author who has just learnt !!block should not have to learn where it stops working. See [block-in-meta-text] for how the two forms differ.

The opener ends its physical line. A field-filling !!block never has anything to its right: the body follows on the next lines, and whatever would have come after the field follows after the block terminates. Recognition is therefore lexical, without running the snippet grammar. The command token is an opener iff

  • to its left, across WS only, lies the start of the line or one of DT2, DT4, CM2, SC2; and

  • to its right, across WS only, lies the end of the line.

Anything else — a !!block with text before it in the same field, or inside running prose — is not an opener; it is ordinary in-text command-shaped text with no pre-parse effect. Allowing openers mid-text would force rules for how the surrounding text and the multi-line body glue together — which whitespace survives, where the newlines go; requiring the opener to end its line avoids that entirely.

The logical line resumes after the terminator. The spliced-in field is sealed: no further token can extend it. The line directly after the terminator is checked:

  • If its first token (after WS) is one the interrupted position accepts after a complete field, that line continues the logical line. Three of the four opener positions have such a token:

    • after a block subject — a DT2 or DT4 continues, e.g. ..knows.. Bob

    • after a block object — a CM2 continues, e.g. ,, ..since.. 2016

    • after a block meta object — an SC2 continues, e.g. ;; ..until.. 2020

    • after a block meta text — nothing continues. Meta text is the last thing a logical line can hold, so the line always ends at the terminator.

  • Otherwise the logical line ends at the terminator, and the next line is an ordinary fresh line. In particular, plain text after a block object starts a new line (Example 1: Dirk … is a fresh triple), and ..p.. o after a completed block-object triple is the ordinary omitted-subject shorthand, not a continuation.

A continuation line may itself end in another !!block, so one logical line can carry several blocks (e.g. a block subject and a block object), each body directly following its own opener:

!!block
Alice
Anderson

..knows.. !!block
Bob
Baker

,, ..since.. 2016

reads as S = Alice⏎Anderson, P = knows, O = Bob⏎Baker, with one meta pair. Each opener still ends its own physical line, and each body is terminated before the logical line resumes.

Two openers on one physical line is impossible by construction: an opener must have end-of-line to its right, so nothing can follow it on that line. In !!block ..knows.. !!block the first token has DT2 to its right and is therefore not an opener — it is inert in-text text, and the subject is the literal string !!block. Only the second token opens.

Further rules:

  • In the meta text position (… ,, !!block) the form is allowed but redundant. The body becomes the triple’s meta text — the value of the built-in text relation — exactly as the multi-line ,,,, form already does:

    water ..formula.. H2O ,, !!block
    solid, liquid, gas
    all three occur naturally

    states the same meta as

    water ..formula.. H2O ,,
    solid, liquid, gas
    all three occur naturally
    ,,

    The two differ only in how they end — a blank line or ?end= marker versus a closing ,, — and in that a !!block body is taken verbatim, so a ,, or .. inside it stays literal. Prefer the ,,,, form; reach for !!block only when the text itself contains a line that would close that form.

  • If the assembled logical line does not parse as a Triple, the line, its bodies and its continuations are emitted as NotATriple (Unterminated Constructs applies at end of input as usual). The bodies were still consumed verbatim — commands inside them stay inert either way. This also covers a lone !!block in prose: being line-initial it does open, and the whole construct comes out as NotATriple.

Example 1
John ..address.. !!block
1 Broad Way
Lower Manhatten
..No !!command has an effect here..
U.S.A.

Dirk ..works at.. SAP
Example 2
John ..address.. !!block?end=END
1 Broad Way
Lower Manhatten
..No !!command has an effect here..
U.S.A.
END
Dirk ..works at.. SAP
Interpretation (for both Examples)
  • S=John, P=address, O=1 Broad Way
    Lower Manhatten
    ..No !!command has an effect here..
    U.S.A.

  • S=Dirk, P=works at, O=SAP

Example 3 — !!block in other fields
!!block?end=EOS
../../notes.adoc
EOS
..has type.. relative path
water ..formula.. H2O ,, ..note.. !!block
solid, liquid, gas -- all three
phases occur naturally

Dirk ..works at.. SAP
Interpretation (Example 3)
  • S=../../notes.adoc, P=has type, O=relative path
    (a subject containing .. — impossible to type directly, since Subject excludes DT2. The ..has type.. line continues the logical line after the EOS terminator.)

  • S=water, P=formula, O=H2O, Meta: note = solid, liquid, gas — all three
    phases occur naturally

  • S=Dirk, P=works at, O=SAP — a fresh line: plain text cannot continue the completed water triple.

This shows the difference between syntax and information model. In the information model every field value may contain newlines, .., ,,, and other otherwise-special sequences. At the syntax level those cannot be typed directly into their field (Object := TextExcept{NL,CM2}, Subject := TextExcept{NL,DT2,DT4}, …); the !!block wrapper is what puts them there verbatim. A subject containing .., for instance, can only be written via !!block.
The ?end= Parameter

!!block takes one optional command parameter, end, naming the line that closes the block. It is an ordinary URI query, so all four command spellings accept it: !!block?end=END, ddot.it/block?end=END, and the http(s):// forms.

Spec

!!block is terminated by:

  • the first blank line, when no end parameter is given; or

  • the first line whose content equals the end marker, when one is given.

With an end marker:

  • The marker matches only as a whole line. Leading and trailing WS on that line are ignored; the marker itself is compared literally and is case-sensitive. A line merely containing the marker does not close the block.

  • Blank lines no longer terminate the block, so a marked block may contain them. This is the reason to use ?end= at all.

  • The marker line is not part of the block’s value.

  • The marker runs to the first WS, since a command ends there (command-begin[^ ]+). A marker containing a space cannot be written.

  • If the marker line never appears, the block runs to the end of the chunk.

Highlighting

The marker appears twice and both occurrences are tagged block-end, which is what visually pairs an opener with its terminator:

john ..address.. !!block?end=END
                 ^^^^^^^ ^^^^^ ^^^
                 command  |    block-end   (the marker in the opener)
                          command-param
1 Broad Way

still inside: a blank line does not close a marked block
END
^^^                                        block-end   (the terminator line)
Dirk ..works at.. SAP

!!block is a pre-parse command, so none of this appears in the AST: by the time the snippet grammar runs, the block’s lines have already become the value of the position the opener fills (any of the four). That is why the grammar has no production for it, and why the highlighter treats the body as its own verbatim region rather than as field content. See the golden corpus: 17-block (blank-line form) and 21-block-end-marker (marked form).

The golden corpus pins three of the four opener positions: 17-block and 21-block-end-marker (Object, both terminator forms), 29-block-subject (Subject, with a DT2 continuation line) and 30-block-meta-object (MetaObject). The meta text position has no case yet; one should be added.

In-Text Commands

They occur inside a triple field and are part of that field’s text (see Abstract Syntax Tree). A strict source-to-source tool leaves them untouched; interpreting tools give them their meaning.

A ddot.it parser can ignore these commands. They are handled by higher layers interpreting the triple+meta stream. New commands can be used without changing this part of the spec.

Spec

Command syntax is recognised without consulting any registry of command names. Anything matching a command spelling — !!NAME, ddot.it/NAME, http(s)://ddot.it/NAME — is a Command node, whether or not NAME is a command this or any other specification defines.

Consequences:

  • A parser or highlighter needs no list of known commands, and stays correct as commands are added or retired. !!label is still lexed and highlighted as a Command even though label is no longer a defined command — see corpus case 15-label.

  • Deciding what a command means — including that an unknown or retired one means nothing — belongs to the interpreting layer, never to the parser.

The interpreting layer holds the registry. It knows which command names it implements. A command-shaped token whose name it does not know carries no command semantics, and the field keeps its ordinary value — the text stands for itself.

Spec: telling a ddot.it URI value from a command

Ddot.it URIs live under the same host as commands, so an object like https://ddot.it/vocabulary/2026-07-20#entity is command-shaped. The two-layer split above resolves it without any new syntax:

  1. The parser tags it as a Command, as it tags every command-shaped token. It does not and need not decide whether this is "really" a command.

  2. The interpreting layer looks the name up. vocabulary/2026-07-20 is not a command it implements, so no command semantics apply and the field keeps its plain value — the URI string, which is exactly what ..uri.. wants.

This is why the vocabulary’s own ..uri.. https://ddot.it/vocabulary/2026-07-20#entity and its ddot ..prefix.. namespace value behave correctly: they are unknown names, so they stay values. It also means defining a new command can change how existing documents are interpreted — a name that used to be an inert value becomes live. Keep command names short and at the root (ddot.it/NAME), and keep URI values on deeper paths, so the two spaces do not collide in practice.

Command !!this

To annotate the document in which the ddot.it is typed, a built-in command, !!this, can be used.

  • It is replaced with the sourceUri of the chunk in which it is written, when the ddot.it syntax is to be interpreted.

  • Usually used as Subject.

  • A strict source-source processor (e.g. a linter) should leave the !!this as it is.

Parsing Process Overview

Now we know the Information Model and Syntax Concepts, we look into the process from file on disk to triples in memory.

We have several goals in parsing:

  • Extract triples (and their meta and sourceUri) from a chunk.

  • Syntax-highlight text in a chunk. For this, we need to tag each character into a set of nested but non-overlapping regions. We must classify characters into a tree. Then, we define styling via CSS for each region and thus define the syntax highlighting.

  • Auto-completion support. Building on tagged regions, we need to define for each substate (in which region is the cursor, what’s before and after the cursor, how we got at this state: by typing or moving the cursor?) whether to launch auto-completion or not. And what to show.

As all goals are interwoven, we explain them together. The purpose of this spec is to clearly define the parse semantics. Actual implementations for fast parsing don’t need to construct tagged regions for every character.
Process
  1. First, we must get chunks (text blocks) to be parsed from the system in which ddot.it has been entered.

  2. Then we pre-parse the chunk into snippets — and the excluded parts. This needs to carefully process both !!block (verbatim) and !!off/!!on (exclude) commands.

Chunks: Getting Text for Parsing

  • Ddot.it can appear in any text box, in any system or file. Getting the text out of a system in general requires help from the host system (e.g. Atlassian Confluence) to deliver a documents’ text (and its sourceUri) to the ddot.it parser.

  • Plain .ddot text files can be parsed directly. Markdown and AsciiDoc documents can also be parsed with a plain ddot.it parser, but specialised parsers offer more features.

  • More complex formats such as HTML, Java source code or PowerPoint slides SHOULD be parsed with format-specific ddot.it parsers.

Spec

Every file is parsed by default. Ddot.it exists to augment files in other formats with structured annotations, so a tool MUST NOT require a file to opt in before looking at it. The default behaviour is: parse everything you are pointed at, and report whatever triples are found. A file containing no ddot.it syntax simply yields no triples — which is not an error and needs no marker to arrange.

The signature string is an optional filter, not a precondition. The character sequence ddot.it appearing anywhere in a file — as an explicit ddot.it/on, or merely as a ddot.it URL, or even the bare word in prose — MAY be used as a marker, in the manner of a magic byte signature. It has an effect only when a tool is explicitly run in a mode that skips unmarked files (e.g. an --only-marked option over a large corpus, where scanning everything is too expensive).

What counts as a marker. A file is marked if either holds:

  1. the character sequence ddot.it appears anywhere in it, or

  2. any command appears, in any of its four spellings.

The second clause is not implied by the first: !!this is a command containing no ddot.it substring at all, and it marks the file.

Every command is a marker; a marker is not a command. The implication runs one way only. Bare ddot.it — in a URL, or as a word in prose — satisfies the marker and is not a command, because a command requires the slash and a non-empty name. So a file may be marked without containing a single command, and a tool that only needs to answer "is this file marked?" can do so with a substring scan plus the !! form, without a command parser.

Such a mode is an opt-in optimisation. Enabling it can only ever lose triples that the default would have found; it never changes how a file that is parsed is interpreted.

This is the same principle the Highlight Specification applies at D9, where the injection grammar deliberately ships with no marker precondition and lets the line-shape gate decide. Parsing and highlighting agree: look at everything, and let the syntax speak for itself.
Chunk
  • Usually, one file is one chunk. Some files, like YAML files, can encode multiple documents in a single file. In a wiki, each page could be one chunk.

  • The host parser must deliver one chunk for each such part.

  • Each chunk must be given its own sourceURI, which is often derived from file URI plus an appended hash fragment. This fragment can be a chunk index or start line number. Line numbers help tracing back the origin of unintended triples. The sourceUri is also what allows ddot.it to annotate parts of a document via the !!this command.

Spec

A host format parser delivers chunks with a sourceUri to the ddot.it parser. For plain .ddot files, the ddot.it parser does this itself.

Each pair of text chunk plus URI is the given to the ddot.it parser.

Spec: chunk preprocessing

Two normalisations are applied to a chunk before anything else, in this order:

  1. Strip a leading byte-order mark. A single U+FEFF at the very start of the chunk is an encoding artefact, not content, and is removed. Only there, and only one: a U+FEFF anywhere else is an ordinary character (it is category Cf, not Zs) and is part of whatever field contains it.

  2. Normalise newlines, in this order — CR LFNL, then LFNL, then CRNL.

    The order matters, and so does testing CR LF first. A pattern written (\r|\n|\r\n) matches the CR of a CRLF and leaves the LF behind as a second newline, silently turning every line ending into a blank line.

After this the only line terminator is NL, and line numbers are counted over the result. Split in a way that preserves trailing empty lines (split("\n", -1) in Java; plain split("\n") in JavaScript), or the location of later triples drifts.

Splitting Chunks to Snippets

A chunk MAY be split into smaller snippets if it contains !!off and !!on commands. Otherwise, all the chunks’ content is the snippet. In any case, the pre-parser must be careful in respecting the interleaving of both Command !!block and Command !!off/Command !!on.

A stateful parser must remember if it is in plain ddot.it or in verbatim ddot.it. When in plain, the off/on commands have an effect and bring the parser into the excluded state.

Diagram
Figure 2. Pre-Parsing Chunks to Snippets
Spec

Each chunk is pre-processed by a ddot.it parser into snippets. All snippets of a chunk share the chunk’s source URI; they are located by line numbers relative to the chunk. Leaving out the subject only works within a snippet, not across snippet boundaries.

Parsing a Snippet

This is where the meat of the parsing happens.

In case of doubt, the golden corpus at ddot.it/test-data/cases/ shall have the last word.

Lexical Layer

Only the following basic tokens have a special role when parsing ddot.it.

Table 1. Token Alphabet, Single Codepoints,
Token Name Comment Regex
WS

Whitespace

TAB or any Unicode space separator — see Spec: what counts as WS

[\t\p{Zs}]
NL

Newline

CR, LF, or CR+LF

(\r\n|\r|\n)
DT

Dot

A single dot .

\.
CM

Comma

A single comma ,

,
SC

Semicolon

A single semicolon ;

;
EM

Exclamation Mark

A single exclamation mark !

!
Spec: what counts as WS

WS is TAB (U+0009) plus every character in the Unicode general category Zs (Separator, space). It is never a newline: NL is a separate token, and no WS character is a line terminator.

Written as a Unicode property, WS is [\t\p{Zs}]. Not every regex flavour supports \p{Zs}, so the equivalent explicit class is normative and MUST be used where the property is unavailable:

[\t\u0020\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]
Codepoint Name
 U+0009

CHARACTER TABULATION (TAB)

 U+0020

SPACE

 U+00A0

NO-BREAK SPACE

 U+1680

OGHAM SPACE MARK

 U+2000-U+200A

EN QUAD …​ HAIR SPACE (11 characters)

 U+202F

NARROW NO-BREAK SPACE

 U+205F

MEDIUM MATHEMATICAL SPACE

 U+3000

IDEOGRAPHIC SPACE

Zs is defined by the Unicode standard, not by this document: an implementation using \p{Zs} is conforming even if a future Unicode release adds a character to the category. The explicit class above is the equivalent as of Unicode 15.

Why more than space and tab. Ddot.it is typed into documents that pass through word processors, wikis, PDFs and chat clients, and those routinely substitute a NO-BREAK SPACE (U+00A0) or a NARROW NO-BREAK SPACE (U+202F) for an ordinary space — as does kbd:[Option+Space] on macOS. Such a character is visually indistinguishable from a space, so treating it as ordinary text would silently produce a different subject. Berlin typed with a trailing SPACE and Berlin typed with a trailing NO-BREAK SPACE would be two different nodes that look identical in every editor and every rendering. A knowledge graph that splits nodes on invisible differences is worse than useless, and the author has no way to see what went wrong. Accepting the whole Zs category draws the boundary at "characters Unicode says are spaces", which needs no case-by-case argument.

Zero-width characters are not WS. U+200B ZERO WIDTH SPACE, U+FEFF ZERO WIDTH NO-BREAK SPACE (BOM) and the other format characters are category Cf, not Zs, and are ordinary text. They are invisible rather than space-like, and silently discarding them would lose data.

Implementations that reach for their language’s built-in trim must check what it actually strips. JavaScript’s String.prototype.trim removes Zs and U+FEFF and line terminators — close, but it over-strips U+FEFF. Java’s String.trim strips only characters <= U+0020, which is far too narrow, while String.strip uses Character.isWhitespace, which excludes U+00A0 and U+202F — exactly the two that matter most here. Prefer an explicit class over a built-in.

The syntax of ddot.it is fundamentally based on counting consecutive characters. The main element is the double-dot (..) which is only valid if it is not preceded or followed by another dot. One or three dots are fine and can be used in text as usual, without any special interpretation. Four dots, well, we made that a special case of the empty relation, so that has a defined interpretation. Five dots are fine and mean nothing.

And here we define the counted token sequences.

The lookarounds are what express "neither preceded nor followed by another one of the same character" — the counting rule cannot be written with a quantifier alone. \.{2} on its own would happily match the first two dots of …​, which is exactly what Tokenization rule forbids. These are the same forms the Highlight Specification uses, and they are normative.
Table 2. Token Sequences
Token Description Regex
DT2

A sequence of 2 consecutive DT.
The whole sequence is neither preceded by another DT nor followed by another DT.

(?

DT4

A sequence of 4 consecutive DT.
The whole sequence is neither preceded by another DT nor followed by another DT.

(?

CM2

A sequence of 2 consecutive CM.
The whole sequence is neither preceded by another CM nor followed by another CM.

(?

SC2

A sequence of 2 consecutive SC.
The whole sequence is neither preceded by another SC nor followed by another SC.

(?

EM2

A sequence of 2 consecutive EM.
The whole sequence is neither preceded by another EM nor followed by another EM.

(?

TX

Any maximal sequence of characters that contains no WS and no NL, and in which no DT2, DT4, CM2, SC2, or EM2 occurs.
This also covers symbol characters that do not form one of those tokens: a lone ., ,, ;, or !, a run of three or more dots, and so on (see Tokenization rule).

(see Tokenization rule)

Tokenization rule

Symbol characters (., ,, ;, !) are grouped into maximal runs of the same character. A run is a special token only at its special length:

  • a run of dots of length 2 is DT2, of length 4 is DT4;

  • a run of commas of length 2 is CM2;

  • a run of semicolons of length 2 is SC2;

  • a run of exclamation marks of length 2 is EM2.

Every other symbol run — a single ., three or five dots, a lone ,, ;, or ! — is ordinary text and lexes as TX, exactly like a run of word characters. Adjacent TX pieces with no intervening WS, NL, or special token form a single TX token. So Node.js, Mr. Smith, and U.S.A. are each a plain subject or object value, with no special interpretation.

Grammar in EBNF

Syntax (of the grammar)
  • Terminals are UPPERCASE

  • Variables are TitleCase

  • ? optional, * zero or more, + one or more, | alternative, () grouping

  • // comments about the syntax itself. ddot.it has no comments syntax.

Disambiguation

The productions below would be ambiguous read as a plain context-free grammar, because `Line’s second alternative matches any text and therefore overlaps every other production. Two rules make the grammar deterministic; both match what the parse automaton does operationally.

Ordered choice

| is an ordered choice, as in a PEG: alternatives are attempted left to right and the first one that matches wins. A later alternative is reached only when every earlier one fails. So Line is "a Triple if it can be, otherwise text".

Greedy optionals and repetitions

?, and + are greedy: they consume as much input as they can while still allowing the rest of the production to match. In particular Meta? is preferred *present whenever a Meta can be derived.

Together these resolve the one genuine overlap in the grammar — whether a ,, on the line after a triple opens that triple’s meta block or is just text.

  • TextExcept{…​} is a shorthand for:

    • We have text, that does not start or end with whitespace (WS).

    • We allow all of (NL | WS | DT2 | DT4 | CM2 | SC2 | EM2 | TX) except those listed in the Except clause.

    • E.g.: TextExcept{CM2,NL} = (DT2 | DT4 | SC2 | EM2 | TX) (WS* (DT2 | DT4 | SC2 | EM2 | TX))*.

Full
Snippet     := Line*

// Ordered choice: a Line is a Triple if one can be derived, otherwise NotATriple.
// NotATriple is therefore never reachable for text that forms a Triple.
Line        := WS* Triple WS* NL
              | NotATriple NL

// Any run of tokens on one line. Reached only when the Triple alternative failed.
NotATriple  := (WS | DT2 | DT4 | CM2 | SC2 | EM2 | TX)*

DoubleDot   := WS* DT2 WS*
// the untyped (empty) relation, written '....' or '.. ..'.
// WS+, not WS*: with no WS between, the two DT2 would be four adjacent dots,
// which lexes as DT4 — that is the first alternative, not the second.
QuadDot     := WS* ( DT4 | DT2 WS+ DT2 ) WS*

Triple      :=  Subject?
                ( DoubleDot Relation DoubleDot | QuadDot )
                 Object
                 WS* Meta?

Subject     :=  TextExcept{NL,DT2,DT4}
Relation    :=  TextExcept{NL,DT2,DT4}
Object      :=  TextExcept{NL,CM2}

Meta              :=  CM2 WS* MetaInline WS*
// the block may be empty: ',,' NL WS* ',,'
                    | MetaBlockOpen NL (MetaBlock NL)? WS* CM2 WS*

// The ',,' that opens a block may sit at the end of the triple line, or alone
// on the line *after* it. Both attach the block to the same triple.
MetaBlockOpen     :=  CM2
                    | NL WS* CM2

MetaRelation      := TextExcept{NL,DT2,DT4}

MetaInline        := ( MetaTripleInline (SC2 MetaTripleInline)* )
                    | MetaTextInline

MetaTripleInline  :=  DoubleDot MetaRelation DoubleDot MetaObjectInline
                    | QuadDot MetaObjectInline

MetaObjectInline   :=  TextExcept{NL,SC2}

MetaTextInline     :=  (CM2 | SC2 | EM2 | TX)
                       (WS* (DT2 | DT4 | CM2 | SC2 | EM2 | TX))*

MetaBlock          :=  MetaTripleInBlock (NL MetaTripleInBlock)*
                    | MetaTextBlock

MetaTripleInBlock   := DoubleDot MetaRelation DoubleDot MetaObjectInBlock
                    | QuadDot MetaObjectInBlock

MetaObjectInBlock    := TextExcept{NL}

MetaTextBlock      := MetaTextBlockLine (NL MetaTextBlockLine)*

MetaTextBlockLine  := WS*
// a real line must contain anything besides just CM2 and WS
// and may not be DT2 to avoid parsing as MetaTripleInBlock/MetaRelation
                      (TX | EM2 | SC2 )+
                      (WS | TX | DT2 | DT4 | EM2 | CM2 | SC2 )*
Notes on the productions
  • SC2 (;;) is special only inside the ,, meta part, where the inline form uses it to separate several (relation, object) assertions about one triple (see MetaInline). In a Subject, Relation, or Object it is ordinary text — a triple is recognised by its ../.. markers, so ddot.it never uses ;; to terminate or abort a triple.

  • Object is mandatory and non-empty (TextExcept requires at least one token): a ..b.. with nothing after the closing .. is not a Triple.

  • Subject? is optional: an omitted subject means continue with the subject of the previous line.

  • Text is a whitespace-trimmed sequence; the WS around and between words is not part of the Subject, Relation, or Object contents.

  • QuadDot is the shorthand for an implicit relation: a…​.b and a .. .. b both mean a ..links to.. b.The two spellings are the same operator, not two operators — see 10-spaced-untyped and 02-untyped-link in the golden corpus.

  • MetaBlockOpen has two spellings, and they mean the same thing. The block form’s opening ,, may end the triple line (04-multiline-meta, 14-multiline-meta-text) or stand alone on the next line (09-standalone-comma-block). Only the block form allows the second spelling — the inline form’s ,, must stay on the triple line, since what follows it on that same line is the meta.

    The second spelling is where the grammar would be ambiguous without ordered choice and greedy optionals: a lone ,, after a complete triple could equally be that triple’s block opener or a NotATriple line. The two rules settle it: Meta? is greedy, so a Meta that can be derived is derived, and the ,, attaches.

    Two properties keep this from over-reaching:

    1. MetaBlockOpen’s second alternative is `NL WS* CM2WS never includes a newline. The ,, must therefore sit on the line immediately after the triple; a blank line in between breaks the attachment.

    2. The block form only matches if a closing ,, is also found. A lone ,, after a triple with no closing ,, derives no Meta at all, so the greedy choice falls through and that line is NotATriple — as is any ,, that follows no triple.

Abstract Syntax Tree

What a parsed ddot.it Snippet looks like as a tree.Every arrow is a contains relation; there are no other relations in this diagram.Orange nodes are exclusive choices: exactly one of their children is present.

Diagram
Figure 3. The ddot.it AST
Notes on the tree
  • Line is either a Triple or a NotATriple. NotATriple is the second alternative of the Line production: text that is passed through unparsed. It has no children.

  • Subject is absent when it is continued from the previous Line.

  • Relation is absent for the QuadDot shortcut; it then means links to.

  • Meta is either inline or block, never both. Likewise MetaInline and MetaBlock each hold either triples or text, never both.

  • MetaObject unifies MetaObjectInline and MetaObjectInBlock: the two differ only in which characters may occur, not in what they mean once parsed.

  • MetaTextInline and MetaTextBlock carry the built-in relation text, so in the Information Model they are a Meta like any other.

  • A Command is a part of the text of the field holding it, not a replacement for it: in !!this ..author.. John the Subject holds one Command, whereas in John ..text.. 42 no field holds a Command. !!off, !!on, and !!block are Pre-Parse Commands and never appear as AST nodes. Each in-text command may equally be written ddot.it/this, http://ddot.it/this or https://ddot.it/this.

  • Commands may occur in every field of a Triple and of a Meta alike: Subject, Relation, Object, MetaRelation and MetaObject.

Parse State Automaton

Diagram Legend

Red arrows indicate the 'happy path': a simple Triple without any Meta.

Diagram
Figure 4. The ddot.it Parsing Model, part 1: Triple
Diagram
Figure 5. The ddot.it Parsing Model, part 2: Meta, inline form
Diagram
Figure 6. The ddot.it Parsing Model, part 3: Meta, block form
Notes on the states
  • StartOfLine is both the start and the accepting state: a Snippet is a sequence of Line, and each Line ends by returning here. A Triple is emitted on the transition back.

  • NotATriple implements the second alternative of Line: text that is not a Triple is consumed up to the next NL and passed through unparsed.

  • NL never enters NotATriple. Every transition consumes its token, so a … -→ NotATriple : NL would consume the newline and then have NotATriple eat the following line — one bad line would swallow its innocent successor. A failure detected on NL means the line is already over, so those transitions go to StartOfLine and are annotated emit Line as NotATriple. NotATriple is entered only on a non-NL token, where there really is a rest of the line left to consume.

  • Where a state fails on NL inside a ,, block, the whole multi-line construct is abandoned and every line it spans is emitted as NotATriple — see Unterminated Constructs for what "abandoned" means operationally.

  • The DT4 shortcut skips the Relation state entirely, since QuadDot carries the implicit relation links to.

  • The same holds in the meta part: MetaTripleInline := QuadDot MetaObjectInline and MetaTripleInBlock := QuadDot MetaObjectInBlock emit the same implicit links to as the meta relation — "implicit MetaRelation" in parts 2 and 3 of the automaton means exactly that, and there is no separate meta-only default. See corpus case 24-untyped-inline-meta.

  • MetaStart is the only place where the inline and the block form are told apart: an NL right after the CM2 selects the block form, anything else the inline form.

  • A block can also be opened from StartOfLine, one line later than the triple it belongs to (MetaBlockOpen’s second spelling). `MaybeBlockOpen is the one-token lookahead this needs: a line-initial CM2 is a block opener only if an NL follows it, otherwise it is just the start of a Subject that happens to begin with ,,. Compare 09-standalone-comma-block (opener) with `09’s own first line (an ordinary triple).

  • MetaTextInline and MetaTextBlockLine are entered only on a token other than DT2 / DT4. This mirrors the grammar: once meta text has started, a later DT2 is text, not a separator (see Example 2).

  • Inside a ,,-block, triples and text cannot be mixed. BlockFirstLine commits the whole block: a leading DT2 / DT4 selects BlockTripleLine (where TX / EM2 / SC2 at line start is an error), plain text selects BlockTextLine (where DT2 / DT4 at line start is an error). This matches MetaBlock, which is either all MetaTripleInBlock or all MetaTextBlockLine.

  • The closing CM2 is recognised only at the start of a line, after optional WS, from BlockFirstLine, BlockTripleLine or BlockTextLine. A CM2 anywhere else in a block line is content.

  • A block may be empty: reaching the closing CM2 straight from BlockFirstLine gives ,, NL WS* ,, with no Meta at all.

  • The object must hold at least one token. ObjectStart skips leading WS; an NL or CM2 before any object token makes the line a NotATriple, matching the mandatory Object in the grammar.

  • SC2 (;;) is plain text everywhere in the triple line (Subject, RelationStart, Relation, ObjectStart, TripleObject, NotATriple). It becomes a separator only in the inline ,, meta form (part 2), where MetaObjectInline -→ MetaNextInline on SC2 divides several (relation, object) assertions about one triple; in the block meta form (part 3) newlines do that job and ;; is content.

Unterminated Constructs

A ,, block needs a closing ,,, and a !!block needs its terminator (a blank line, or the ?end= marker). What happens when the closing token never arrives has to be stated once, because parsing and highlighting answer it differently and both answers are correct for what they do.

Spec

Parsing. An unterminated construct produces no triple. Every line it spans — the triple line that opened it and each line after — is emitted as NotATriple and passed through unparsed. This follows directly from the grammar: the Meta alternative requires a closing CM2, so it simply fails to derive, and ordered choice falls through to the NotATriple alternative for each line.

End of input. The end of a snippet acts as an implicit NL. A final line that is not newline-terminated is therefore an ordinary Line, and a triple on it is emitted normally — Line := WS* Triple WS* NL does not require the file to end with a line break.

After that implicit NL, being in any state other than StartOfLine means a multi-line construct was left open. That construct fails: the same rule applies, and every line it spans is NotATriple. StartOfLine is the only accepting state.

Highlighting. A highlighter MAY instead render the region as if it continued to the end of the document. It is not required to reproduce the parse result, because a line-at-a-time highlighter cannot revisit lines it has already coloured (see the Highlight Specification). Colouring an unterminated block to the end of the document is a legitimate rendering of "this block was never closed".

Only the parse result is normative for the triples a tool extracts. The divergence is confined to presentation, and is visible only for input that is malformed to begin with.

References

  • The Pomsky portable regex syntax seems smart