reladraw 0.18.0 → 0.19.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/README.md CHANGED
@@ -43,7 +43,7 @@ npm install -g reladraw
43
43
  reladraw render diagram.reladraw -o diagram.svg
44
44
  ```
45
45
 
46
- `reladraw check diagram.reladraw` prints the warnings without writing a picture.
46
+ `reladraw check diagram.reladraw` prints the warnings without writing a picture, and `reladraw format diagram.reladraw` lines its statements up in columns.
47
47
 
48
48
  Or from a clone, which also gets you the examples:
49
49
 
@@ -86,7 +86,7 @@ That gets you a copy of the skill at the time you run it, so you'll need to re-r
86
86
 
87
87
  ## Status
88
88
 
89
- Version 0.18.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.
89
+ Version 0.19.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.
90
90
 
91
91
  The language isn't stable yet, so expect the syntax to change.
92
92
 
package/SYNTAX.md CHANGED
@@ -1,4 +1,4 @@
1
- # Syntax reference — 0.18.0
1
+ # Syntax reference — 0.19.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
 
@@ -55,7 +55,7 @@ What every statement of a kind has is positional; what is optional is a key. A w
55
55
 
56
56
  **A value ends when its own shape is complete.** `height: 0.5 group height right of a` reads a size, then a placement. `to: top, right of z` keeps `right of z` inside `to:`, because it is part of that value. So word order stays free and nothing is ambiguous.
57
57
 
58
- **A bracket modifies the one thing it follows.** A setting that belongs to one part of a statement is written in brackets right after that part: a placement's `(gap: wide)`, a text's `(size: small, wrap: 30)`, a line's `line: (path: square)`, a layout's `layout: column (gap: none)`, a waypoint's `b top (gap: wide)`. The scope is visible instead of inferred. Inside a bracket the comma is optional.
58
+ **A bracket modifies the one thing it follows.** A setting that belongs to one part of a statement is written in brackets right after that part: a placement's `(gap: wide)`, a text's `(size: small, wrap: 30)`, a line's `line: (path: square)`, a layout's `layout: column (gap: none)`, a waypoint's `b top (gap: wide)`. The scope is visible instead of inferred. Inside a bracket the comma is optional, and spaces around it don't matter: `(corners: rounded,path: square)` and `(corners: rounded , path: square)` are both `(corners: rounded, path: square)`.
59
59
 
60
60
  ### Names and words
61
61
 
@@ -1162,6 +1162,59 @@ reladraw check diagram.reladraw
1162
1162
 
1163
1163
  The output is a standalone SVG: every picture is drawn into it as path data, never from a font or a linked file. A web page can draw a diagram in place with `<reladraw-diagram>`; see "In a web page" in the README.
1164
1164
 
1165
+ ### Formatting
1166
+
1167
+ ```
1168
+ reladraw format diagram.reladraw other.reladraw
1169
+ reladraw format --check diagram.reladraw
1170
+ reladraw format - < diagram.reladraw
1171
+ reladraw format --init
1172
+ ```
1173
+
1174
+ `format` lays each file out again in place. By default every statement is on one line, and neighboring statements with the same keyword line their clauses up in columns, each as wide as its longest clause plus two spaces; a blank line or a new keyword starts a new set of columns, and a comment line between them doesn't. A comment at the end of a line lines up with the others in its set. A statement written over several lines with a comment among them keeps its lines, with the indent tidied. Blank lines stay as written, and so do Windows line endings.
1175
+
1176
+ Inside a clause, spacing comes out as these pages write it, whatever was typed: one space after a key's colon and between words, none just inside a bracket, and a comma against the word before it, so `line:( corners:rounded , path:square )` becomes `line: (corners: rounded, path: square)`.
1177
+
1178
+ Only whitespace changes, and the order of clauses when `clause-order` asks: every word is copied as it was written, and the result is checked to draw exactly what the original did. A file that doesn't parse is reported and left alone, and the other files still run. `--check` changes nothing, prints each file that would change, and fails if there is one, for a pre-commit hook or CI. A shell expands `*.reladraw` into the files; folders aren't searched.
1179
+
1180
+ `-` in place of the files reads the diagram from standard input and prints it formatted, touching no file. That is how an editor formats a buffer, saved or not: Vim's `:%!reladraw format -`, or the command a format-on-save package runs. Its settings come from the nearest `.reladraw-format` to the current folder.
1181
+
1182
+ The settings are in the nearest `.reladraw-format`, in the file's folder or a folder above it; with none, the defaults. A project that puts one at its top keeps everyone's formatting the same. `reladraw format --init` writes one in the current folder with every setting at its default and a comment on what each value does, and refuses if one is there already. One setting a line, as in a diagram:
1183
+
1184
+ ```
1185
+ // keep each clause straight after the last
1186
+ columns: off
1187
+ clause-gap: 2
1188
+ ```
1189
+
1190
+ | setting | values, default first |
1191
+ |---|---|
1192
+ | `wrap-at` | `off`, or a number of characters: past it, a statement wraps onto as few lines as fit |
1193
+ | `clause-per-line-at` | `off`, a number of characters, or `always`: past it, every clause gets its own line |
1194
+ | `first-line` | `text` (keyword, name and texts), `name`: where a broken statement's first line ends |
1195
+ | `clause-order` | `as-written`, or a list of kinds: `placement`, and keys by name, such as `style, placement, fill` |
1196
+ | `columns` | `aligned`, `off` |
1197
+ | `columns-shared-by` | `same-keyword-neighbors`, `neighbors`, `same-keyword` (across blank lines), `file` |
1198
+ | `clause-gap` | a number of spaces, `2`; or `tab`, with `columns: off` only |
1199
+ | `trailing-comments` | `shared-column`, `after-statement` |
1200
+ | `comments-inside` | `keep-lines`, `move-above` (joined, comments above it), `refuse` (left alone, comment named) |
1201
+ | `continued-lines-indent` | where a broken or kept statement's later lines start: `4` or another number, `tab`, `under-name`, `under-name + 2`, `under-text` (under the name when there's no text) |
1202
+ | `blank-lines` | `as-written`, `exactly 2`, `at-most 3` |
1203
+ | `edge-name-and-ends` | `one-column`, `own-columns` (name, each end and the arrow a column each) |
1204
+
1205
+ An unknown setting, one written twice, or a value it doesn't take is an error naming it.
1206
+
1207
+ **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 on its own, before columns, so whether it breaks never depends on its neighbors; its first line still lines up with theirs. 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.
1208
+
1209
+ ```
1210
+ // wrap-at: 60
1211
+ node confirm "Confirm"
1212
+ below accept (gap: tight) left of accept (gap: wide)
1213
+ style: plain
1214
+ ```
1215
+
1216
+ **Ordering clauses.** `clause-order` lists the kinds of clause in the order they are written: `placement` for every relation, and a key by its name (`style`, `fill`, `via`). Kinds it leaves out follow, in the order written, and clauses of one kind keep theirs. A node's or edge's texts never move, since the language takes them only straight after the name; `text` in the list means a style's `text:` key. A clause never moves where moving it would change what the statement says.
1217
+
1165
1218
  ## Attributes
1166
1219
 
1167
1220
  Every attribute, and what takes one. A node's kind is what its body is: a box (`shape:`), a picture (`icon:`) or bare text (`shape: none`).
@@ -1228,6 +1281,15 @@ Designed and absent from the code.
1228
1281
 
1229
1282
  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.
1230
1283
 
1284
+ **0.19.0**
1285
+
1286
+ - **`reladraw format`** lays a file out again: each statement on one line, neighboring statements with the same keyword lined up in columns, comments kept beside what they are about. No word is respelled, and the result is checked to draw what the original did. `--check` fails on a file that would change, and `.reladraw-format` holds a project's settings; `reladraw format --init` writes one with every setting explained. `reladraw format -` reads standard input and prints the result, for editors. A long statement can wrap past a width (`wrap-at`) or go one clause to a line (`clause-per-line-at`), and `clause-order` puts placements and keys in a chosen order. The playground has a Format button (Shift+Alt+F, undone with Ctrl+Z) and a panel for its settings, which can copy them as a `.reladraw-format`. Spacing inside a clause is evened out too: `gap:tight` becomes `gap: tight`, `( gap: tight )` becomes `(gap: tight)`. Prompted by #14, where a long statement was hard to read in a plain editor.
1287
+ - **A comma may touch the next word or stand apart from both.** `(corners: rounded,path: square)` used to be refused with "`corners` has no value", and `(at: start , above edge)` was refused in a text's bracket; both now read as `rounded, path:` and `start, above` do.
1288
+ - **The playground opens and saves `.reladraw` files**, with Open… and Save… above the source. Save suggests the name of the file last opened or saved; Chrome and Edge ask where to put it, other browsers save it with the downloads. Opening a file over edits that haven't been saved to one asks first.
1289
+ - **The playground's error message can be copied**, with a Copy button beside it or by selecting it. Only the underlined line number jumps to the line now; a click anywhere on the message used to, which took a selection away.
1290
+ - **The playground no longer fails on some diagrams with 0.18.0's shapes**, which stopped with "insidePolygon is not defined". The command line was unaffected.
1291
+ - **The agent skill checks it matches the installed tool.** It compares its own version with `reladraw --version` and tells the user which of the two to update.
1292
+
1231
1293
  **0.18.0**
1232
1294
 
1233
1295
  - **More shapes, for flowcharts and the rest** (#18): `diamond`, `pill`, `parallelogram`, `hexagon`, `trapezoid`, `triangle` and `cylinder`, named for how they look, and the flowchart standard's shapes that have no geometric name as `flow-` and what they mean, from `flow-subprocess` and `flow-delay` to `flow-collate`. A shape's settings go in a bracket after its name: `shape: parallelogram (slant: 2, lean: left)`, `shape: cylinder (lying)`, `shape: document (fold: 1.5)`.
package/dist/cli.js CHANGED
@@ -4,17 +4,32 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { dirname, resolve as resolvePath } from 'node:path';
5
5
  import { nearMatch, readChecks } from './checks.js';
6
6
  import { SourceError } from './errors.js';
7
+ import { format } from './format.js';
8
+ import { DEFAULT_FORMAT, readFormatSettings, SETTINGS_FILE, settingsFileText } from './format-settings.js';
7
9
  import { draw } from './index.js';
8
10
  import { THEME_NAMES, THEMES } from './themes.js';
9
11
  const USAGE = `reladraw — draw a diagram from stated placement
10
12
 
11
13
  reladraw render <input.reladraw> [-o <output.svg>] [options]
12
14
  reladraw check <input.reladraw> [options]
15
+ reladraw format <input.reladraw>... [--check]
16
+ reladraw format - [--check]
17
+ reladraw format --init
13
18
 
14
19
  render draw the diagram to an SVG, and print any findings: places
15
20
  where the picture, drawn as written, may not read as meant.
16
21
  check lay the diagram out and print only the findings. No SVG is
17
22
  written, so a good one is left alone while you iterate.
23
+ format lay each file out again in place: spacing, columns, comments
24
+ and blank lines. No word is respelled, and a file that
25
+ doesn't parse is left alone. The nearest .reladraw-format, in
26
+ the file's folder or one above it, holds the settings.
27
+ --check changes nothing, and fails if a file would change.
28
+ - reads the diagram from standard input and prints it
29
+ formatted, for an editor; settings come from the current
30
+ folder up.
31
+ --init writes a .reladraw-format here, every setting at its
32
+ default with what it does.
18
33
 
19
34
  -o, --out where render writes the SVG. Defaults to the input path with
20
35
  its extension replaced by .svg. Use - for standard output.
@@ -29,7 +44,7 @@ const USAGE = `reladraw — draw a diagram from stated placement
29
44
  print the version.
30
45
  -h, --help print this.
31
46
  `;
32
- const COMMANDS = ['render', 'check'];
47
+ const COMMANDS = ['render', 'check', 'format'];
33
48
  async function main(argv) {
34
49
  if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
35
50
  process.stdout.write(USAGE);
@@ -56,6 +71,8 @@ async function main(argv) {
56
71
  return 1;
57
72
  }
58
73
  const command = verb;
74
+ if (command === 'format')
75
+ return formatFiles(argv.slice(1));
59
76
  let input;
60
77
  let out;
61
78
  let theme;
@@ -172,6 +189,176 @@ async function main(argv) {
172
189
  process.stderr.write(`${target}\n`);
173
190
  return 0;
174
191
  }
192
+ /**
193
+ * `reladraw format`: each file laid out again in place, by the settings in
194
+ * the nearest `.reladraw-format`. A file that fails is reported and the rest
195
+ * still run, so one broken file doesn't hide the state of the others.
196
+ */
197
+ async function formatFiles(args) {
198
+ const files = [];
199
+ let check = false;
200
+ let init = false;
201
+ for (const arg of args) {
202
+ if (arg === '--check')
203
+ check = true;
204
+ else if (arg === '--init')
205
+ init = true;
206
+ else if (arg.startsWith('-') && arg !== '-') {
207
+ process.stderr.write(`reladraw: format takes --check and --init, not ${arg}\n`);
208
+ return 1;
209
+ }
210
+ else
211
+ files.push(arg);
212
+ }
213
+ if (init) {
214
+ if (check || files.length > 0) {
215
+ process.stderr.write('reladraw: --init writes a settings file and formats nothing — run reladraw format --init on its own\n');
216
+ return 1;
217
+ }
218
+ if (existsSync(SETTINGS_FILE)) {
219
+ process.stderr.write(`reladraw: there is already a ${SETTINGS_FILE} here — delete it first to write a fresh one\n`);
220
+ return 1;
221
+ }
222
+ await writeFile(SETTINGS_FILE, settingsFileText(), 'utf8');
223
+ process.stderr.write(`${resolvePath(SETTINGS_FILE)}\n`);
224
+ return 0;
225
+ }
226
+ if (files.length === 0) {
227
+ process.stderr.write('reladraw: no input file — reladraw format <input.reladraw>..., or - to read standard input\n');
228
+ return 1;
229
+ }
230
+ if (files.includes('-'))
231
+ return formatStandardInput(files, check);
232
+ let status = 0;
233
+ const found = new Map();
234
+ for (const file of files) {
235
+ let settings = DEFAULT_FORMAT;
236
+ try {
237
+ const nearest = nearestSettings(dirname(resolvePath(file)), found);
238
+ if (nearest)
239
+ settings = nearest.settings;
240
+ }
241
+ catch (error) {
242
+ if (!(error instanceof SettingsError))
243
+ throw error;
244
+ process.stderr.write(`${error.cause.format(error.path)}\n`);
245
+ status = 1;
246
+ continue;
247
+ }
248
+ let source;
249
+ try {
250
+ source = await readFile(file, 'utf8');
251
+ }
252
+ catch {
253
+ process.stderr.write(`reladraw: can't read ${file}\n`);
254
+ status = 1;
255
+ continue;
256
+ }
257
+ let laid;
258
+ try {
259
+ laid = format(source, settings);
260
+ }
261
+ catch (error) {
262
+ process.stderr.write(error instanceof SourceError ? `${error.format(file)}\n` : `reladraw: ${file}: ${error.message}\n`);
263
+ status = 1;
264
+ continue;
265
+ }
266
+ if (laid === source)
267
+ continue;
268
+ if (check) {
269
+ process.stderr.write(`${file}: not formatted — reladraw format ${file} lays it out\n`);
270
+ status = 1;
271
+ continue;
272
+ }
273
+ await writeFile(file, laid, 'utf8');
274
+ process.stderr.write(`formatted ${file}\n`);
275
+ }
276
+ return status;
277
+ }
278
+ /**
279
+ * `reladraw format -`: the diagram read from standard input and printed
280
+ * formatted, which is how an editor formats a buffer it hasn't saved. The
281
+ * settings are the nearest `.reladraw-format` from the current folder. With
282
+ * `--check` nothing is printed, and it fails if the text would change.
283
+ */
284
+ async function formatStandardInput(files, check) {
285
+ if (files.length > 1) {
286
+ process.stderr.write('reladraw: - reads standard input, so it goes on its own — reladraw format -\n');
287
+ return 1;
288
+ }
289
+ if (process.stdin.isTTY) {
290
+ process.stderr.write('reladraw: format - reads a diagram from standard input — pipe one in: reladraw format - < diagram.reladraw\n');
291
+ return 1;
292
+ }
293
+ let settings = DEFAULT_FORMAT;
294
+ try {
295
+ settings = nearestSettings(process.cwd(), new Map())?.settings ?? DEFAULT_FORMAT;
296
+ }
297
+ catch (error) {
298
+ if (!(error instanceof SettingsError))
299
+ throw error;
300
+ process.stderr.write(`${error.cause.format(error.path)}\n`);
301
+ return 1;
302
+ }
303
+ const chunks = [];
304
+ for await (const chunk of process.stdin)
305
+ chunks.push(chunk);
306
+ const source = Buffer.concat(chunks).toString('utf8');
307
+ let laid;
308
+ try {
309
+ laid = format(source, settings);
310
+ }
311
+ catch (error) {
312
+ process.stderr.write(error instanceof SourceError ? `${error.format('<stdin>')}\n` : `reladraw: <stdin>: ${error.message}\n`);
313
+ return 1;
314
+ }
315
+ if (check) {
316
+ if (laid === source)
317
+ return 0;
318
+ process.stderr.write('<stdin>: not formatted\n');
319
+ return 1;
320
+ }
321
+ process.stdout.write(laid);
322
+ return 0;
323
+ }
324
+ /** A `.reladraw-format` that doesn't read, and where it is. */
325
+ class SettingsError extends Error {
326
+ path;
327
+ cause;
328
+ constructor(path, cause) {
329
+ super(cause.message);
330
+ this.path = path;
331
+ this.cause = cause;
332
+ }
333
+ }
334
+ /**
335
+ * The settings in the nearest `.reladraw-format`, looking in `folder` and
336
+ * then each folder above it, or nothing when there is none. Each folder is
337
+ * looked in once however many files share it.
338
+ */
339
+ function nearestSettings(folder, found) {
340
+ const known = found.get(folder);
341
+ if (known !== undefined)
342
+ return known;
343
+ const path = resolvePath(folder, SETTINGS_FILE);
344
+ let result;
345
+ if (existsSync(path)) {
346
+ try {
347
+ result = { path, settings: readFormatSettings(readFileSync(path, 'utf8')) };
348
+ }
349
+ catch (error) {
350
+ if (error instanceof SourceError)
351
+ throw new SettingsError(path, error);
352
+ throw error;
353
+ }
354
+ }
355
+ else {
356
+ const parent = dirname(folder);
357
+ result = parent === folder ? null : nearestSettings(parent, found);
358
+ }
359
+ found.set(folder, result);
360
+ return result;
361
+ }
175
362
  main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
176
363
  process.stderr.write(`reladraw: ${error instanceof Error ? error.message : String(error)}\n`);
177
364
  process.exit(1);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * How `reladraw format` lays a file out. Each key is a setting as it is
3
+ * written in a `.reladraw-format` file, so an error, the generated file and
4
+ * the code all use one name for each.
5
+ */
6
+ export interface FormatSettings {
7
+ 'wrap-at': 'off' | number;
8
+ 'clause-per-line-at': 'off' | 'always' | number;
9
+ 'first-line': 'text' | 'name';
10
+ 'continued-lines-indent': number | 'tab' | 'under-name' | 'under-text' | {
11
+ underName: number;
12
+ };
13
+ 'clause-order': 'as-written' | string[];
14
+ 'clause-gap': number | 'tab';
15
+ columns: 'aligned' | 'off';
16
+ 'columns-shared-by': 'same-keyword-neighbors' | 'neighbors' | 'same-keyword' | 'file';
17
+ 'comments-inside': 'keep-lines' | 'move-above' | 'refuse';
18
+ 'trailing-comments': 'shared-column' | 'after-statement';
19
+ 'blank-lines': 'as-written' | {
20
+ exactly: number;
21
+ } | {
22
+ atMost: number;
23
+ };
24
+ 'edge-name-and-ends': 'one-column' | 'own-columns';
25
+ }
26
+ /** One setting: what it controls, its values in words, and how to read one. */
27
+ interface Setting {
28
+ name: keyof FormatSettings;
29
+ /** One sentence for the generated file: what the setting controls. */
30
+ about: string;
31
+ /** Each value as written, and what it means. The first is the default. */
32
+ values: [string, string][];
33
+ /** The default, as written. */
34
+ written: string;
35
+ /** The value as written, read; a string thrown is the reason it is refused. */
36
+ read: (value: string) => FormatSettings[keyof FormatSettings];
37
+ }
38
+ /**
39
+ * The kinds of clause `clause-order` can list: a statement's texts, its
40
+ * placements, and each key by its own name. `text` is also a style's `text:`
41
+ * key, which is a style's way of saying what a text says.
42
+ */
43
+ export declare const CLAUSE_KINDS: readonly string[];
44
+ export declare const SETTINGS: Setting[];
45
+ export declare const DEFAULT_FORMAT: FormatSettings;
46
+ /** The file a project's settings are written in. */
47
+ export declare const SETTINGS_FILE = ".reladraw-format";
48
+ /**
49
+ * Read a `.reladraw-format` file: one `key: value` setting a line, `//`
50
+ * comments, blank lines. An unknown setting, one written twice, or a value it
51
+ * doesn't take is refused by name, with the line.
52
+ */
53
+ export declare function readFormatSettings(text: string): FormatSettings;
54
+ /**
55
+ * The `.reladraw-format` that `reladraw format --init` writes: every setting
56
+ * at its default, each under a comment saying what it controls and what each
57
+ * of its values means.
58
+ */
59
+ export declare function settingsFileText(): string;
60
+ export {};
@@ -0,0 +1,250 @@
1
+ import { ALL_ATTR_KEYS } from './ast.js';
2
+ import { nearMatch } from './checks.js';
3
+ import { SourceError } from './errors.js';
4
+ const SETTING_COUNT = /^[1-9]\d*$/;
5
+ /**
6
+ * A setting whose values are words, one of them the default. Anything else is
7
+ * refused with the list.
8
+ */
9
+ function wordSetting(name, about, values) {
10
+ return {
11
+ name,
12
+ about,
13
+ values,
14
+ written: values[0][0],
15
+ read: (value) => {
16
+ if (values.some(([word]) => word === value))
17
+ return value;
18
+ const near = nearMatch(value, values.map(([word]) => word));
19
+ throw `${name}: "${value}" is not one of its values${near ? ` — did you mean ${near}?` : ''} — it takes ${orList(values.map(([word]) => word))}`;
20
+ },
21
+ };
22
+ }
23
+ /**
24
+ * The kinds of clause `clause-order` can list: a statement's texts, its
25
+ * placements, and each key by its own name. `text` is also a style's `text:`
26
+ * key, which is a style's way of saying what a text says.
27
+ */
28
+ export const CLAUSE_KINDS = [...new Set(['text', 'placement', ...ALL_ATTR_KEYS])];
29
+ export const SETTINGS = [
30
+ {
31
+ name: 'wrap-at',
32
+ about: 'Past this many characters, a statement wraps onto more lines, as few as fit.',
33
+ values: [
34
+ ['off', 'never wrap'],
35
+ ['a number', 'wrap past that many characters, such as 80'],
36
+ ],
37
+ written: 'off',
38
+ read: (value) => {
39
+ if (value === 'off')
40
+ return value;
41
+ if (SETTING_COUNT.test(value))
42
+ return Number(value);
43
+ throw `wrap-at: "${value}" is not one of its values — it takes off, or a number of characters such as 80`;
44
+ },
45
+ },
46
+ {
47
+ name: 'clause-per-line-at',
48
+ about: 'Past this many characters, each clause goes on its own line instead.',
49
+ values: [
50
+ ['off', 'never'],
51
+ ['a number', 'past that many characters, such as 120'],
52
+ ['always', 'every statement, whatever its length'],
53
+ ],
54
+ written: 'off',
55
+ read: (value) => {
56
+ if (value === 'off' || value === 'always')
57
+ return value;
58
+ if (SETTING_COUNT.test(value))
59
+ return Number(value);
60
+ throw `clause-per-line-at: "${value}" is not one of its values — it takes off, always, or a number of characters such as 120`;
61
+ },
62
+ },
63
+ wordSetting('first-line', 'When a statement breaks onto more lines, where its first line ends.', [
64
+ ['text', 'after the text: keyword, name and text stay together'],
65
+ ['name', 'after the name'],
66
+ ]),
67
+ {
68
+ name: 'continued-lines-indent',
69
+ about: 'Where the lines after a statement\'s first one start.',
70
+ values: [
71
+ ['4', 'that many spaces in, such as 4'],
72
+ ['tab', 'one tab in'],
73
+ ['under-name', 'under the statement\'s name'],
74
+ ['under-name + 2', 'that many characters past the start of the name'],
75
+ ['under-text', 'under the statement\'s text, or what comes after the name'],
76
+ ],
77
+ written: '4',
78
+ read: (value) => {
79
+ if (SETTING_COUNT.test(value))
80
+ return Number(value);
81
+ if (value === 'tab' || value === 'under-name' || value === 'under-text')
82
+ return value;
83
+ const shifted = /^under-name \+ ([1-9]\d*)$/.exec(value);
84
+ if (shifted)
85
+ return { underName: Number(shifted[1]) };
86
+ throw `continued-lines-indent: "${value}" is not one of its values — it takes a number of spaces, tab, under-name, under-name + 2 or under-text`;
87
+ },
88
+ },
89
+ {
90
+ name: 'clause-order',
91
+ about: 'The order a statement\'s clauses are written in after its text, which always\ncomes straight after the name. A list names placement and keys such as style;\nwhat it leaves out follows, in the order written.',
92
+ values: [
93
+ ['as-written', 'the order the writer chose'],
94
+ ['a list', 'that order, such as: placement, style, fill'],
95
+ ],
96
+ written: 'as-written',
97
+ read: (value) => {
98
+ if (value === 'as-written')
99
+ return value;
100
+ const kinds = value.split(',').map((kind) => kind.trim());
101
+ const seen = new Set();
102
+ for (const kind of kinds) {
103
+ if (kind === '')
104
+ throw `clause-order: "${value}" has an empty place in its list — name a kind of clause between each comma`;
105
+ if (!CLAUSE_KINDS.includes(kind)) {
106
+ const near = nearMatch(kind, CLAUSE_KINDS);
107
+ throw `clause-order: "${kind}" is not a kind of clause${near ? ` — did you mean ${near}?` : ''} — it takes text, placement and keys such as style or fill`;
108
+ }
109
+ if (seen.has(kind))
110
+ throw `clause-order: ${kind} is listed twice — keep one`;
111
+ seen.add(kind);
112
+ }
113
+ return kinds;
114
+ },
115
+ },
116
+ {
117
+ name: 'clause-gap',
118
+ about: 'The least space between two clauses on a line.',
119
+ values: [
120
+ ['2', 'that many spaces, such as 2'],
121
+ ['tab', 'one tab, which only works with columns: off'],
122
+ ],
123
+ written: '2',
124
+ read: (value) => {
125
+ if (SETTING_COUNT.test(value))
126
+ return Number(value);
127
+ if (value === 'tab')
128
+ return 'tab';
129
+ throw `clause-gap: "${value}" is not one of its values — it takes a number of spaces, or tab`;
130
+ },
131
+ },
132
+ wordSetting('columns', 'Whether statements that belong together line their clauses up in columns.', [
133
+ ['aligned', 'each column as wide as its longest clause'],
134
+ ['off', 'each clause straight after the one before, a clause gap apart'],
135
+ ]),
136
+ wordSetting('columns-shared-by', 'Which statements line their clauses up in shared columns. A comment line\nbetween them never ends a set.', [
137
+ ['same-keyword-neighbors', 'adjacent, with the same keyword\n(a blank line or new keyword ends the set)'],
138
+ ['neighbors', 'adjacent, whatever their keyword'],
139
+ ['same-keyword', 'the same keyword, across blank lines'],
140
+ ['file', 'the whole file'],
141
+ ]),
142
+ wordSetting('comments-inside', 'What happens to a statement written over several lines, with comments\namong them.', [
143
+ ['keep-lines', 'it stays on its own lines, with the indent tidied'],
144
+ ['move-above', 'the comments move above it, and it is joined onto one line'],
145
+ ['refuse', 'the file is left alone, and the comment in the way is named'],
146
+ ]),
147
+ wordSetting('trailing-comments', 'Where a comment at the end of a statement\'s line goes.', [
148
+ ['shared-column', 'lined up with the other comments in its set'],
149
+ ['after-statement', 'a clause gap after its own statement'],
150
+ ]),
151
+ {
152
+ name: 'blank-lines',
153
+ about: 'What happens to runs of blank lines. A run is never removed, since a blank\nline starts a new set of columns.',
154
+ values: [
155
+ ['as-written', 'every blank line stays'],
156
+ ['exactly 2', 'every run becomes that many'],
157
+ ['at-most 3', 'longer runs shrink to that many; shorter ones stay'],
158
+ ],
159
+ written: 'as-written',
160
+ read: (value) => {
161
+ if (value === 'as-written')
162
+ return value;
163
+ const counted = /^(exactly|at-most) ([1-9]\d*)$/.exec(value);
164
+ if (counted)
165
+ return counted[1] === 'exactly' ? { exactly: Number(counted[2]) } : { atMost: Number(counted[2]) };
166
+ throw `blank-lines: "${value}" is not one of its values — it takes as-written, exactly and a number, or at-most and a number`;
167
+ },
168
+ },
169
+ wordSetting('edge-name-and-ends', 'How an edge\'s name and ends, such as one a -> b, line up.', [
170
+ ['one-column', 'together, as one column, the way a node\'s name is'],
171
+ ['own-columns', 'the name, each end and the arrow in a column each'],
172
+ ]),
173
+ ];
174
+ const SETTING_BY_NAME = new Map(SETTINGS.map((setting) => [setting.name, setting]));
175
+ export const DEFAULT_FORMAT = Object.fromEntries(SETTINGS.map((setting) => [setting.name, setting.read(setting.written)]));
176
+ /** The file a project's settings are written in. */
177
+ export const SETTINGS_FILE = '.reladraw-format';
178
+ /**
179
+ * Read a `.reladraw-format` file: one `key: value` setting a line, `//`
180
+ * comments, blank lines. An unknown setting, one written twice, or a value it
181
+ * doesn't take is refused by name, with the line.
182
+ */
183
+ export function readFormatSettings(text) {
184
+ const settings = { ...DEFAULT_FORMAT };
185
+ const seen = new Map();
186
+ text.split(/\r?\n/).forEach((raw, index) => {
187
+ const line = index + 1;
188
+ const written = raw.replace(/\/\/.*$/, '').trim();
189
+ if (written === '')
190
+ return;
191
+ const colon = written.indexOf(':');
192
+ if (colon === -1)
193
+ throw new SourceError(`"${written}" is not a setting — write one as name: value, such as columns: aligned`, line);
194
+ const name = written.slice(0, colon).trim();
195
+ const value = written.slice(colon + 1).trim().replace(/\s+/g, ' ');
196
+ const setting = SETTING_BY_NAME.get(name);
197
+ if (setting === undefined) {
198
+ const near = nearMatch(name, SETTINGS.map((one) => one.name));
199
+ throw new SourceError(`there is no setting "${name}"` + (near ? ` — did you mean ${near}?` : ` — the settings are ${orList(SETTINGS.map((one) => one.name))}`), line);
200
+ }
201
+ const before = seen.get(name);
202
+ if (before !== undefined)
203
+ throw new SourceError(`${name} is set twice, here and on line ${before} — keep one`, line);
204
+ seen.set(name, line);
205
+ if (value === '')
206
+ throw new SourceError(`${name} has no value — it takes ${orList(setting.values.map(([word]) => word))}`, line);
207
+ try {
208
+ settings[name] = setting.read(value);
209
+ }
210
+ catch (reason) {
211
+ if (typeof reason === 'string')
212
+ throw new SourceError(reason, line);
213
+ throw reason;
214
+ }
215
+ });
216
+ if (settings['clause-gap'] === 'tab' && settings.columns === 'aligned') {
217
+ throw new SourceError(`clause-gap: tab can't line columns up, since how wide a tab is depends on the editor — use a number of spaces, or columns: off`, seen.get('clause-gap') ?? seen.get('columns') ?? 1);
218
+ }
219
+ return settings;
220
+ }
221
+ /**
222
+ * The `.reladraw-format` that `reladraw format --init` writes: every setting
223
+ * at its default, each under a comment saying what it controls and what each
224
+ * of its values means.
225
+ */
226
+ export function settingsFileText() {
227
+ const blocks = SETTINGS.map((setting) => {
228
+ const width = Math.max(...setting.values.map(([word]) => word.length));
229
+ // A meaning written over two lines carries on under where it started.
230
+ const values = setting.values.flatMap(([word, meaning], index) => {
231
+ const [first, ...rest] = meaning.split('\n');
232
+ return [
233
+ `// ${word.padEnd(width)} ${first}${index === 0 ? ' (default)' : ''}`,
234
+ ...rest.map((more) => `// ${' '.repeat(width)} ${more}`),
235
+ ];
236
+ });
237
+ const about = setting.about.split('\n').map((line) => `// ${line}`);
238
+ return [...about, ...values, `${setting.name}: ${setting.written}`].join('\n');
239
+ });
240
+ const header = [
241
+ '// .reladraw-format: how `reladraw format` lays out the .reladraw files in this',
242
+ '// folder and every folder below it, unless a nearer one says otherwise.',
243
+ '// Every setting is written out at its default; change the ones you want, or',
244
+ '// delete a line to use the default.',
245
+ ].join('\n');
246
+ return `${header}\n\n${blocks.join('\n\n')}\n`;
247
+ }
248
+ function orList(words) {
249
+ return words.length < 2 ? words.join('') : `${words.slice(0, -1).join(', ')} or ${words[words.length - 1]}`;
250
+ }
@@ -0,0 +1,14 @@
1
+ import { type FormatSettings } from './format-settings.js';
2
+ /**
3
+ * Lay a source file out again: spacing between clauses, columns shared by
4
+ * statements that belong together, comments and blank lines, as `settings`
5
+ * say. Only whitespace changes, and the order of clauses when `clause-order`
6
+ * asks. Every token is copied out of the line it was
7
+ * written on, so no word is respelled, and the result is parsed again and
8
+ * compared with the original, so a mistake here can never change what a file
9
+ * draws: it throws instead.
10
+ *
11
+ * Throws the parser's error for a file that does not parse, so a broken file
12
+ * is never rewritten.
13
+ */
14
+ export declare function format(source: string, settings?: FormatSettings): string;