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-08-07

  • Version: 1.3.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

  • Media type: text/vnd.ddot.it

  • Character encoding: UTF-8, always. A charset parameter is not required; when present it MUST be utf-8. (RFC 6838, 4.2.1 requires a text/* type to say how its charset is determined — there is no US-ASCII default to fall back on.)

The vnd. facet is not decoration. In a media subtype the characters before the first . name a registration facet, so a bare text/ddot.it would parse as the undefined facet ddot, and the standards tree forbids faceted names outright. text/vnd.ddot.it is well-formed, keeps the ddot.it name, and is registerable by anyone interchanging these files. x-ddot is not an option: RFC 6648 deprecated the x- prefix, and RFC 6838 no longer treats such names as belonging to any tree.
  • 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 (abbreviated P 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.

A source provides 1 to n snippets. Each snippet is parsed. Unlike a programming language parser or Markdown parser, a ddot.it parser is not transforming its input, but extracting data from the input.

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
Syntax vs. Information Model

In the information model a Triple always has all three of Subject, Relation and Object. E.g. the AST shows Subject and Relation as 0..1 because the syntax may leave them out. Omitted parts are filled in by the time a triple reaches this model, and the serialised form carries them.

  • An omitted subject is inherited from the previous line.

  • An omitted relation (but using the .. .. markers) is the implicit links to relation.

  • A meta may just contain text, without and .. markers, which is interpreted as text relation.

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 this JSON. Whitespace is optional.

Example Snippet as ddot.it Syntax
Dirk ..works at.. SAP ,, ..since.. 2016
Claudia ..owns.. cat
The same snippet as triple events
{
  "from": "Dirk", "type": "works at", "to": "SAP",
  "meta": [ {"type":"since", "to":"2016"} ],
  "kind":"asciidoc", "source":"file:///~/Desktop/example.adoc", "location": 1
}
{
  "from": "Claudia", "type": "owns", "to": "cat",
  "kind":"asciidoc", "source":"file:///~/Desktop/example.adoc", "location": 2
}
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

yes

The relation, as a string. Always written, never omitted: when the syntax used the untyped …​. / .. .. form the relation is the implicit links to, and that is what the event carries. A consumer therefore never has to default a missing field.

 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. If a source provides multiple chunks, the URI should use different URIs per chunk, usually using URI fragment identifiers (…​sourceURI…​,#foo).

 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 (required) 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

{"type":"links to","to":"2025"} — the implicit links to is written out, 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; an untyped '....' link carries 'links to'" },
    "to":       { "type": "string", "description": "Object" },
    "meta": {
      "type": "array",
      "description": "Meta entries, in source order. Meta is omitted when empty.",
      "minItems": 1,
      "items": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "description": "Meta relation; an untyped ',, ....' pair carries 'links to'" },
          "to":   { "type": "string", "description": "Meta object" }
        },
        "required": [ "type", "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", "type", "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, applying prefix, merging duplicate triples into one entry set. Defaulting a relation is not on that list: type is always written, so an untyped link arrives as links to and a note as text.

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
Inline Form

Meta entries and the ,, are on the same line.

Block form

Meta entries follow on lines after ,, NEWLINE and are terminated by another ,, on its own line. Meta blocks can freely mix text notes, ..rel.. object, and .... object lines. Consecutive text lines are considered to be the same text note. A subject can hold any number of text notes, represented as (subject, text, text of note possibly including newlines).

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 ,,), in the inline form, that is on the same line as the ,,.

Design Rationale
  • To make "simple things simple and complex things possible", ddot.it needs a way to add multiple meta-entries to a triple even when newlines are not allowed (e.g. in HTML data-ddot attributes) or not desired. -

  • Also, a simple triple like aaa ..bbb.. ccc ,, my note should be easily extended into a triple with even more metadata pairs without having to change the triple meta into block form. So we want to allow aaa ..bbb.. ccc ,, my note ;; ..foo.. bar ;; ..ppp.. qqq.

  • To make (mental and technical) parsing simple, the double-semicolon separates meta-entries, which may be free-text, ..rel.. object pairs, or .... object pairs.

  • Notes containing ;; can be written in meta-block form.

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:

  • https://ddot.it/COMMAND_NAME

  • http://ddot.it/COMMAND_NAME

  • ddot.it/COMMAND_NAME

  • !!COMMAND_NAME

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 the character sequence ddot.it appears anywhere in it. Merely using !!someCommand should not be enough to trigger ddot.it-processing.

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.

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, 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

A chunk is the unit of provenance which a host parser provides to the ddot.it parser. The text in a chunk can use the !!this command to annotate the chunk itself, e.g., to state is author or other metadata.

  • 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.

    • In PowerPoint, each slide may be two chunks, one for the slide text and one for the slide notes.

  • 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 LF → NL, then LF → NL, then CR → NL.

    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 SHOULD only work 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.

(?<!\.)\.{2}(?!\.)

DT4

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

(?<!\.)\.{4}(?!\.)

CM2

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

(?<!,),{2}(?!,)

SC2

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

(?<!;);{2}(?!;)

EM2

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

(?<!!)!{2}(?!!)

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))*.

EBNF Grammar for Snippet Parsing
/* ==========================================================================
   ddot.it — the grammar for parsing one Snippet.

   Notation: the EBNF of the XML 1.0 specification, section 6 "Notation":
       https://www.w3.org/TR/xml/#sec-notation

   Operators used here, all from that notation:
       A ::= B     production            A?          optional
       "lit"       literal string        A*          zero or more
       #xN         character by number   A+          one or more
       [#xN-#xM]   character range       A B         sequence
       (A)         grouping              A | B       alternative
       /* ... star slash  comment        A - B       matches A but not B

   TWO SEMANTIC EXTENSIONS. The notation above fixes the *syntax* of this file;
   two of its operators are read with stronger semantics than XML gives them,
   because the grammar is otherwise ambiguous. Both are specified in the parse
   spec under "Disambiguation":

     1. `|` is an ORDERED choice, as in a PEG: alternatives are tried left to
        right and the first that matches wins.
     2. `?`, `*` and `+` are GREEDY: they consume as much as they can while
        still allowing the rest of the production to match.

   Symbol names follow the parse spec's own convention -- terminals UPPERCASE,
   non-terminals TitleCase -- rather than XML's initial-capital-for-regular-
   expressions rule, so that every name here matches the name used in the
   prose, in the AST diagram and in the parse automaton.
   ========================================================================== */


/* ==========================================================================
   1. TERMINALS

   Restated here so this file stands on its own. The normative definitions,
   including the "maximal run" counting rule, are in the parse spec's
   Lexical Layer; where the two differ, that section wins.
   ========================================================================== */

/* Any Unicode code point. A ddot.it chunk is always UTF-8. */
Char        ::= [#x0-#x10FFFF]

/* TAB plus every character in Unicode general category Zs. Never a newline.
   Zero-width characters (U+200B, U+FEFF) are category Cf and are ordinary text. */
WS          ::= #x9 | #x20 | #xA0 | #x1680 | [#x2000-#x200A]
              | #x202F | #x205F | #x3000

/* Chunk preprocessing has already normalised CR and CR LF to LF. */
NL          ::= #xA

/* A symbol run is one of these tokens only at its special length: the run must
   be neither preceded nor followed by another of the same character. A run of
   any other length -- one dot, three dots, a lone comma -- is TX. That rule is
   contextual and cannot be written in this notation; see the parse spec's
   Tokenization rule. */
DT2         ::= ".."
DT4         ::= "...."
CM2         ::= ",,"
SC2         ::= ";;"
EM2         ::= "!!"

/* Everything else: a run of characters holding no WS, no NL, and none of the
   symbol tokens above. This is where `Node.js`, `U.S.A.` and `...` land. */
TxChar      ::= Char - (WS | NL)
TX          ::= TxChar+ - (TxChar* (DT2 | DT4 | CM2 | SC2 | EM2) TxChar*)


/* ==========================================================================
   2. FIELD TEXT

   Every field of a triple holds the same shape of text: a run of tokens that
   may contain WS between its parts but is not bounded by WS. The fields differ
   only in which token they may not contain, which is written with `-`.
   ========================================================================== */

/* Every token a field is built from, except the WS that may separate them. */
Word        ::= DT2 | DT4 | CM2 | SC2 | EM2 | TX

/* Whitespace-trimmed, at least one token. No field may contain a NL: a triple
   is confined to one logical line. */
Text        ::= Word (WS* Word)*

/* A name stops at the '..' markers that delimit it. */
NameWord    ::= Word - (DT2 | DT4)
Name        ::= NameWord (WS* NameWord)*

/* An object stops at the ',,' that opens the meta part. */
ObjectWord  ::= Word - CM2
ObjectText  ::= ObjectWord (WS* ObjectWord)*

/* An inline meta entry stops at the ';;' that starts the next entry -- the
   object of a pair and a free-text note alike. */
EntryWord   ::= Word - SC2
EntryText   ::= EntryWord (WS* EntryWord)*


/* ==========================================================================
   3. GRAMMAR
   ========================================================================== */

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 | Word)*

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, Relation and MetaRelation are the same production. They keep
   separate names because the AST does. */
Subject     ::= Name
Relation    ::= Name
Object      ::= ObjectText


/* --- meta ---------------------------------------------------------------- */

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        ::= Name


/* --- meta, inline form: entries separated by ';;' ------------------------- */

/* One chain of entries, in any mix: `,, ..k.. v ;; a note ;; .... o` is a
   typed pair, a note, and an untyped pair, in that order.

   An entry may be EMPTY -- `;;` parts entries and demands nothing of them, so
   `,, ;; x` and a line ending in `;;` both derive. An empty entry carries no
   metadata and is dropped: it is not an error, and a separator is not a
   promise that something follows.

   MetaInline itself is never empty, though, which is what keeps a trailing
   `,,` unambiguous: with no SC2 the single entry must be there, so `,,' NL is
   only ever a MetaBlockOpen. Hence the two alternatives rather than a plain
   `MetaEntryInline? (SC2 MetaEntryInline?)*`. */
MetaInline          ::= MetaEntryInline? (SC2 MetaEntryInline?)+
                      | MetaEntryInline

/* Ordered choice: the two dotted forms are tried first, and text is what is
   left. So a '..' that never closes is a note, not an error -- `,, ..bla` is
   the note '..bla'. */
MetaEntryInline     ::= MetaTripleInline
                      | MetaTextInline

MetaTripleInline    ::= DoubleDot MetaRelation DoubleDot MetaObjectInline
                      | QuadDot MetaObjectInline

/* Both end at the next ';;' or at the newline. The two productions are the
   same shape and differ only in the role they carry. */
MetaObjectInline    ::= EntryText
MetaTextInline      ::= EntryText


/* --- meta, block form: entries separated by NL ---------------------------- */

/* Pairs and text lines may be mixed freely, in any order. No ';;' is special
   in a block: NL separates the entries, so ';;' is content everywhere. */
MetaBlock           ::= MetaEntryInBlock (NL MetaEntryInBlock)*

MetaEntryInBlock    ::= MetaTripleInBlock
                      | MetaTextBlock

MetaTripleInBlock   ::= DoubleDot MetaRelation DoubleDot MetaObjectInBlock
                      | QuadDot MetaObjectInBlock

MetaObjectInBlock   ::= Text

/* Greedy: ADJACENT text lines are one note, joined by newlines, so a pasted
   paragraph stays one note. A pair line ends the run; the next text line after
   it starts a new note. */
MetaTextBlock       ::= MetaTextBlockLine (NL MetaTextBlockLine)*

/* Any text at all -- MetaEntryInBlock already tried the pair forms, so a
   leading DT2/DT4 that did not form a pair is this line's text, exactly as
   inline. The subtraction is the one restriction: the line may not BEGIN with
   CM2, because a line-initial ',,' closes the block. */
MetaTextBlockLine   ::= Text - (CM2 (WS | Word)*)
Note on the productions
  • Meta text containing ;;? Inside a ,, block, newlines separate the entries, so no ;; is special anywhere in it (case 28-semicolon-in-meta-block). A note or a value that must carry a literal ;; is written there, or in a !!block.

  • Ordered choice in MetaEntryInline and MetaEntryInBlock means a malformed pair is a note, not an error: ,, ..bla has no closing .., so the dotted alternatives fail and the entry is the note ..bla. The text is kept rather than the line destroyed — an unclosed .. is a typo, and a typo in the metadata must not take its triple down with it.

  • 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* CM2 — WS 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. Within one of them, triples and text are not exclusive: both may occur, repeatedly and in any order. Their order is the written one, which is why the event stream is a list and not a map.

  • One MetaTextBlock is one note however many lines it spans; a triple line between two text lines ends the first note and the line after it starts a second.

  • 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).

  • Once meta text has started, a later DT2 is text, not a separator: MetaTextInline’s self-loop carries `DT2 / DT4. Only SC2 leaves the state, and it does so unconditionally — MetaTextInline and MetaObjectInline have the same transitions and differ only in the role they emit.

  • [a MetaTripleInline derives] / [a MetaTripleInBlock derives] is the ordered choice of MetaEntryInline / MetaEntryInBlock: a leading DT2 selects the pair states only if the whole pair is there (a closing DT2 and a non-empty object). Otherwise the same DT2 starts a note — ,, ..bla is the note ..bla, not a dead line. DT4 needs no guard: QuadDot MetaObject cannot fail once the object is non-empty, and an empty one falls through to text like any other.

    This is the automaton’s only lookahead, and it is bounded: an entry ends at the next SC2 or NL, so the guard never has to look past one entry.

  • An entry may be empty. SC2 parts entries and demands nothing of them, so ,, ;; x is an empty entry followed by the note x, and a line ending in ;; is its entries followed by a final empty one. An empty entry carries no metadata and is simply dropped: it is not an error, and a separator is not a promise that something follows it. That is why MetaStart -→ MetaNextInline and MetaNextInline -→ StartOfLine : NL are ordinary transitions here and not failures — the triple and every entry around the empty one stand.

  • Inside a ,,-block, triples and text mix freely, so every line decides for itself and the three old line states (BlockFirstLine, BlockTripleLine, BlockTextLine) are one BlockLine. What distinguishes a note from the note above it is only what lies between them: adjacent MetaTextBlockLine s are one MetaTextBlock, and a triple line ends the run.

  • Two adjacent notes are therefore not writable in a block — the lines fold into one. This is the price of keeping a pasted paragraph intact, and the inline form writes them apart when that is what is meant: ,, one ;; two is two notes.

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

  • A block may be empty: reaching the closing CM2 straight from the first BlockLine 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 it divides the entries about one triple — after an object and inside a note alike, with no condition on what follows it. In the block meta form (part 3) newlines do that job and ;; is content everywhere, which is what makes the block the escape hatch for a note or value that has to carry one (28-semicolon-in-meta-block).

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