reladraw 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/reladraw/SKILL.md +27 -6
- package/.claude/skills/reladraw/reference/syntax.md +40 -4
- package/README.md +3 -1
- package/SYNTAX.md +40 -4
- package/dist/ast.d.ts +6 -0
- package/dist/cli.js +12 -6
- package/dist/format.js +67 -20
- package/dist/icons.d.ts +11 -1
- package/dist/links.d.ts +31 -0
- package/dist/links.js +34 -0
- package/dist/model.d.ts +3 -1
- package/dist/page.d.ts +8 -5
- package/dist/page.js +411 -49
- package/dist/parser.js +159 -10
- package/dist/render.d.ts +13 -0
- package/dist/render.js +57 -8
- package/dist/resolve.js +22 -9
- package/package.json +1 -1
- package/dist/search.d.ts +0 -104
- package/dist/search.js +0 -490
|
@@ -15,7 +15,7 @@ That matters here specifically. You can't see the SVG you produced. With auto-la
|
|
|
15
15
|
|
|
16
16
|
## Check it is installed, and the same version as this skill
|
|
17
17
|
|
|
18
|
-
This skill teaches reladraw 0.
|
|
18
|
+
This skill teaches reladraw 0.23.0. Once per session, before the first render, run:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
21
|
reladraw --version
|
|
@@ -29,8 +29,8 @@ If `reladraw` isn't installed, `npx reladraw` works without installing, and `npm
|
|
|
29
29
|
|
|
30
30
|
## The loop
|
|
31
31
|
|
|
32
|
-
1. Write a `.reladraw` file.
|
|
33
|
-
2. `reladraw
|
|
32
|
+
1. Write a `.reladraw` file. Where it goes depends on what it is for. A diagram that belongs to the project, one the user asked to keep or that a README or document will show, goes where they say, or beside what it describes. A diagram drawn only to show the user something, such as an explanation or a sketch of a change, goes in a folder of its own outside the project, such as `~/.cache/reladraw/` or the system's temporary folder, named after what it shows, so the project isn't littered. Nothing is lost if that folder is cleared: the page in step 7 carries its source and saves it.
|
|
33
|
+
2. `reladraw check diagram.reladraw`. It prints errors and findings and writes nothing.
|
|
34
34
|
3. If it fails, the **error** names the line and what to write instead. Fix and re-run.
|
|
35
35
|
4. If it succeeds, read the **findings** it printed. Each is one line saying what may not read as meant, where, and how much:
|
|
36
36
|
|
|
@@ -39,9 +39,9 @@ diagram.reladraw:12: warning(text-overlap): text "statements" covers the top-lef
|
|
|
39
39
|
diagram.reladraw:9: warning(edge-node-overlap): line api -> db crosses node cache, in and out of its top
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
5. Fix each finding in the source, by placing something differently or saying where a line goes, and re-run until there are none.
|
|
42
|
+
5. Fix each finding in the source, by placing something differently or saying where a line goes, and re-run until there are none.
|
|
43
43
|
6. `reladraw format diagram.reladraw` before handing the file over. It lines statements up in columns and evens out spacing, by the project's `.reladraw-format` if there is one, and changes nothing about what the file draws. A file you wrote then reads like one the user's project already has.
|
|
44
|
-
7. Finish by showing the user the picture. Give the diagram a title with `diagram title: "…"`, which heads the page, then run `reladraw render diagram.reladraw --page`. It writes `diagram.html
|
|
44
|
+
7. Finish by showing the user the picture. Give the diagram a title with `diagram title: "…"`, which heads the page, then run `reladraw render diagram.reladraw --page`. It writes `diagram.html` beside the source, a page in the diagram's own colors that pans, zooms, goes full screen, and saves itself, its source, an SVG or a PNG. For a diagram kept in the project, put the page in the outside folder of step 1 instead, with `-o ~/.cache/reladraw/diagram.html`, so it isn't committed by mistake. Write an SVG only when something will use it, such as a README picture: `reladraw render diagram.reladraw` writes `diagram.svg` beside the source. Open the page in the default browser: `xdg-open diagram.html` on Linux, `open diagram.html` on macOS, `start diagram.html` on Windows. Show the page rather than the bare SVG, and don't build a page of your own around it. Open it once per diagram; after later edits, render the page again and tell the user to reload it. Don't open it when there is no display to open it on, as in a remote or headless session, or when the user has said not to; give them the page's path instead.
|
|
45
45
|
|
|
46
46
|
A file with no errors and no findings has nothing on top of anything. That is still not the same as a good picture: re-read your source to confirm the arrangement says what you meant, and for a large diagram tell the user you have checked the findings but not seen the render.
|
|
47
47
|
|
|
@@ -136,7 +136,7 @@ edge [<name>] <from> -> <to> ["<text>" [(<placement>)]] [attributes]
|
|
|
136
136
|
|
|
137
137
|
```
|
|
138
138
|
node bob "Bob the builder" (inside top) height: 7
|
|
139
|
-
node
|
|
139
|
+
node bob_role "builder" (size: small) inside bob bottom-center shape: none
|
|
140
140
|
node count "3" on bob top-right shape: circle
|
|
141
141
|
```
|
|
142
142
|
|
|
@@ -153,6 +153,27 @@ node records "Records" style: store
|
|
|
153
153
|
|
|
154
154
|
`fill` is a box's area, `border` its outline, `line` an edge's line, and a text's color goes in its bracket. `corners: sharp` squares a box's rounded corners, and `border: (color: theme-primary, thickness: thick)` thickens its outline, the box growing to keep its padding. Prefer theme colors, which read in light and dark themes alike: `theme-primary` and `theme-secondary` are the accents, `-subtle` is an accent softened toward the page (the one to fill a box with), `theme-muted` is quieter text. A hex or CSS color stays fixed in every theme.
|
|
155
155
|
|
|
156
|
+
### Links
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
node docs "Documentation" url: "https://example.com/docs"
|
|
160
|
+
edge api -> db "SQL" url: "https://example.com/schema"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**A link goes on the thing it is about, never in a text of its own.** Give the node a `url:`, or give it to a line's text when the link is about the connection. The whole box, or the line's text, becomes clickable. When asked to add links to a diagram, put `url:` on the nodes and lines already there. Don't add nodes to hold links: no column of link texts beside the diagram, and no caption with an address under each box. Add one only when the user asks to see the addresses written out.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
// Wrong: a new text beside the thing the link belongs to
|
|
167
|
+
node api_link "github.com/acme/api" shape: none right of api
|
|
168
|
+
|
|
169
|
+
// Right: the link is on the thing itself
|
|
170
|
+
node api "API server" url: "https://github.com/acme/api"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The address is never drawn; a reader follows it by clicking. A line with no text can't carry one, and neither can a style. Quote the value, since an unquoted `//` starts a comment.
|
|
174
|
+
|
|
175
|
+
**Offer links; don't add them unasked.** When the things in a diagram have real web addresses, such as the files of a pull request on GitHub, end your reply with one line offering to link each node to its file. Never guess an address, and don't link to files on the local machine.
|
|
176
|
+
|
|
156
177
|
## A complete small file
|
|
157
178
|
|
|
158
179
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.23.0
|
|
2
2
|
|
|
3
3
|
What the language accepts. The changelog at the end lists what changed in each published version; 0.17.0's changes are large, and its entry has a table of every old spelling and what to write instead.
|
|
4
4
|
|
|
@@ -199,6 +199,8 @@ node more icon: crate right of store
|
|
|
199
199
|
- **Its ids and class names are renamed**, so two icons that both call a gradient `a` don't collide.
|
|
200
200
|
- **Script is refused**: a `<script>` element or an `on…` attribute.
|
|
201
201
|
|
|
202
|
+
A letter icon is a few letters in a circle, with `letters:` and, if wanted, its colors: `icon gh letters: "GH" fill: #24292f text: white`. A color left out is the theme's: the circle one of its two accents, picked from the letters so the same letters get the same one, and the letters that accent's own text color. It goes anywhere a picture does, as a node's `icon:`, a `badge:`, or a link's badge.
|
|
203
|
+
|
|
202
204
|
A file (`icon rack "./rack.svg"`) is found relative to the diagram's own file. Only the command line can read one; in the playground or a web page, paste the SVG. A declared name replaces a built-in of the same name, and declaring one name twice is an error. Every picture is drawn into the output as path data, so the SVG stays standalone.
|
|
203
205
|
|
|
204
206
|
### Badges
|
|
@@ -1137,7 +1139,22 @@ node store "Store" right of docs (gap: 12)
|
|
|
1137
1139
|
edge docs -> store "read first" url: "https://example.com/order"
|
|
1138
1140
|
```
|
|
1139
1141
|
|
|
1140
|
-
`url:` makes a thing clickable in a viewer that follows links. The whole thing is the target: a node's box and everything in it, a line
|
|
1142
|
+
`url:` makes a thing clickable in a viewer that follows links. The whole thing is the target: a node's box and everything in it, a line and its text. The value is quoted, since an unquoted `//` starts a comment. Nothing about a destination is visible. A container's destination reaches children that name none. A line with no text may have one; an unseen wider copy of the line takes the click. A style may not carry one.
|
|
1143
|
+
|
|
1144
|
+
A thing can have several links, written one after another, each with a name and a badge in a bracket after it:
|
|
1145
|
+
|
|
1146
|
+
```
|
|
1147
|
+
node api "Orders API"
|
|
1148
|
+
url: "https://app.datadoghq.com/…" (name: "Datadog", icon: "D" (fill: #632ca6, text: white))
|
|
1149
|
+
"https://github.com/…" (name: "GitHub", icon: gh)
|
|
1150
|
+
"https://app.shortcut.com/…" (name: "Shortcut")
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
- **`name:`** is what a reader is shown, in quotes. A link with none shows its site, such as `github.com`.
|
|
1154
|
+
- **`icon:`** is its badge. Quoted, it is letters in a small circle, `icon: "GH"`, whose colors go in a bracket after it, `(fill: #24292f, text: white)`; left out, they are the theme's. Unquoted, it is a picture: a built-in icon, or one declared with `icon <name>` ([Your own icons](#your-own-icons)), which keeps its own colors. With no `icon:`, the badge is the first letter of the name.
|
|
1155
|
+
- **In an SVG**, a click follows the first link, and the browser's tooltip lists them all. **On a page** made with `--page`, a click selects the thing and the panel beside the drawing lists its links; a link is followed from there ([A page for looking at a diagram](#a-page-for-looking-at-a-diagram)).
|
|
1156
|
+
|
|
1157
|
+
The same address twice in one `url:` is refused.
|
|
1141
1158
|
|
|
1142
1159
|
## Errors and findings
|
|
1143
1160
|
|
|
@@ -1196,7 +1213,11 @@ The output is a standalone SVG: every picture is drawn into it as path data, nev
|
|
|
1196
1213
|
reladraw render arch.reladraw --page
|
|
1197
1214
|
```
|
|
1198
1215
|
|
|
1199
|
-
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Drag to pan; scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The
|
|
1216
|
+
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Under the title it says which file it was drawn from and when, such as "drawn from arch.reladraw, 2026-11-05 14:32": the page is a snapshot of the file, and is behind it once the file is edited, until it is rendered again. Drag to pan; scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The Save menu saves the page itself, as it was written rather than at its current zoom; the `.reladraw` source it was drawn from; or the picture as an SVG or a PNG, as `render` would draw it; or copies the SVG. Each is named after the source file: `arch.html`, `arch.reladraw`, `arch.svg`, `arch.png`. In Chrome and Edge it asks where to save; other browsers put the file with their downloads. The findings are listed beside it in a panel that folds away, and clicking one lights up what its line drew. A box's or a line's name shows under the pointer.
|
|
1217
|
+
|
|
1218
|
+
**A click on a box or a line selects it**, and never follows a link. The selection is amber; what feeds it is green and what it feeds is red, the lines dashed and moving the way they run; the rest dims. A line runs from the end without an arrowhead when only one end has one, and otherwise from the end written first. The panel beside the drawing names the selection and lists its links, each with its badge, and the boxes it comes from and goes to; a link opens in a new tab, and a box selects it. One line out lights by default, and the panel's button lights everything upstream and downstream. A box with links underlines its text under the pointer. A click on the background or Escape clears the selection.
|
|
1219
|
+
|
|
1220
|
+
Keys: `+` and `-` zoom, `0` fits, `1` is actual size, `f` is full screen, Escape clears the selection.
|
|
1200
1221
|
|
|
1201
1222
|
### Formatting
|
|
1202
1223
|
|
|
@@ -1240,7 +1261,7 @@ clause-gap: 2
|
|
|
1240
1261
|
|
|
1241
1262
|
An unknown setting, one written twice, or a value it doesn't take is an error naming it.
|
|
1242
1263
|
|
|
1243
|
-
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured
|
|
1264
|
+
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured as it would be printed, lined up with its neighbors, so a short statement pushed past the width by a long neighbor's columns breaks too; its first line still lines up with theirs. A statement that can't break, such as a node's name and text with nothing after them, is left as it is, even when its columns carry it past the width. A break falls only between clauses, never inside one or inside a bracket, so a clause longer than the width stands alone on its line, past it. A statement broken by hand is broken again by these rules, or joined when they leave it short enough; one kept on its lines for a comment among them is left as it is. A comment at the end of a broken statement follows its last line.
|
|
1244
1265
|
|
|
1245
1266
|
```
|
|
1246
1267
|
// wrap-at: 60
|
|
@@ -1317,6 +1338,21 @@ Designed and absent from the code.
|
|
|
1317
1338
|
|
|
1318
1339
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal is refused by name with the replacement quoted, rather than dropped in silence: an older file stops with an error saying what to write instead.
|
|
1319
1340
|
|
|
1341
|
+
**0.23.0**
|
|
1342
|
+
|
|
1343
|
+
- **A box or a line can have several links**, each with a name and a badge: `url: "https://github.com/…" (name: "GitHub", icon: "GH") "https://app.datadoghq.com/…" (name: "Datadog")`. In an SVG a click follows the first and the tooltip lists them all. See [Destinations](#destinations).
|
|
1344
|
+
- **A click on the page selects a box or a line.** The page made with `render --page` lights what feeds it in green and what it feeds in red, and a panel beside the drawing lists its links and the boxes at either end. A click no longer follows a link on the page; the panel does. See [A page for looking at a diagram](#a-page-for-looking-at-a-diagram).
|
|
1345
|
+
- **A line with no text can have a link.** It used to be refused as too thin to click; an unseen wider copy of the line now takes the click.
|
|
1346
|
+
- **A letter icon**: `icon gh letters: "GH" fill: #24292f text: white` is a few letters in a circle, usable anywhere an icon is. See [Your own icons](#your-own-icons).
|
|
1347
|
+
- **The page can save its own source, and itself.** A page made with `render --page` now carries the `.reladraw` file it was drawn from, so whoever is handed the page can get the source back. Its Export menu is now a Save menu: This page, .reladraw source, SVG, PNG and Copy SVG, and in Chrome and Edge each save asks where the file goes. Under its title the page says which file it was drawn from and when, so you can tell whether it is behind the file.
|
|
1348
|
+
- **The agent skill keeps throwaway diagrams out of your project.** A diagram drawn only to show you something now goes in a folder outside the project, such as `~/.cache/reladraw/`, and a project's own diagram gets its page there too. While fixing findings it runs `reladraw check`, so no stray SVG is left beside the source.
|
|
1349
|
+
|
|
1350
|
+
**0.22.1**
|
|
1351
|
+
|
|
1352
|
+
- **`reladraw format` keeps to `wrap-at` once the columns are lined up.** A statement's length used to be measured on its own, before it was lined up with its neighbors, so a short one like `node api "API" layout: column` stayed on one line and was then padded out by a long neighbor's name to nearly twice the width. It is now measured as printed and breaks like any other long statement; `clause-per-line-at` is measured the same way.
|
|
1353
|
+
- **Output sent to another program is no longer cut off.** `reladraw format -` and `reladraw render -o -` stopped after the first 64 KB when their output went into a pipe rather than a file, and still reported success, so an editor formatting a large diagram could replace it with a shortened copy. All of it now comes through.
|
|
1354
|
+
- **The agent skill puts a link on the thing it belongs to.** Asked to add links, an agent would add a new text node for each one; the skill now says to give the existing node or line a `url:`. When a diagram's things have real web addresses, such as a pull request's files, it offers to link them rather than doing it unasked.
|
|
1355
|
+
|
|
1320
1356
|
**0.22.0**
|
|
1321
1357
|
|
|
1322
1358
|
- **`reladraw install-skill`** installs the agent skill that came with your reladraw, so it teaches what your copy accepts rather than what the main branch does. It hands the work to `skills`, passing on `-g`, `-a` and the rest. The npm package now carries the skill.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# reladraw
|
|
2
2
|
|
|
3
|
+
<a href="https://news.ycombinator.com/item?id=49858513"><img src="https://hackergit.org/badge/reladraw/reladraw.svg" alt="Hacker News" height="20"></a>
|
|
4
|
+
|
|
3
5
|
reladraw is a text language for diagrams where you say where things go.
|
|
4
6
|
|
|
5
7
|
> The language is changing frequently. The playground always runs the newest syntax, so a diagram that draws there today may need a small edit later. An installed copy from npm changes only when you upgrade it.
|
|
@@ -90,7 +92,7 @@ To install the skill from the main branch instead, which can run ahead of the re
|
|
|
90
92
|
|
|
91
93
|
## Status
|
|
92
94
|
|
|
93
|
-
Version 0.
|
|
95
|
+
Version 0.23.0. Early stage, but works. The parser, layout engine, and SVG renderer are written in TypeScript, with zero runtime dependencies. There is a command-line tool that turns a .reladraw text file into an SVG, and an element that does the same inside a web page.
|
|
94
96
|
|
|
95
97
|
## License
|
|
96
98
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.23.0
|
|
2
2
|
|
|
3
3
|
What the language accepts. The changelog at the end lists what changed in each published version; 0.17.0's changes are large, and its entry has a table of every old spelling and what to write instead.
|
|
4
4
|
|
|
@@ -199,6 +199,8 @@ node more icon: crate right of store
|
|
|
199
199
|
- **Its ids and class names are renamed**, so two icons that both call a gradient `a` don't collide.
|
|
200
200
|
- **Script is refused**: a `<script>` element or an `on…` attribute.
|
|
201
201
|
|
|
202
|
+
A letter icon is a few letters in a circle, with `letters:` and, if wanted, its colors: `icon gh letters: "GH" fill: #24292f text: white`. A color left out is the theme's: the circle one of its two accents, picked from the letters so the same letters get the same one, and the letters that accent's own text color. It goes anywhere a picture does, as a node's `icon:`, a `badge:`, or a link's badge.
|
|
203
|
+
|
|
202
204
|
A file (`icon rack "./rack.svg"`) is found relative to the diagram's own file. Only the command line can read one; in the playground or a web page, paste the SVG. A declared name replaces a built-in of the same name, and declaring one name twice is an error. Every picture is drawn into the output as path data, so the SVG stays standalone.
|
|
203
205
|
|
|
204
206
|
### Badges
|
|
@@ -1137,7 +1139,22 @@ node store "Store" right of docs (gap: 12)
|
|
|
1137
1139
|
edge docs -> store "read first" url: "https://example.com/order"
|
|
1138
1140
|
```
|
|
1139
1141
|
|
|
1140
|
-
`url:` makes a thing clickable in a viewer that follows links. The whole thing is the target: a node's box and everything in it, a line
|
|
1142
|
+
`url:` makes a thing clickable in a viewer that follows links. The whole thing is the target: a node's box and everything in it, a line and its text. The value is quoted, since an unquoted `//` starts a comment. Nothing about a destination is visible. A container's destination reaches children that name none. A line with no text may have one; an unseen wider copy of the line takes the click. A style may not carry one.
|
|
1143
|
+
|
|
1144
|
+
A thing can have several links, written one after another, each with a name and a badge in a bracket after it:
|
|
1145
|
+
|
|
1146
|
+
```
|
|
1147
|
+
node api "Orders API"
|
|
1148
|
+
url: "https://app.datadoghq.com/…" (name: "Datadog", icon: "D" (fill: #632ca6, text: white))
|
|
1149
|
+
"https://github.com/…" (name: "GitHub", icon: gh)
|
|
1150
|
+
"https://app.shortcut.com/…" (name: "Shortcut")
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
- **`name:`** is what a reader is shown, in quotes. A link with none shows its site, such as `github.com`.
|
|
1154
|
+
- **`icon:`** is its badge. Quoted, it is letters in a small circle, `icon: "GH"`, whose colors go in a bracket after it, `(fill: #24292f, text: white)`; left out, they are the theme's. Unquoted, it is a picture: a built-in icon, or one declared with `icon <name>` ([Your own icons](#your-own-icons)), which keeps its own colors. With no `icon:`, the badge is the first letter of the name.
|
|
1155
|
+
- **In an SVG**, a click follows the first link, and the browser's tooltip lists them all. **On a page** made with `--page`, a click selects the thing and the panel beside the drawing lists its links; a link is followed from there ([A page for looking at a diagram](#a-page-for-looking-at-a-diagram)).
|
|
1156
|
+
|
|
1157
|
+
The same address twice in one `url:` is refused.
|
|
1141
1158
|
|
|
1142
1159
|
## Errors and findings
|
|
1143
1160
|
|
|
@@ -1196,7 +1213,11 @@ The output is a standalone SVG: every picture is drawn into it as path data, nev
|
|
|
1196
1213
|
reladraw render arch.reladraw --page
|
|
1197
1214
|
```
|
|
1198
1215
|
|
|
1199
|
-
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Drag to pan; scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The
|
|
1216
|
+
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Under the title it says which file it was drawn from and when, such as "drawn from arch.reladraw, 2026-11-05 14:32": the page is a snapshot of the file, and is behind it once the file is edited, until it is rendered again. Drag to pan; scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The Save menu saves the page itself, as it was written rather than at its current zoom; the `.reladraw` source it was drawn from; or the picture as an SVG or a PNG, as `render` would draw it; or copies the SVG. Each is named after the source file: `arch.html`, `arch.reladraw`, `arch.svg`, `arch.png`. In Chrome and Edge it asks where to save; other browsers put the file with their downloads. The findings are listed beside it in a panel that folds away, and clicking one lights up what its line drew. A box's or a line's name shows under the pointer.
|
|
1217
|
+
|
|
1218
|
+
**A click on a box or a line selects it**, and never follows a link. The selection is amber; what feeds it is green and what it feeds is red, the lines dashed and moving the way they run; the rest dims. A line runs from the end without an arrowhead when only one end has one, and otherwise from the end written first. The panel beside the drawing names the selection and lists its links, each with its badge, and the boxes it comes from and goes to; a link opens in a new tab, and a box selects it. One line out lights by default, and the panel's button lights everything upstream and downstream. A box with links underlines its text under the pointer. A click on the background or Escape clears the selection.
|
|
1219
|
+
|
|
1220
|
+
Keys: `+` and `-` zoom, `0` fits, `1` is actual size, `f` is full screen, Escape clears the selection.
|
|
1200
1221
|
|
|
1201
1222
|
### Formatting
|
|
1202
1223
|
|
|
@@ -1240,7 +1261,7 @@ clause-gap: 2
|
|
|
1240
1261
|
|
|
1241
1262
|
An unknown setting, one written twice, or a value it doesn't take is an error naming it.
|
|
1242
1263
|
|
|
1243
|
-
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured
|
|
1264
|
+
**Breaking a long statement.** With both at a number, a statement up to `wrap-at` stays on one line, one up to `clause-per-line-at` wraps, and a longer one goes one clause to a line. Its length is measured as it would be printed, lined up with its neighbors, so a short statement pushed past the width by a long neighbor's columns breaks too; its first line still lines up with theirs. A statement that can't break, such as a node's name and text with nothing after them, is left as it is, even when its columns carry it past the width. A break falls only between clauses, never inside one or inside a bracket, so a clause longer than the width stands alone on its line, past it. A statement broken by hand is broken again by these rules, or joined when they leave it short enough; one kept on its lines for a comment among them is left as it is. A comment at the end of a broken statement follows its last line.
|
|
1244
1265
|
|
|
1245
1266
|
```
|
|
1246
1267
|
// wrap-at: 60
|
|
@@ -1317,6 +1338,21 @@ Designed and absent from the code.
|
|
|
1317
1338
|
|
|
1318
1339
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal is refused by name with the replacement quoted, rather than dropped in silence: an older file stops with an error saying what to write instead.
|
|
1319
1340
|
|
|
1341
|
+
**0.23.0**
|
|
1342
|
+
|
|
1343
|
+
- **A box or a line can have several links**, each with a name and a badge: `url: "https://github.com/…" (name: "GitHub", icon: "GH") "https://app.datadoghq.com/…" (name: "Datadog")`. In an SVG a click follows the first and the tooltip lists them all. See [Destinations](#destinations).
|
|
1344
|
+
- **A click on the page selects a box or a line.** The page made with `render --page` lights what feeds it in green and what it feeds in red, and a panel beside the drawing lists its links and the boxes at either end. A click no longer follows a link on the page; the panel does. See [A page for looking at a diagram](#a-page-for-looking-at-a-diagram).
|
|
1345
|
+
- **A line with no text can have a link.** It used to be refused as too thin to click; an unseen wider copy of the line now takes the click.
|
|
1346
|
+
- **A letter icon**: `icon gh letters: "GH" fill: #24292f text: white` is a few letters in a circle, usable anywhere an icon is. See [Your own icons](#your-own-icons).
|
|
1347
|
+
- **The page can save its own source, and itself.** A page made with `render --page` now carries the `.reladraw` file it was drawn from, so whoever is handed the page can get the source back. Its Export menu is now a Save menu: This page, .reladraw source, SVG, PNG and Copy SVG, and in Chrome and Edge each save asks where the file goes. Under its title the page says which file it was drawn from and when, so you can tell whether it is behind the file.
|
|
1348
|
+
- **The agent skill keeps throwaway diagrams out of your project.** A diagram drawn only to show you something now goes in a folder outside the project, such as `~/.cache/reladraw/`, and a project's own diagram gets its page there too. While fixing findings it runs `reladraw check`, so no stray SVG is left beside the source.
|
|
1349
|
+
|
|
1350
|
+
**0.22.1**
|
|
1351
|
+
|
|
1352
|
+
- **`reladraw format` keeps to `wrap-at` once the columns are lined up.** A statement's length used to be measured on its own, before it was lined up with its neighbors, so a short one like `node api "API" layout: column` stayed on one line and was then padded out by a long neighbor's name to nearly twice the width. It is now measured as printed and breaks like any other long statement; `clause-per-line-at` is measured the same way.
|
|
1353
|
+
- **Output sent to another program is no longer cut off.** `reladraw format -` and `reladraw render -o -` stopped after the first 64 KB when their output went into a pipe rather than a file, and still reported success, so an editor formatting a large diagram could replace it with a shortened copy. All of it now comes through.
|
|
1354
|
+
- **The agent skill puts a link on the thing it belongs to.** Asked to add links, an agent would add a new text node for each one; the skill now says to give the existing node or line a `url:`. When a diagram's things have real web addresses, such as a pull request's files, it offers to link them rather than doing it unasked.
|
|
1355
|
+
|
|
1320
1356
|
**0.22.0**
|
|
1321
1357
|
|
|
1322
1358
|
- **`reladraw install-skill`** installs the agent skill that came with your reladraw, so it teaches what your copy accepts rather than what the main branch does. It hands the work to `skills`, passing on `-g`, `-a` and the rest. The npm package now carries the skill.
|
package/dist/ast.d.ts
CHANGED
|
@@ -804,6 +804,12 @@ export interface IconStmt {
|
|
|
804
804
|
* starting with `<`. Otherwise it names a file.
|
|
805
805
|
*/
|
|
806
806
|
pasted: boolean;
|
|
807
|
+
/** `icon gh letters: "GH" fill: … text: …`: letters in a circle, in place of a picture. */
|
|
808
|
+
letters?: {
|
|
809
|
+
letters: string;
|
|
810
|
+
fill?: string;
|
|
811
|
+
text?: string;
|
|
812
|
+
};
|
|
807
813
|
line: number;
|
|
808
814
|
}
|
|
809
815
|
export type Stmt = NodeStmt | EdgeStmt | StyleStmt | DiagramStmt | DefaultStmt | IconStmt | PointStmt | RegionStmt | BundleStmt;
|
package/dist/cli.js
CHANGED
|
@@ -45,9 +45,9 @@ const USAGE = `reladraw — draw a diagram from stated placement
|
|
|
45
45
|
its extension replaced by .svg. Use - for standard output.
|
|
46
46
|
--page render writes a page for looking at the diagram instead: one
|
|
47
47
|
HTML file in the diagram's colors, headed with its title, that
|
|
48
|
-
pans, zooms, goes full screen, saves
|
|
49
|
-
findings and names what is under the
|
|
50
|
-
input path with .html.
|
|
48
|
+
pans, zooms, goes full screen, saves itself, its source, the SVG
|
|
49
|
+
or a PNG, lists the findings and names what is under the
|
|
50
|
+
pointer. Defaults to the input path with .html.
|
|
51
51
|
--theme render in this theme, whatever the file's \`diagram theme:\`
|
|
52
52
|
says. One of ${THEME_NAMES.join(', ')}.
|
|
53
53
|
--guides draw the guides: everything written with \`guide\` in front,
|
|
@@ -218,7 +218,7 @@ async function main(argv) {
|
|
|
218
218
|
let drawn;
|
|
219
219
|
try {
|
|
220
220
|
if (page) {
|
|
221
|
-
const made = drawPage(source, basename(input)
|
|
221
|
+
const made = drawPage(source, basename(input), options);
|
|
222
222
|
drawn = { text: made.html, findings: made.findings };
|
|
223
223
|
}
|
|
224
224
|
else {
|
|
@@ -417,7 +417,13 @@ function nearestSettings(folder, found) {
|
|
|
417
417
|
found.set(folder, result);
|
|
418
418
|
return result;
|
|
419
419
|
}
|
|
420
|
-
|
|
420
|
+
// The exit code is set, not exited with: process.exit ends the process at
|
|
421
|
+
// once, and a write to a pipe that hasn't drained yet is lost, which cut
|
|
422
|
+
// `format -` and `render -o -` off at 64 KB. Node exits on its own once the
|
|
423
|
+
// output is written. `./dev.sh pipe` checks it.
|
|
424
|
+
main(process.argv.slice(2)).then((code) => {
|
|
425
|
+
process.exitCode = code;
|
|
426
|
+
}, (error) => {
|
|
421
427
|
process.stderr.write(`reladraw: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
422
|
-
process.
|
|
428
|
+
process.exitCode = 1;
|
|
423
429
|
});
|
package/dist/format.js
CHANGED
|
@@ -280,16 +280,15 @@ function textWidth(text) {
|
|
|
280
280
|
/** How wide a tab counts as, where a width has to be known. */
|
|
281
281
|
const TAB_WIDTH = 4;
|
|
282
282
|
/**
|
|
283
|
-
* Whether a statement goes past `wrap-at` or
|
|
284
|
-
* where its first line ends.
|
|
285
|
-
*
|
|
286
|
-
* holds the keyword and name, and the texts
|
|
287
|
-
* so, and shares columns with its neighbors
|
|
288
|
-
* clauses after it are laid out by
|
|
283
|
+
* Whether a statement `length` characters long goes past `wrap-at` or
|
|
284
|
+
* `clause-per-line-at`, and if so where its first line ends. The length is
|
|
285
|
+
* the line as it would be printed, lined up with its neighbors (see
|
|
286
|
+
* `layOutItems`). The first line holds the keyword and name, and the texts
|
|
287
|
+
* after them if `first-line` says so, and shares columns with its neighbors
|
|
288
|
+
* like a one-line statement; the clauses after it are laid out by
|
|
289
|
+
* `continuedLines`.
|
|
289
290
|
*/
|
|
290
|
-
function breakStatement(item, settings,
|
|
291
|
-
const texts = item.cells.map((cell) => cell.text);
|
|
292
|
-
const length = textWidth(texts.filter((text, at) => text !== '' || at === 0).join(gap));
|
|
291
|
+
function breakStatement(item, settings, length) {
|
|
293
292
|
const perLine = settings['clause-per-line-at'];
|
|
294
293
|
const wrap = settings['wrap-at'];
|
|
295
294
|
const oneEach = perLine === 'always' || (typeof perLine === 'number' && length > perLine);
|
|
@@ -338,7 +337,8 @@ function continuedLines(item, indent, settings, gap, gapWidth) {
|
|
|
338
337
|
function layOutItems(written, settings) {
|
|
339
338
|
const gap = settings['clause-gap'] === 'tab' ? '\t' : ' '.repeat(settings['clause-gap']);
|
|
340
339
|
const gapWidth = settings['clause-gap'] === 'tab' ? 1 : settings['clause-gap'];
|
|
341
|
-
|
|
340
|
+
// Which statements break is decided further down, once the columns are known.
|
|
341
|
+
const items = [...written];
|
|
342
342
|
// What lines up in columns: a one-line statement whole, a broken one's first line.
|
|
343
343
|
const aligned = (item) => item.kind === 'statement' ? item.cells : item.kind === 'broken' ? item.first : undefined;
|
|
344
344
|
// Which statements share columns: each is given a group number. A comment
|
|
@@ -372,17 +372,20 @@ function layOutItems(written, settings) {
|
|
|
372
372
|
previous = item;
|
|
373
373
|
});
|
|
374
374
|
// Each group's column widths, and the column its trailing comments start at.
|
|
375
|
-
|
|
375
|
+
let widths = new Map();
|
|
376
376
|
const commentAt = new Map();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
377
|
+
const measureColumns = () => {
|
|
378
|
+
widths = new Map();
|
|
379
|
+
items.forEach((item, index) => {
|
|
380
|
+
const cells = aligned(item);
|
|
381
|
+
if (cells === undefined)
|
|
382
|
+
return;
|
|
383
|
+
const id = group.get(index);
|
|
384
|
+
const own = widths.get(id) ?? [];
|
|
385
|
+
cells.forEach((cell, at) => (own[at] = Math.max(own[at] ?? 0, textWidth(cell.text))));
|
|
386
|
+
widths.set(id, own);
|
|
387
|
+
});
|
|
388
|
+
};
|
|
386
389
|
// Each cell's text, and where on the line it starts.
|
|
387
390
|
const placed = (cells, id) => {
|
|
388
391
|
const own = widths.get(id);
|
|
@@ -407,6 +410,50 @@ function layOutItems(written, settings) {
|
|
|
407
410
|
line += ' '.repeat(one.at - textWidth(line)) + one.text;
|
|
408
411
|
return line;
|
|
409
412
|
};
|
|
413
|
+
// Which statements break. A statement is measured as it would be printed,
|
|
414
|
+
// lined up with its neighbors, so a short statement padded out past
|
|
415
|
+
// `wrap-at` by a long neighbor's columns breaks too. Breaking one takes its
|
|
416
|
+
// later clauses out of the columns, which can only narrow them and shorten
|
|
417
|
+
// the rest, so they are broken one at a time, the longest on its own first:
|
|
418
|
+
// that is the one most likely to be widening a column the others are past
|
|
419
|
+
// only because of. A statement whose break is settled by its unaligned
|
|
420
|
+
// length alone, since lining it up can only lengthen it, breaks before the
|
|
421
|
+
// first measure: one past `clause-per-line-at`, or past `wrap-at` with no
|
|
422
|
+
// `clause-per-line-at` above it to reach.
|
|
423
|
+
const ownLength = (item) => textWidth(item.cells.map((cell) => cell.text).filter((text, at) => text !== '' || at === 0).join(gap));
|
|
424
|
+
const breakAt = (index, length) => {
|
|
425
|
+
const item = items[index];
|
|
426
|
+
if (item.kind !== 'statement')
|
|
427
|
+
return false;
|
|
428
|
+
const broken = breakStatement(item, settings, length);
|
|
429
|
+
items[index] = broken;
|
|
430
|
+
return broken !== item;
|
|
431
|
+
};
|
|
432
|
+
const perLine = settings['clause-per-line-at'];
|
|
433
|
+
const settled = (length) => typeof perLine !== 'number' || length > perLine;
|
|
434
|
+
items.forEach((item, index) => {
|
|
435
|
+
if (item.kind !== 'statement')
|
|
436
|
+
return;
|
|
437
|
+
const own = ownLength(item);
|
|
438
|
+
if (settled(own))
|
|
439
|
+
breakAt(index, own);
|
|
440
|
+
});
|
|
441
|
+
for (;;) {
|
|
442
|
+
measureColumns();
|
|
443
|
+
let longest;
|
|
444
|
+
items.forEach((item, index) => {
|
|
445
|
+
if (item.kind !== 'statement')
|
|
446
|
+
return;
|
|
447
|
+
const printed = textWidth(lineOf(item.cells, group.get(index)));
|
|
448
|
+
if (breakStatement(item, settings, printed) === item)
|
|
449
|
+
return;
|
|
450
|
+
const own = ownLength(item);
|
|
451
|
+
if (longest === undefined || own > longest.own)
|
|
452
|
+
longest = { index, own, printed };
|
|
453
|
+
});
|
|
454
|
+
if (longest === undefined || !breakAt(longest.index, longest.printed))
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
410
457
|
items.forEach((item, index) => {
|
|
411
458
|
if (item.kind !== 'statement')
|
|
412
459
|
return;
|
package/dist/icons.d.ts
CHANGED
|
@@ -49,7 +49,17 @@ export interface PastedIcon {
|
|
|
49
49
|
readonly attrs: string;
|
|
50
50
|
readonly inner: string;
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* `icon gh letters: "GH" fill: #24292f text: white`: letters in a circle.
|
|
54
|
+
* A color left unsaid is the theme's, chosen from the letters, so the same
|
|
55
|
+
* letters get the same color in a theme.
|
|
56
|
+
*/
|
|
57
|
+
export interface LetterIcon {
|
|
58
|
+
readonly letters: string;
|
|
59
|
+
readonly fill?: string;
|
|
60
|
+
readonly text?: string;
|
|
61
|
+
}
|
|
62
|
+
export type Icon = DrawnIcon | PastedIcon | LetterIcon;
|
|
53
63
|
/** Line width on the 24-unit grid, scaled with everything else. */
|
|
54
64
|
export declare const ICON_STROKE = 1.1;
|
|
55
65
|
export declare const ICONS: Record<string, Icon>;
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A node's or a line's links. `url:` takes one address or several, each with
|
|
3
|
+
* an optional name and badge: `url: "https://github.com/…" (name: "GitHub",
|
|
4
|
+
* icon: "GH" (fill: #24292f, text: white))`. The parser keeps the first
|
|
5
|
+
* address under `url`, so everything that follows a single link follows the
|
|
6
|
+
* first of several, and the whole list under `url.links` when there is more
|
|
7
|
+
* to say than one bare address.
|
|
8
|
+
*/
|
|
9
|
+
import type { Attrs } from './ast.js';
|
|
10
|
+
export declare const LINKS_KEY = "url.links";
|
|
11
|
+
/** A link's badge: letters in a small circle, or a picture reladraw knows. */
|
|
12
|
+
export type LinkIcon = {
|
|
13
|
+
letters: string;
|
|
14
|
+
fill?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
} | {
|
|
17
|
+
picture: string;
|
|
18
|
+
};
|
|
19
|
+
export interface Link {
|
|
20
|
+
href: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
icon?: LinkIcon;
|
|
23
|
+
}
|
|
24
|
+
/** Every link a thing carries, in the order the source writes them. */
|
|
25
|
+
export declare function linksOf(attrs: Attrs): Link[] | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* What a reader is shown for a link: its name, or its site's address without
|
|
28
|
+
* the path. Two unnamed links to one site would read the same, so among
|
|
29
|
+
* `others` that share its site a link shows its whole address after the scheme.
|
|
30
|
+
*/
|
|
31
|
+
export declare function linkLabel(link: Link, others?: readonly Link[]): string;
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A node's or a line's links. `url:` takes one address or several, each with
|
|
3
|
+
* an optional name and badge: `url: "https://github.com/…" (name: "GitHub",
|
|
4
|
+
* icon: "GH" (fill: #24292f, text: white))`. The parser keeps the first
|
|
5
|
+
* address under `url`, so everything that follows a single link follows the
|
|
6
|
+
* first of several, and the whole list under `url.links` when there is more
|
|
7
|
+
* to say than one bare address.
|
|
8
|
+
*/
|
|
9
|
+
export const LINKS_KEY = 'url.links';
|
|
10
|
+
/** Every link a thing carries, in the order the source writes them. */
|
|
11
|
+
export function linksOf(attrs) {
|
|
12
|
+
const listed = attrs[LINKS_KEY];
|
|
13
|
+
if (listed !== undefined)
|
|
14
|
+
return JSON.parse(listed);
|
|
15
|
+
const url = attrs['url'];
|
|
16
|
+
return url === undefined ? undefined : [{ href: url }];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What a reader is shown for a link: its name, or its site's address without
|
|
20
|
+
* the path. Two unnamed links to one site would read the same, so among
|
|
21
|
+
* `others` that share its site a link shows its whole address after the scheme.
|
|
22
|
+
*/
|
|
23
|
+
export function linkLabel(link, others = []) {
|
|
24
|
+
if (link.name !== undefined)
|
|
25
|
+
return link.name;
|
|
26
|
+
const host = hostOf(link.href);
|
|
27
|
+
if (host === undefined)
|
|
28
|
+
return link.href;
|
|
29
|
+
const shared = others.some((other) => other !== link && other.name === undefined && hostOf(other.href) === host);
|
|
30
|
+
return shared ? link.href.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '') : host;
|
|
31
|
+
}
|
|
32
|
+
function hostOf(href) {
|
|
33
|
+
return href.match(/^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i)?.[1];
|
|
34
|
+
}
|
package/dist/model.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface Rect {
|
|
|
7
7
|
height: number;
|
|
8
8
|
}
|
|
9
9
|
import type { Line } from './text.js';
|
|
10
|
-
import type { Body } from './icons.js';
|
|
10
|
+
import type { Body, Icon } from './icons.js';
|
|
11
11
|
/** A node with its geometry solved. Coordinates are absolute, origin top-left. */
|
|
12
12
|
/** A distance past each side of a box. */
|
|
13
13
|
export interface Reach {
|
|
@@ -312,6 +312,8 @@ export interface Layout {
|
|
|
312
312
|
* rather than stating one, and styles are otherwise merged away by here.
|
|
313
313
|
*/
|
|
314
314
|
markup: Record<string, string>;
|
|
315
|
+
/** The pictures links' badges name, by name, for the page that draws the badges. */
|
|
316
|
+
linkIcons: Record<string, Icon>;
|
|
315
317
|
/**
|
|
316
318
|
* What the `diagram` statement said, as written. Nothing here affects
|
|
317
319
|
* geometry; it rides along so the renderer sees the whole compiled document
|
package/dist/page.d.ts
CHANGED
|
@@ -3,12 +3,15 @@ import type { DrawOptions } from './index.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* `reladraw render --page`: one diagram in one self-contained HTML page, made
|
|
5
5
|
* for looking at rather than embedding. The page is the diagram's own colors,
|
|
6
|
-
* headed with its title, and it pans, zooms, goes full screen, saves
|
|
7
|
-
* or a PNG, lists the findings, and names a box under the
|
|
8
|
-
* nothing running and fetches nothing, so it can be
|
|
9
|
-
* committed. `
|
|
6
|
+
* headed with its title, and it pans, zooms, goes full screen, saves itself,
|
|
7
|
+
* its source, the SVG or a PNG, lists the findings, and names a box under the
|
|
8
|
+
* pointer. It needs nothing running and fetches nothing, so it can be
|
|
9
|
+
* attached, mailed or committed. `file` is the source file's name, such as
|
|
10
|
+
* `flow.reladraw`; without its extension it is the title when the file states
|
|
11
|
+
* none, and the name everything saved from the page is given. The page is a
|
|
12
|
+
* snapshot of the file at `at`, and says so under its title.
|
|
10
13
|
*/
|
|
11
|
-
export declare function drawPage(source: string,
|
|
14
|
+
export declare function drawPage(source: string, file: string, options?: DrawOptions, at?: Date): {
|
|
12
15
|
html: string;
|
|
13
16
|
findings: Finding[];
|
|
14
17
|
};
|