Links

You place the nodes; Lini routes the wires. A link is two ids and an operator, and the operator is the whole look.

The operator is the look

a b a b a b a b a b a b a b a b -> <-> --> ---> ~> *-* -<> -
{
  direction: column; gap: 14;
  |row| { gap: 150; }
  |box| { width: 60; padding: 6; font-size: 13; }
  |-| { font-size: 12; }
}

|row| [ |box#a1| "a"; |box#b1| "b"; a1 -> b1 "->" ]
|row| [ |box#a2| "a"; |box#b2| "b"; a2 <-> b2 "<->" ]
|row| [ |box#a3| "a"; |box#b3| "b"; a3 --> b3 "-->" ]
|row| [ |box#a4| "a"; |box#b4| "b"; a4 ---> b4 "--->" ]
|row| [ |box#a5| "a"; |box#b5| "b"; a5 ~> b5 "~>" ]
|row| [ |box#a6| "a"; |box#b6| "b"; a6 *-* b6 "*-*" ]
|row| [ |box#a7| "a"; |box#b7| "b"; a7 -<> b7 "-<>" ]
|row| [ |box#a8| "a"; |box#b8| "b"; a8 - b8 "-" ]

An operator is [start marker][line][end marker], glued together with no spaces. That is the whole grammar:

LineMarker
-solid>an arrow
--dashed*a dot
---dotted<>a diamond
~wavy

So --> is a dashed arrow, and *-* a solid line with a dot at each end. A bare line op leaves both ends plain.

The ER cardinality markers ride the same scheme — Tables & entities shows them.

Labels ride the route

{
  gap: 120;
  |box| { fill: --purple-wash; stroke: --purple-deep; color: --purple-ink; }
}

|box#client| "Client"
|box#server| "Server"
|box#store| "Store"

client -> server "request"
server -> store { along: 0.25, 0.75; } [ "read" "write" ]
store --> client [ "rows" { color: --teal-ink; font-weight: bold; } ]
Client Server Store request read write rows

One label follows the head, and slides along the route to keep clear of nodes.

Several labels, or a styled one, ride the link’s own [ ] — and along: fixes each as a fraction of the route, so 0.25, 0.75 puts them at the quarter and three-quarter marks.

A label cuts its wire where it sits, so it reads over any background.

Chains and fans

{
  gap: 60;
  |box| { fill: --sky-wash; stroke: --sky-deep; color: --sky-ink; }
  |cyl| { fill: --teal-wash; stroke: --teal-deep; color: --teal-ink; }
}

|box#lb| "LB" { fill: --rose-wash; stroke: --rose-deep; color: --rose-ink; }
|column#apps| { gap: 20; } [
  |box#a| "app-1"
  |box#b| "app-2"
  |box#c| "app-3"
]
|cyl#db| "DB"

lb -> apps.a & apps.b & apps.c
apps.a & apps.b & apps.c -> db
LB app-1 app-2 app-3 DB

a -> b -> c is a chain: two links, every hop marked.

& groups endpoints, and which side you group decides the shape:

  • lb -> a & b & c fans out.
  • a & b & c -> db fans in.
  • Both sides grouped is a cartesian product — every left to every right.

A fan shares its trunk wherever the geometry allows, which is what makes the two bundles above read as buses rather than as six separate wires.

Steering: sides, clearance and dotted paths

{ gap: 70; clearance: 20; }

|box#a| "a"
|box#b| "b"
|box#c| "c"

a -> b -> c
a:top -> c:top "skip"
c:bottom -> a:bottom "retry"
a b c skip retry

Without a side the router picks the edges by geometry. a:top forces one.

clearance is the gap a wire keeps from every node and every other wire. Set it on a container and it cascades to the links written inside.

It shapes the drawing as well as spacing it: corners round to fit the clearance, and a parallel run steps aside so two wires never share a track.

Three routing strategies

{
  gap: 40;
  |group| { layout: grid; columns: repeat(2); gap: 44 60; padding: 24; }
  |box| { fill: --amber-wash; stroke: --amber-deep; color: --amber-ink; }
}

|group#o| "orthogonal" [
  |box#o1| "a"; |box#o2| "b"
  |box#o3| "c"; |box#o4| "d"
  o1 -> o4
  o2 -> o3
]
|group#n| "natural" { routing: natural; } [
  |box#n1| "a"; |box#n2| "b"
  |box#n3| "c"; |box#n4| "d"
  n1 -> n4
  n2 -> n3
]
|group#s| "straight" { routing: straight; } [
  |box#s1| "a"; |box#s2| "b"
  |box#s3| "c"; |box#s4| "d"
  s1 -> s4
  s2 -> s3
]
a b c d orthogonal a b c d natural a b c d straight

routing: is scene config, like clearance:

  • orthogonal (the default) — horizontal and vertical legs through the free space between nodes.
  • natural — smooth curves that bend around what they would hit, and are free to cross.
  • straight — one segment between the bodies, avoiding nothing.

A group can route its internals one way while the root routes another, which is what the figure above is doing.

{
  gap: 70;
  |-| { stroke: --gray-deep; stroke-width: 1.5; font-size: 12; }
  .hot { stroke: --red-deep; stroke-width: 2.5; color: --red-ink; }
}

|box#a| "a"
|box#b| "b"
|box#c| "c"

a -> b "cold"
b -> c "hot" .hot
a b c cold hot

A link is styled exactly like a node. stroke, stroke-width and stroke-style dress the wire; color and the font-* family dress its labels.

The cascade is the same one too: |-| { } is the rule for every link, a class applies to whichever nodes or links wear it, and a link’s own { } wins.

There is no separate link-* vocabulary to learn.

Go deeper

Links in the reference covers endpoints, scope and capsule endpoints; the full routing contract — priority, crossings, self-loops — is ROUTING.md.