Skip to content

Dictim Syntax

Jude Payne edited this page Sep 7, 2024 · 29 revisions

Terminology

'dictim syntax' is a data format which is a subset of Clojure's edn data format for specifying a diagram.

'dictim format' a diagram specified in dictim syntax may be specified in either edn or json. They are equivalent.

Overview

The syntax of dictim closely resembles that of d2's text language. It is intended to be easy to read across between the two. dictim is nested format like d2. It also closely resembles that of graphviz' dot language (which is itself similar to d2). In the specification below, it is called out when a particular feature is d2 only. Dictim's primary target is d2, with dot secondary.

If dictim's nested syntax is not what you require, there are two other options:

  • The dictim.graph library allows you to specify a diagram as a graph. The graph ddefinition is then converted to dictim syntax.

  • dictim itself (i.e. this library) has a second syntax, called 'flat dictim' (which is covered at the bottom of this document). Its flat structure makes it easier to manipulate for some use cases, e.g. to break part for storage, than dictim's nested syntax.

Contents

Dictim Syntax Specification

Dictim is a series of dictim elements - each one closely matching in syntax their d2 equivalent. The d2 tour explains the strcutre and semantics of the d2 language.

A dictim element can be one of six principal types: comments, shapes, connections, containers, attributes and lists.

All but attributes are modelled using Clojure vectors - they produce objects in the diagram when dictim is compiled to d2 and converted into an image. Attributes on the other hand are for styling how objects are rendered and are modelled as Clojure maps. Just remember; vectors for objects and maps for styling objects. Every good rule has an exception, and Vars which are d2's variables are modelled in dictim as maps, but may be used to specify both rendering/styling information and data.

Elements

A collection of dictim element is modlled as a Clojure list, (..elements..).

This is the only time that a Clojure list is used in the dictim syntax.

Shapes

d2 docs

The syntax for a shape is:

[<shape-key> <label>(optional) <attribute-map>(optional)]

for example:

[:personA "Person A" {:style {:fill "red"}}]

which is represented in d2 as:

personA: Person A {style {fill "red"}}

The shape-key can be either a string, a number or a keyword. The label should be a string. The attribute map should be a map whose keys are either keywords or strings, and whose values are either strings or numbers.

In dictim, the elements of a shape are laid out in a Clojure vector.

Examples with the optional parts missing:

[:personA "Person A" {:style {:fill "red"}}]
[:personA "Person A"]
[:personA {:style {:fill "red"}}]
[:personA]
["person A" "A person called Mike"]
["person A"]
[:people.personA]

are all valid shapes. The last one denotes that 'personA' is inside a container called people

d2 offers a shortcut for creating a shape and styling one aspect of it at the same time.

for example:

aShape.style.fill: red

which when parsed into dictim becomes

["aShape" {"style.fill" "red"}]

Strictness of conversion

As a text format, d2 is quite free-form and occasionally offers more than one way to express the same thing. That means that when you round-trip dictim to d2 and back again (by compiling to d2, then parsing to dictim) you might not get the same as the original, even though semantically it's the same.

For example, let's compile a dictim shape to d2 and then parse it back

=> (dictim (d2 [:aShape.style.fill "red"]))
(["aShape" {"style.fill" "red"}])

Note how the attributes are put into their own map on the way back.

Clojure keywords are converted to strings for d2 and not automatically converted back again, although you can control this behaviour by passing a key-fn to the dictim function that handles parsing.

Multi-line labels

For shapes (and connections and containers), if you wish to have a multi-line label, use \\n at the desired point in the label string. \n must be escaped, so always \\n.

Connections

d2 docs

There are two types of connections; single connections and multiple connections.

Single Connections

Syntax:

[<key1> <direction> <key2> <label>(optional) <attribute-map>(optional)]

for example:

[:personA "--" :personB "brothers" {:style.stroke-width 2}]

which is represented in d2 as:

personA -- personB: brothers {style.stroke-width: 2}

key1 and key2 must each be either the key of a shape or a container direction must be one of "<>" "--" "<-" "->"

Multiple Connections

Multiple connections are expressed all in one go..

Syntax:

[<key1> <dir1> <key2> <dir2> <key2> ... <keyN>  <label>(optional) <attribute-map>(optional)]

for example:

[:a "<-" :b "->" :c "children" {:style.stroke-width 2}]

which is represented in d2 as:

a <- b -> c: children {style.stroke-width: 2}

Multiple connections have to share the same label and attributes if those are specified.

Referencing connections

[!Note] This part of the dictim syntax applies to d2 only

Connection references are a way of referring to a connection that you've already specified, either to style it or null it out.

for example:

(x -> y)[0].style.stroke: red

in d2 becomes

[:x "->" :y [0] {:style.stroke "red"}]

where the index 0 would refer to the first occurence of the connection x -> y in the d2 text.

An index of a connection reference can also be a glob * when you wish to refer to all connections.

You can null out a connection reference by

[:x "->" :y [0] nil]

will not be displayed.

Containers

d2 docs

The syntax for containers is the same as for shapes, except that at the end you put the set of contained elements, which may be of any number and each of any of the six types.

Syntax:

[<ctr-key> <label>(optional) <attribute-map>(optional) <nested elem1> <nested elem2> ... <nested elemN>]]

for example:

[:family1 "The Jones'" {:style {:fill "red"}}
  [:personA "Henrick"]
  [:personB "Michael"]
  [:personA "--" :personB "brothers"]]

which in d2 is:

family1: The Jones' {
  style:  {
    fill: red
  }
  personA: Henrick
  personB: Michael
  personA -- personB: brothers
}

Containers may nest other containers which may nest ... and so on.

Special Objects

[!Note] This part of the dictim syntax is valid in dot, but has no special meaning

d2 docs

Special objects, e.g.:

  • Text & Code
  • Icons & Images
  • SQL Tables
  • UML classes
  • Sequence diagrams
  • Grid diagrams

are all supported via the dictim equivalent of the relevant d2 syntax.

Attributes

d2 docs

We've seen attibutes already; attribute maps can belong to shapes, connections and containers to specify d2 rendering instructions.

They can also appear at the top level, i.e. an attribute map can exist as a top level element.

An example is the :direction/ direction: reserved keyword, e.g.

{:direction "right"}

which compiles to d2:

direction: right

note the lack of brackets in d2 for top level attributes, i.e. those occurring at diagram level.

hex colors

Note that because d2 reserves the special character # to indicate the start of a comment, hex colors must be either single or double-quoted. e.g.

{:style.fill "#bfe6a5"}

won't work. Instead do:

{:style.fill "'#bfe6a5'"}

which could be equivalently expressed as a nested version:

{:style {
          :fill "'#bfe6a5'"
	}
}

d2's Classes and Vars are also modelled as special maps which resemble an attribute map.

Please note that in dictim, attributes that exist as standalone elements at the top level of a set of elements are sometimes called 'directives'.

Classes

[!Note] This part of the dictim syntax is valid in dot, but has no special meaning

d2 docs

classes is a reserved word in d2 and in dictim.

This d2 class definition:

classes: {
  uno: {
    label: load balancer
    width: 100
    height: 200
    style: {
      stroke-width: 0
      fill: '#44C7B1'
    }
  }
  dos: {
    label: dos
  }
}

which be represented in dictim as

{"classes"
  {"uno"
   {"label" "load balancer",
    "width" 100,
    "height" 200,
    "style" {"stroke-width" 0, "fill" "'#44C7B1'"}}}}

As per d2, a class definition is later used on a shape, container or edge, for example in dictim.

{"x.class" [:list "uno" "dos"]}

Vars

[!Note] This part of the dictim syntax is valid in dot, but has no special meaning

d2 docs

vars is a reserved word in d2 and in dictim.

This d2 var definition:

vars: {
  server-name: Cat
}

is equivalent to the dictim

{"vars" {"server-name" "Cat"}}

Substitutions in dictim are the same as in d2. For example, substituting the above var into a shape's label

server2: ${server-name}-2

Comments

In d2 comment lines are started with the character #.

Dictim mirrors this; a comment is a string that starts with the character #;

"# Hans, accused of cheating"

In d2:

"# Hans, accused of cheating"

Lists and Ordering

Lists are a second type of container, for a sequence of elements that should all be put on one line separated by semicolons:

[:list [:a-shape "A shape"] [:b-shape "B Shape"]]

In d2:

"a-shape: A shape; b-shape: B shape"

Lists may not nest other lists.

In d2, putting elements on one line doesn't have a special meaning, except inside sequence_diagrams where it denotes the order of the actors - hence the need for [:list <sequence-of-shapes-or-ctrs>] in dictim. Note that only shapes or containers may be put into a list in dictim.

In d2, this is a top level list. i.e. it occupies its own line in a d2 text file.

There are inner lists as well in d2, which are surrounded by square brackets.

We've seen an example of an inner-list already, with this shape, x, that participates in two styling classes

{"x.class" [:list "uno" "dos"]}

[!Note] Inner lists are not allowed in dot

Empty Lines

The goal of this project is to allow for both ways faithful conversion between clojure data structures (e.g. 'dictim') and d2. In d2, to improve readability, you'll often leave empty lines to separate different parts of the file.

Empty lines are parsed into dictim as:

[:empty-lines 1]

where the '1' indicates the number of empty lines at this point.

Whether empty-lines are captured during parsing is an option (defaulted to not). Please see the parsing page for details.

Reserved d2 keywords

Something to watch out for!

d2 has a number of reserved keywords for use only in attributes which are worth avoiding when you name the keys in shapes and containers.

The list of reserved keywords:

'shape' 'label' 'source-arrowhead' 'target-arrowhead' 'near' 'icon' 'width' 'height' 'constraint' 'direction' 'tooltip' 'style' 'class' 'grid-rows' 'grid-columns' 'link'

and sub-keys of style....

'opacity' 'fill' 'stroke' 'stroke-width' 'stroke-dash' 'border-radius' 'font-color' 'shadow' 'multiple' '3d' 'animated' 'link' 'filled' 'fill-pattern' 'underline' 'double-border' 'bold' 'italic' 'animated' 'text-transform'

For details of what these all do, please see the d2 [tour] (https://d2lang.com/tour/intro) of the d2 language.

and don't forget the dictim reserved keywords:

:list and :empty-lines

keywords cannot be used in your shapes, connections and containers.

For dot, Dictim does not currently check against Graphviz' reserved attribute keywords during compilation.

Types

Dictim is quite flexible with types. Element keys can be keywords or strings. In attr maps, the values can be integers, floats , booleans or strings. Non-string types will be converted to strings during compilation. Values in attr maps will be converted back to the primitive type during parsing, and there are the optional key-fn and label-fn functions in parsing that can be used to convert the type or format of keys and labels.

In Depth

Globs

[!Note] Globs/asterisks and ampersands are not valid syntax in shape, container, connection keys in the dot language. In labels, they are fine.

d2 documentation

In d2, a wildcard character '*' is called a glob.

globs may be used in two different positions in d2 and dictim.

In attributes, when trying to apply a style to more than one object. For example in the dictim

[:aShape] [:bShape] {:*Shape.style.fill "red"}

would cause both shapes to have a red fill.

The & character can be used to filter where the style is applied as per the d2 docs.

Secondly, in connection keys to indicate multiple connections. The dictim elements

["zone-A"
  ["machine A"]
  ["machine B" ["submachine A"] ["submachine B"]]]
["zone-A.**" "->" "load balancer"]

indicates that there should be connections between all three shapes in zone-A and load balancer.

Imports

[!Note] This d2 symbol to denote an import '@' is not permitted in dot as a shape/ container/ connection key.

d2 documentation

Imports in d2 are represented by the @ symbol which is compiled and parsed by dictim.

Layers, scenarios and steps

Layers, scenarios and steps are modelled in dictim as normal containers whose key is either layers, scenarios or steps

Gotchas

d2 is a text format so can be a bit looser in it's syntax than a data format. There are a small number of gotchas to be aware of.

Composites: Shape or attribute?

d2 allows for a shape and one styling attribute to be specified all in one go:

aShape.style.fill: red

Is this a shape or an attribute? Dictim decides that since this line of d2 would result in a shape being displayed in the resultant diagram, its purpose is to be a shape, therefore the dictim equivalent is:

["aShape" {"style.fill" "red"}]

However the d2

*Shape.style.fill: red

would not produce a shape in the diagram. It's purpose is to style and the dictim equivalent is:

{"*Shape.style.fill" "red"}

i.e. it's an attribute.

dictim syntax in json format

dictim is made from plain Clojure data structures; vectors and maps which have their json equivalents as arrays and objects. Edn dictim and Json dictim therefore look very close and have the same syntax; only the format is different.

For example, the dictim to describe a shape and a container:

[:family1 "The Jones'" {:style {:fill "red"}}
  [:personA "Henrick"]
  [:personB "Michael"]
  [:personA "--" :personB "brothers"]]

in json becomes

[
  [
    "aShape"
  ]
  [
    "family1",
    "The Jones'",
    {
      "style": {
	"fill": "red"
      }
    },
    [
      "personA",
      "Henrick"
    ],
    [
      "personB",
      "Michael"
    ],
    [
      "personA",
      "--",
      "personB",
      "brothers"
    ]
  ]
]

The Utilities page details how to convert to/ from json.

Flat Dictim Syntax Specification

In flat dictim, every element is represented by a Clojure map with the same form:

{:type <element type i.e. :shape/:ctr/:conn/:cmt/:attrs>
 :key <key for the element>
 :meta <a map of all other meta data about the element, e.g. it's label and attrs>
 :parent <the key of the parent element (lists and containers)>
 .. <any other keys you'd like to add!>}

The flat, homogenous nature of the syntax makes it easier to manipulate than dictim.

The dicitim.flat namespace has utility functions for flattening and (re)building dictim to\from flat dictim.

:key is a key generated for the element. For a shape or container, it is its key. For a connection, the connection information as a vector e.g. [:a "->" :b], for a comment, the comment string itself, for an attribute map, the map itself and for a list a sequence of the contained elements' keys.

:meta is all the other information needed in the 'dictim format (as opposed to 'flat dictim') element. It's a map with up to two keys; :label and :attrs

:parent is created during the flattening process (dictim -> flat dictim) and use in the rebuilding process.

Please see the Uitilties page for more information about working with flat dictim.