reladraw

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

reladraw

reladraw

A text language for diagrams. Source in, standalone SVG out, no runtime dependencies.
Every other diagram language hands the arrangement to a layout engine, so where things land is an output you cannot predict from what you wrote. reladraw inverts that: the file states the arrangement in the terms a person would say out loud —
right of api
,
between web and worker
,
level with queue
— and the tool works out only the distances. Nothing is ever chosen for you.
That matters here specifically. You cannot see the SVG you produced. With auto-layout that leaves you guessing; with reladraw the arrangement is in the sentences you just wrote, so re-reading your own source tells you where everything is.
用于图表的文本语言。输入源代码,输出独立 SVG,无运行时依赖。
大多数其他图表语言都会把布局交给布局引擎,因此元素最终落在哪里,是你无法仅根据所写内容预测的输出。reladraw 反其道而行:文件使用人们口头表达位置时会使用的语句来描述布局——
right of api
、
between web and worker
、
level with queue
——工具只负责计算距离。绝不会替你选择任何布局。
这一点在这里尤其重要。你无法看到自己生成的 SVG。使用自动布局时,这会让你只能猜测;使用 reladraw 时,布局就写在你刚写下的语句里,因此重新阅读自己的源代码就能知道所有元素的位置。

Check it is installed

检查是否已安装

reladraw --help
If that fails,
npx reladraw
works without installing, and
npm install -g reladraw
installs the command.
reladraw --help
如果失败,
npx reladraw
可在无需安装的情况下运行,
npm install -g reladraw
则会安装该命令。

The loop

工作流程

  1. Write a
    .reladraw
    file.
  2. reladraw diagram.reladraw -o diagram.svg
    (
    -o -
    writes to stdout; the default output path is the input with
    .svg
    in place of its extension).
  3. If it fails, the error names the statement and what is wrong with it. Fix and re-run.
  4. Re-read your own source to confirm the picture says what you meant.
Errors are the feedback you have. A file that renders is a file whose arrangement you fully stated, because anything left ambiguous is refused rather than guessed:
diagram.reladraw:3: "c" does not say where it sits vertically: "right of a" and "left of b" would put it in different places
Be honest about what this does not give you. Re-reading the source confirms intent — that the worker landed under the API, that all four machines hang off the hub. It does not confirm outcome: whether a long text overflowed, whether a line crosses four others, whether two independently-anchored clusters collided. A machine-readable diagnostics report is planned and is not built yet, so for a large or dense diagram, say plainly that you have stated the arrangement but not verified the render.
  1. 编写一个
    .reladraw
    文件。
  2. 运行
    reladraw diagram.reladraw -o diagram.svg
    (
    -o -
    会写入 stdout;默认输出路径会将输入文件的扩展名替换为
    .svg
    )。
  3. 如果执行失败,错误信息会指出具体语句及其问题。修复后重新运行。
  4. 重新阅读自己的源代码,确认图表表达了你原本的意图。
错误就是你所获得的反馈。一个能够渲染的文件,意味着你已经完整陈述了它的布局,因为任何含糊之处都会被拒绝,而不是被猜测:
diagram.reladraw:3: "c" does not say where it sits vertically: "right of a" and "left of b" would put it in different places
要如实说明这不能保证什么。 重新阅读源代码可以确认意图——worker 位于 API 下方,四台机器都连接在 hub 上;但不能确认结果——长文本是否溢出、连线是否穿过另外四条线、两个独立锚定的节点簇是否发生碰撞。机器可读的诊断报告仍在规划中,尚未实现。因此,对于大型或密集的图表,应明确说明:你已经陈述了布局,但没有验证渲染结果。

The one idea

核心理念

A gap is a minimum distance, never an exact one. Everything ends up as close together as your statements allow.
So putting something between two things is what pushes them apart, by exactly what it needs:
node hub    "Hub"
node side   "Side"    left of hub
node wedge  "Wedged"  right of side  left of hub  level with hub
Nothing says how far apart
hub
and
side
are. Delete
wedge
and they close back up. You never pick a number and no number goes stale when a text grows. There are no coordinates in this language, in any form.
间距是最小距离,绝不是精确距离。 一切都会按照你的陈述允许的方式尽量靠近。
因此,把某个元素放在两个元素之间,会将它们推开,所需距离恰好足够:
node hub    "Hub"
node side   "Side"    left of hub
node wedge  "Wedged"  right of side  left of hub  level with hub
没有任何语句规定
hub
和
side
相距多远。删除
wedge
后,它们会重新靠拢。你永远不需要选择具体数值,文本增长时也不会有过期数值。这个语言中没有任何形式的坐标。

Writing a file

编写文件

One statement per line. No blocks, no continuations, no significant indentation.
//
starts a comment and may trail a statement.
A statement is a positional head — the keyword, a name, then a text — followed by attributes and placements in any order. A token ending in a colon opens an attribute and nothing else does, so
node n "text" gap: wide below worker
and
node n "text" below worker gap: wide
are the same statement.
一条语句占一行。没有块结构、没有续行,缩进也没有特殊意义。
//
表示注释,可以跟在语句末尾。
一条语句由位置头开始——依次是关键字、名称和文本——后面可以按任意顺序跟属性和定位。以冒号结尾的词元会开启一个属性,并且只有这种词元才会如此,因此
node n "text" gap: wide below worker
与
node n "text" below worker gap: wide
是同一条语句。

Nodes

节点

node <name> ["<text>" [(<text properties>)]] [<placement> | <attribute>] ...
Leave the text out and the node takes its own name as its text (
node parser
draws a node reading "parser"). Write
""
for a deliberately blank node. Inside text,
/
— a slash with a space on each side — is a line break; a slash without spaces is an ordinary character, so
TCP/IP
and URLs survive.
Containment is a dotted name and the parent must be declared first. Children stack vertically in written order unless a child carries its own placement.
node server   "Server"
node server.api    "API"
node server.worker "Worker"
A container with
""
and
fill: none  border: none
draws nothing and takes no room of its own, which is how you make a group that can be placed against as one shape.
Node attributes:
style
,
fill
and
border
(each a color),
shape
,
icon
,
badge
,
gap
,
overlap: allow
,
url
(a quoted destination), and
contents:
on a container.
contents: (widths: match, align: center)
says how a container's children sit when its title is wider than they are.
widths:
takes
natural
,
match
(all as wide as the widest) or
fill
(all as wide as the band);
align:
takes
left
,
center
or
right
.
node <name> ["<text>" [(<text properties>)]] [<placement> | <attribute>] ...
省略文本时,节点会使用自己的名称作为文本(
node parser
会绘制一个显示 “parser” 的节点)。写入
""
可以有意创建一个空白节点。在文本中,
/
——即两侧各有一个空格的斜杠——表示换行;不带空格的斜杠只是普通字符,因此
TCP/IP
和 URL 可以原样保留。
包含关系使用点号名称,并且父节点必须先声明。除非子节点带有自己的定位,否则子节点会按书写顺序垂直堆叠。
node server   "Server"
node server.api    "API"
node server.worker "Worker"
一个同时带有
""
和
fill: none  border: none
的容器不会绘制任何内容,也不占用自身空间;你可以用这种方式创建一个可作为整体形状参与定位的组。
节点属性包括:
style
、
fill
和
border
(均为颜色)、
shape
、
icon
、
badge
、
gap
、
overlap: allow
、
url
(带引号的目标地址),以及容器上的
contents:
。
contents: (widths: match, align: center)
用于说明当容器标题比其子项更宽时,子项应如何排列。
widths:
可取
natural
、
match
(所有子项与最宽者同宽)或
fill
(所有子项与该区域同宽);
align:
可取
left
、
center
或
right
。

The text and its brackets

文本及其括号

Everything a text says about itself goes in brackets after it, never among the node's attributes:
color
,
size
(
small | normal | large
),
wrap
(fold every n characters),
align
(
left | center | right
, how the lines range against each other) and
at
(where the block sits, named from the nine positions —
top-left
,
bottom-center
,
center
and the rest).
node docker "Docker" (at: bottom-center, align: center)  below deploy
node aside  "a longer remark that folds" (size: small, wrap: 30)  shape: none
edge a -> b "rclone" (color: muted)
An edge's text takes the same keys less
at
. A style has no text of its own, so it hangs the bracket off a key:
style aside  text: (size: small, color: muted)
.
A stretch of a text can borrow a style's text color, which is how a node carries a quieter qualifier:
style dim  text: (color: muted)
node grinder "Grinder / [dim]medium-fine[/dim]"
The mark names a style and never a color; the closer repeats the name;
\[
is a literal bracket. There is no
subtext
attribute — it was removed, and an older file carrying it gets an error naming the mark to write instead.
文本对自身的所有设置都应写在后面的括号中,绝不能放在节点属性之间:
color
、
size
(
small | normal | large
)、
wrap
(每 n 个字符折行)、
align
(
left | center | right
,决定各行彼此之间的对齐方式)以及
at
(文本块所在位置,以九个位置命名——
top-left
、
bottom-center
、
center
等)。
node docker "Docker" (at: bottom-center, align: center)  below deploy
node aside  "a longer remark that folds" (size: small, wrap: 30)  shape: none
edge a -> b "rclone" (color: muted)
边的文本使用相同的键,但不包括
at
。style 本身没有文本,因此要把括号挂在某个键后面:
style aside  text: (size: small, color: muted)
。
文本中的一段文字可以借用某个 style 的文本颜色,因此节点可以附加一个更低调的限定说明:
style dim  text: (color: muted)
node grinder "Grinder / [dim]medium-fine[/dim]"
标记引用的是 style 名称,绝不是颜色;闭合标记会重复该名称;
\[
表示字面量方括号。不存在
subtext
属性——它已被移除;如果旧文件中仍包含它,工具会报错并指出应改用哪个标记。

The body

主体

Every node has one body and two keys can name it.
shape: rectangle | document | none
is the outline it is drawn with —
rectangle
is the default and
none
is text with no box at all, which is what an annotation is.
icon: <name>
draws the node as a picture, with no box:
disk
,
desktop
,
laptop
,
package
,
cubes
,
cube
,
database
. Writing both is an error.
badge: <name>
is different again: it puts one of those pictures beside a node's text, and the node keeps its own body and grows to hold both.
node dump  "nightly dump"  shape: document
node aside "a remark" (wrap: 30)  shape: none
node unit  icon: cube
node drive "External HD"   badge: disk
A node drawn as a picture and given no text shows none — everywhere else a node with no text takes its name, but a picture usually is the statement.
A color attribute names the part it colors:
fill
is the area,
border
the outline,
text
the text, and
line
the drawn line of an edge. A word is refused on a kind that has no such part, so
border:
on a
shape: none
node is an error — with no body there is no outline, and
text:
is its only color.
A style contributes a part only to the kinds that have it, so a style shared between nodes and edges writes one key for each —
style backup  border: #d2904e  line: #d2904e
colors the nodes' borders and the edges' lines from one name.
There is no
stroke
attribute. It was removed because it named no part; if you have seen it in an older file, it is
border
on a node,
text
on one with no body or a picture body, and
line
on an edge.
Every attribute is checked by name, so do not invent one. A word the tool does not know is an error, and so is a real word on a kind that has no use for it —
fill:
on a node with no body,
gap:
or
overlap:
on an edge,
contents:
on a node that can have no children. The error says either what the kind takes or where the word does belong. A key handed over by a style is exempt, which is what lets one style dress both nodes and edges.
每个节点都有一种主体形式,可由两个键指定。
shape: rectangle | document | none
指定绘制节点时使用的轮廓——
rectangle
是默认值,
none
表示只有文本而没有外框,适合用于注释。
icon: <name>
则会将节点绘制成图片,不带外框:
disk
、
desktop
、
laptop
、
package
、
cubes
、
cube
、
database
。同时写入两者会报错。
badge: <name>
的含义又不同:它会将上述某种图片放在节点文本旁边,同时保留节点原有的主体,并扩展空间以容纳两者。
node dump  "nightly dump"  shape: document
node aside "a remark" (wrap: 30)  shape: none
node unit  icon: cube
node drive "External HD"   badge: disk
以图片形式绘制且未提供文本的节点不会显示任何内容;其他情况下,无文本节点会使用其名称,而图片通常已经完整表达了节点含义。
颜色属性会指明所着色的部分:
fill
是区域,
border
是轮廓,
text
是文本,
line
是边所绘制的线条。如果在不具备相应部分的对象上使用某个词,就会报错——例如对
shape: none
节点使用
border:
。因为该节点没有主体,也就没有轮廓,
text:
才是它唯一可用的颜色。
样式只会为具备相应部分的对象贡献属性,因此同时用于节点和边的样式需要为每种对象各写一个键——
style backup  border: #d2904e  line: #d2904e
可通过同一个名称同时设置节点的边框颜色和边的线条颜色。
不存在
stroke
属性。它被移除,因为它没有指明要着色的部分;如果在旧文件中见过它,应在普通节点上改用
border
,在无主体或图片主体节点上改用
text
,在边上改用
line
。
所有属性都会按名称校验,因此不要自行编造。 工具不认识的词会报错;某个实际存在的词若用在不具备该用途的对象上也会报错——例如对无主体节点使用
fill:
、对边使用
gap:
或
overlap:
、对不能有子项的节点使用
contents:
。错误信息会指出该对象支持哪些键,或指出这个键应放在哪里。由 style 提供的键不受此限制,因此同一个 style 可以同时装饰节点和边。

Placement

定位

above X          below X          left of X        right of X
above-left of X  above-right of X below-left of X  below-right of X
level with X     top level with X    bottom level with X
                 left level with X   right level with X
of
is optional after a direction. A node carries as many placements as it needs, and any of them may name several targets joined by
and
—
right of borg and bare
places against the box that just bounds them.
Three rules that decide most of what you write:
  • A lone directional placement binds both axes.
    right of docker
    also centers the node vertically on Docker, because walking right from something keeps you on its center line. That half drops away as soon as another placement claims that axis.
  • Two placements on one axis with nothing on the other is an error, because the tool would have to choose which row the node shares. Add
    level with X
    or a
    below
    .
  • Exactly one node in the file may be left unplaced. Everything else is positioned, directly or transitively, against it.
Gaps are named —
none
,
tight
,
normal
(default),
wide
— and belong to the placement, in brackets:
node stack "Stack"  below wedge  right of wall (gap: tight)  gap: wide
gap:
as a plain attribute is the node's default and reaches every relationship the node is in, including placements written against it.
(gap: none)
puts two sides flat against each other.
above X          below X          left of X        right of X
above-left of X  above-right of X below-left of X  below-right of X
level with X     top level with X    bottom level with X
                 left level with X   right level with X
方向之后可以省略
of
。一个节点可以有任意多个定位语句,其中任意一条都可以通过
and
联合命名多个目标——
right of borg and bare
会相对于恰好包围它们的盒子进行定位。
以下三条规则可以决定你编写的大部分内容:
  • 单独的方向定位会同时约束两个轴。
    right of docker
    也会让节点在垂直方向上与 Docker 居中对齐,因为从某物向右走时仍会位于它的中心线上。当另一个定位语句占用该轴时,这半个约束就会消失。
  • 同一轴上有两个定位语句,而另一轴没有任何定位语句,会报错,因为工具必须决定节点与哪一行对齐。请添加
    level with X
    或
    below
    。
  • 文件中最多只能有一个节点未定位。 其余节点都必须直接或间接地相对于它定位。
间距通过名称指定——
none
、
tight
、
normal
(默认)、
wide
——归属于定位语句,并写在括号中:
node stack "Stack"  below wedge  right of wall (gap: tight)  gap: wide
gap:
作为普通属性时,是节点的默认间距,会作用于该节点参与的每一种关系,包括写在针对该节点的定位中。
(gap: none)
会让两个侧面紧贴在一起。

Edges

边

edge <from> -> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
<-
and
<->
also work;
a <- b
is exactly
b -> a
drawn the same way, and lets you write the subject first. Endpoints may be nested (
server.api
).
from:
and
to:
name a side —
top
,
bottom
,
left
,
right
— and turn the line into a curve that actually leaves and arrives that way. Name them whenever the straight center-to-center line would cut through something.
between a and b
says the line travels down the gap between two named nodes. Use it instead of hoping: the tool will not route around an obstacle by itself, on purpose.
An edge with text widens the corridor between its own two ends by what the text needs, so texts are safe to add.
edge <from> -> <to> ["<text>"] [between <a> and <b> [vertically|horizontally]] [attributes]
<-
和
<->
同样适用;
a <- b
与
b -> a
的绘制结果完全相同,只是允许先写主体。端点可以是嵌套名称(
server.api
)。
from:
和
to:
用于指定边离开或到达的一侧——
top
、
bottom
、
left
、
right
——并会将线条变成真正从该侧离开、从该侧到达的曲线。如果从中心到中心的直线会穿过其他内容,就应明确指定它们。
between a and b
表示连线穿过两个命名节点之间的空隙。应明确使用它,而不要指望工具自动绕开障碍物;这是有意为之。
带文本的边会按文本所需宽度拓宽其两端之间的通道,因此添加文本是安全的。

Annotations

注释

node <name> "<text>" (wrap: 30)  shape: none  <placement> ...
There is no
note
statement — an annotation is a node with no body, anchored to a node so it travels with it. Always give one a
(wrap: n)
— without one a sentence is drawn as one very long line across whatever is beside it.
node <name> "<text>" (wrap: 30)  shape: none  <placement> ...
没有
note
语句——注释就是一个无主体节点,并锚定到某个节点上,以便随其移动。务必为它提供
(wrap: n)
——否则一句话会被绘制成一条很长的横线,横穿旁边的内容。

On a box

位于框上

node <name> "<text>"  on <node> at <position>
Holds a node on another's box at one of nine named points —
top-left
,
top-center
,
top-right
,
left-center
,
center
,
right-center
,
bottom-left
,
bottom-center
,
bottom-right
— inset from that corner or edge and overlapping it on purpose.
(gap: none)
puts it hard against the edge.
This is not containment: a dotted name puts something inside a box and widens it, an overlay is stamped on it and changes nothing. Use it for a mark, a count, or a link line at the bottom of a box.
node <name> "<text>"  on <node> at <position>
可以将节点固定在另一个节点的框上九个命名位置之一——
top-left
、
top-center
、
top-right
、
left-center
、
center
、
right-center
、
bottom-left
、
bottom-center
、
bottom-right
——位置会从相应角或边向内缩进,并有意与目标框重叠。
(gap: none)
会使其紧贴边缘。
这并不是包含关系:点号名称表示把某物放在框内并使其撑大;叠放层则盖在框上,不改变框。可以用它添加标记、计数,或在框底部添加链接线。

Styles

样式

style store  fill: #142814  border: #486544  badge: database
node records "Records"  style: store
Colors are written directly — any hex or CSS color, or
none
. There is no list of color words the tool knows.
style store  fill: #142814  border: #486544  badge: database
node records "Records"  style: store
颜色直接写入——可以是任意十六进制值或 CSS 颜色,也可以是
none
。工具没有预置颜色词列表。

A complete small file

一个完整的小型文件

// A request path, left to right.
style store  fill: #142814  border: #486544  badge: database

node browser  "Browser"
node api      "API server"  right of browser
node db       "Postgres"    right of api    style: store
node worker   "Worker"      below api

edge browser -> api  "HTTP"    from: right  to: left
edge api -> db       "SQL"     from: right  to: left
edge worker -> db    "writes"  from: right  to: bottom

node aside "The worker shares the database / but takes no HTTP traffic." (wrap: 30)  shape: none  below worker (gap: tight)
// A request path, left to right.
style store  fill: #142814  border: #486544  badge: database

node browser  "Browser"
node api      "API server"  right of browser
node db       "Postgres"    right of api    style: store
node worker   "Worker"      below api

edge browser -> api  "HTTP"    from: right  to: left
edge api -> db       "SQL"     from: right  to: left
edge worker -> db    "writes"  from: right  to: bottom

node aside "The worker shares the database / but takes no HTTP traffic." (wrap: 30)  shape: none  below worker (gap: tight)

What will bite you

容易踩的坑

  • Reaching for
    note
    ,
    box
    or
    link
    .
    They are not statements. A note is
    node … shape: none
    ; the keywords are
    node
    and
    edge
    . Each gets an error naming the replacement.
  • Nodes that nothing orders. Every pair of nodes must clear the other, and where the file says nothing about which side of what, it is an error naming the pair:
    "b" and "c" overlap, and nothing says which side of the other either one sits on
    . Hanging two children off the same side of the same target is the usual cause. Place one against the other.
  • An annotation with no wrap.
  • Reaching for a coordinate, an offset, or a waypoint. None exist. If a line goes somewhere wrong, say more about it with
    between
    and
    from:
    /
    to:
    ; if a node is in the wrong place, add a placement.
  • #
    is not a comment.
    It opens a hex color. Comments are
    //
    .
  • Guessing at syntax from another language. There are no braces, no semicolons, no
    -->
    , no subgraphs. If you want something not written here, check
    reference/syntax.md
    before inventing it.
  • 误用
    note
    、
    box
    或
    link
    。
    它们不是语句。注释应写成
    node … shape: none
    ;可用关键字只有
    node
    和
    edge
    。使用错误写法时,工具会报错并指出替代写法。
  • 节点之间没有任何顺序关系。 每一对节点都必须能够相互错开;当文件未说明二者谁位于谁哪一侧时,会报错并指出相关节点对:
    "b" and "c" overlap, and nothing says which side of the other either one sits on
    。将两个子项挂在同一目标的同一侧是常见原因。应让其中一个相对于另一个定位。
  • 注释没有 wrap。
  • 试图使用坐标、偏移量或路径点。 这些概念都不存在。如果连线位置不对,请使用
    between
    和
    from:
    /
    to:
    进一步描述;如果节点位置不对,请添加定位语句。
  • #
    不是注释符。
    它用于开始十六进制颜色。注释符是
    //
    。
  • 根据其他语言猜测语法。 这里没有花括号、分号、
    -->
    或子图。如果想使用本文未涵盖的语法,请先查看
    reference/syntax.md
    ,不要自行编造。

Reference

参考

reference/syntax.md is the full syntax reference — every construct, the shape, icon and position sets, how edges sharing a side or a channel are ordered, what the language deliberately refuses and why, and the known defects. Read it when you need a construct this page does not cover, or when an error message points at behavior you did not expect.
reference/syntax.md 是完整的语法参考——涵盖所有构造、形状、图标和位置集合、共享同一侧或同一通道的边如何排序、该语言有意拒绝哪些行为及其原因,以及已知缺陷。当需要本文未覆盖的语法,或错误信息指向了意外行为时,请查阅该文档。