reladraw 0.16.0 → 0.17.1

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/dist/ast.js CHANGED
@@ -65,6 +65,23 @@ export const POSITIONS = [
65
65
  export function isPosition(word) {
66
66
  return POSITIONS.includes(word);
67
67
  }
68
+ /**
69
+ * For a missing name like `bin.left`, whose last part is a side or a point:
70
+ * the hint that the part is a separate word after the box's full name,
71
+ * `host.bin left`. `names` is every box's name; empty when there is no such box.
72
+ */
73
+ export function partWordHint(name, names) {
74
+ const dot = name.lastIndexOf('.');
75
+ if (dot < 0)
76
+ return '';
77
+ const part = name.slice(dot + 1);
78
+ if (!isPosition(part) && !PART_SIDES.includes(part) && part !== 'text')
79
+ return '';
80
+ const base = name.slice(0, dot);
81
+ const all = [...names];
82
+ const found = all.includes(base) ? [base] : all.filter((each) => each.endsWith(`.${base}`));
83
+ return found.length === 1 ? ` — a side or point is a separate word after the box's full name: ${found[0]} ${part}` : '';
84
+ }
68
85
  /**
69
86
  * A part of a node that a placement may name: its text, one of its four sides,
70
87
  * or one of its nine points. Written as a separate word after the node's name
@@ -124,6 +141,25 @@ export const OPPOSITE = {
124
141
  };
125
142
  /** The parts `inside` and `outside` can be read from — everything on the boundary. */
126
143
  export const BOUNDARY_PARTS = Object.keys(INWARD);
144
+ /** The axis a fraction measures along. */
145
+ export function fractionAxis(fraction) {
146
+ return fraction.along === 'across' ? 'x' : 'y';
147
+ }
148
+ /** The words that start a line from a thing, in `down from a and left from b`. */
149
+ export const WAYS = ['up', 'down', 'left', 'right'];
150
+ /** A point written out, as the author would write it. */
151
+ export function describePoint(point) {
152
+ if (point.kind === 'between') {
153
+ const at = point.atWritten === undefined ? '' : ` at ${point.atWritten}`;
154
+ const span = point.span ? ' (span: both)' : '';
155
+ return `between ${nameTarget(point.a)} and ${nameTarget(point.b)}${at}${span}`;
156
+ }
157
+ if (point.kind === 'from') {
158
+ return point.lines.map((line) => `${line.way === 'even' ? 'even with' : `${line.way} from`} ${nameTarget(line.target)}`).join(' and ');
159
+ }
160
+ const of = /left|right/.test(point.direction) ? ' of' : '';
161
+ return `${point.distance} ${point.direction}${of} ${nameTarget(point.from)}`;
162
+ }
127
163
  /**
128
164
  * The modifiers a placement understands, in brackets after its targets. Refused
129
165
  * by name when unrecognized, for the reason `DIAGRAM_KEYS` are: a modifier that
@@ -149,6 +185,17 @@ export const PLACEMENT_KEYS = ['gap'];
149
185
  * range against each other.
150
186
  */
151
187
  export const CONTENTS_KEYS = ['widths', 'align'];
188
+ /**
189
+ * What `layout: column (…)` and `layout: row (…)` take in their bracket.
190
+ * `align` is read only to be refused by name: it now means where along a side,
191
+ * and lining the children up is `align-children`.
192
+ */
193
+ export const LAYOUT_KEYS = ['gap', 'widths', 'align-children', 'align', 'centers', 'heights'];
194
+ /**
195
+ * Where `gather-children:` puts a container's children in its spare room: a
196
+ * side, a corner, or the middle.
197
+ */
198
+ export const GATHER_PLACES = ['center', 'left', 'right', 'top', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
152
199
  /**
153
200
  * What `widths:` may say. `natural` is the default and is today's behavior.
154
201
  * `match` is what `align: widths` did — every child as wide as the widest.
@@ -157,12 +204,11 @@ export const CONTENTS_KEYS = ['widths', 'align'];
157
204
  */
158
205
  export const CONTENT_WIDTHS = ['natural', 'match', 'fill'];
159
206
  /**
160
- * Where the block of contents sits when it is narrower than the band. `right`
161
- * is accepted although no diagram has yet wanted it: refusing it would give
162
- * `align:` a different value set depending on which bracket it is in, which is
163
- * the divergence this scheme exists to remove.
207
+ * How `align-children:` lines laid-out children up with each other: in a
208
+ * column their left edges, middles or right edges; in a row their tops,
209
+ * middles or bottoms.
164
210
  */
165
- export const CONTENT_ALIGNMENTS = ['left', 'center', 'right'];
211
+ export const CHILD_ALIGNMENTS = { column: ['left', 'center', 'right'], row: ['top', 'center', 'bottom'] };
166
212
  /** How a placement reads back in the author's own words, for error messages. */
167
213
  export function describePlacement(placement) {
168
214
  const targets = listTargets(placement.targets);
@@ -170,16 +216,28 @@ export function describePlacement(placement) {
170
216
  return `on ${targets}`;
171
217
  }
172
218
  if (placement.kind === 'align') {
173
- const side = placement.side === 'center' ? '' : `${placement.side} `;
174
- return `${side}level with ${targets}`;
219
+ const side = placement.side !== 'center' ? `${placement.side} ` : placement.axis === 'x' ? 'x ' : '';
220
+ return `${side}even with ${targets}`;
175
221
  }
176
- const gap = placement.gap === undefined ? '' : ` (gap: ${placement.gap})`;
177
222
  // `inside` and `outside` take no `of`, and are quoted back as the author
178
223
  // wrote them — the derived direction is what they *mean*, not what they say.
179
- if (placement.written)
224
+ if (placement.written) {
225
+ const gap = placement.gap === undefined ? '' : ` (gap: ${placement.gap})`;
180
226
  return `${placement.written} ${targets}${gap}`;
227
+ }
181
228
  // "left of X" and "above X" are both good English; "above of X" is not.
182
229
  const joiner = placement.direction === 'above' || placement.direction === 'below' ? '' : 'of ';
230
+ const own = placement.own === undefined ? '' : `${placement.own} `;
231
+ if (placement.gaps) {
232
+ const y = placement.direction.startsWith('above') ? 'above' : 'below';
233
+ const x = placement.direction.endsWith('left') ? 'left' : 'right';
234
+ return `${own}${placement.gaps.y} ${y} and ${placement.gaps.x} ${x} of ${targets}`;
235
+ }
236
+ // A distance in front, and one in brackets, are the same thing; quoted in front when the part form needs it.
237
+ if (placement.own !== undefined && placement.gap !== undefined) {
238
+ return `${own}${placement.gap} ${placement.direction} ${joiner}${targets}`;
239
+ }
240
+ const gap = placement.gap === undefined ? '' : ` (gap: ${placement.gap})`;
183
241
  return `${placement.direction} ${joiner}${targets}${gap}`;
184
242
  }
185
243
  /** "borg", "hub text", "borg and bare" — as the author would write them. */
@@ -191,7 +249,12 @@ export function listTargets(targets) {
191
249
  }
192
250
  /** One target in the author's words: the node's name, and its part if it named one. */
193
251
  export function nameTarget(target) {
194
- return target.part === undefined ? target.name : `${target.name} ${target.part}`;
252
+ if (target.point)
253
+ return describePoint(target.point);
254
+ if (target.fraction?.first)
255
+ return `${target.fraction.written} ${target.name}`;
256
+ const named = target.part === undefined ? target.name : `${target.name} ${target.part}`;
257
+ return target.fraction ? `${named} ${target.fraction.written}` : named;
195
258
  }
196
259
  /** How the axis of a passage is written, and what it means. */
197
260
  export const PASSAGE_AXES = {
@@ -228,13 +291,15 @@ export const TEXT_KEYS = ['color', 'size', 'wrap', 'align', 'at'];
228
291
  * it goes in one bracket, as the text's do. `line: red` stays as the short form
229
292
  * of `line: (color: red)`, and both are stored under the bare `line` key.
230
293
  */
231
- export const LINE_KEYS = ['color', 'path', 'corners', 'crossing', 'pattern', 'thickness'];
294
+ export const LINE_KEYS = ['color', 'path', 'corners', 'crossing', 'pattern', 'thickness', 'curve', 'turn', 'step-out'];
232
295
  /**
233
296
  * The words each of the line's properties takes, the default first. Thickness
234
- * takes a plain number of pixels as well.
297
+ * takes a multiple of normal (`2`) or pixels (`3px`) as well. `turn:` takes a fraction and
298
+ * `step-out:` a distance, so neither has a list.
235
299
  */
236
300
  export const LINE_VALUES = {
237
- path: ['curved', 'square', 'straight'],
301
+ path: ['straight', 'square', 'curved'],
302
+ curve: ['normal', 'strong', 'subtle'],
238
303
  corners: ['sharp', 'rounded'],
239
304
  crossing: ['gap', 'none', 'arc', 'square'],
240
305
  pattern: ['solid', 'dashed', 'dotted', 'dash-dot'],
@@ -256,29 +321,60 @@ export const MARK_GLYPHS = {
256
321
  from: { '<': 'arrow', '<|': 'oarrow', '*': 'dot', o: 'odot', '|': 'bar' },
257
322
  to: { '>': 'arrow', '|>': 'oarrow', '*': 'dot', o: 'odot', '|': 'bar' },
258
323
  };
324
+ /** A stop written as the author would write it. */
325
+ export function describeStop(stop) {
326
+ return stop.written;
327
+ }
328
+ /** What a bundle may say besides `via:`, `layout:` and `order:`. */
329
+ export const BUNDLE_KEYS = ['via', 'layout', 'order', 'line', 'from-mark', 'to-mark', 'from', 'to'];
330
+ /** What a point and a region may say. A region's padding pushes every edge out. */
331
+ export const POINT_KEYS = ['style', 'fill', 'depth'];
332
+ /** Which way from its dot a point's text sits (design.org 7.4); the first is the default. */
333
+ export const POINT_TEXT_SIDES = ['above', 'below', 'left', 'right', 'above-right', 'above-left', 'below-right', 'below-left'];
334
+ /** What a point's or a region's text takes in its bracket besides its position words. */
335
+ export const MARK_TEXT_KEYS = ['color', 'size', 'wrap', 'align', 'reads', 'gap'];
336
+ export const REGION_KEYS = ['style', 'fill', 'opacity', 'border', 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'depth'];
259
337
  /**
260
338
  * The attributes a `diagram` statement understands. `text` takes a bracket,
261
339
  * `text: (color: …)`, and sets the text color of everything at once — the
262
340
  * theme has one text color, shared by nodes and edges, and this is that.
341
+ * `checks` takes a bracket too, `checks: (edge-crossings: on)`, turning kinds
342
+ * of finding on or off by name.
263
343
  */
264
- export const DIAGRAM_KEYS = ['theme', 'background', 'text'];
265
- export const DEFAULT_TARGETS = ['node', 'leaf', 'container', 'edge'];
344
+ export const DIAGRAM_KEYS = ['theme', 'background', 'text', 'checks', 'guides'];
345
+ export const DEFAULT_TARGETS = [
346
+ 'node',
347
+ 'leaf',
348
+ 'container',
349
+ 'edge',
350
+ 'guide node',
351
+ 'guide edge',
352
+ 'guide point',
353
+ 'guide region',
354
+ ];
266
355
  /**
267
356
  * What each default may say. A default carries a style's vocabulary, less the
268
357
  * words that would contradict the kind it is written for:
269
358
  *
270
359
  * - A default names its kind, so it is strict where a style is permissive —
271
360
  * `default edge fill:` can only be a mistake.
272
- * - `badge:` gives a box a child, which makes it a container, so every leaf
273
- * given one by `default leaf` or `default node` would stop being a leaf.
274
361
  * - `icon:` draws a picture, and a picture cannot hold children, so it is a
275
362
  * leaf's word and not a container's or every node's.
363
+ * - `heading:` names one node's own child, so it belongs to that node alone.
364
+ *
365
+ * `badge:` is every kind's: it makes the picture part of the label, a heading,
366
+ * and a heading doesn't make a leaf a container.
276
367
  */
277
368
  export const DEFAULT_KEYS = {
278
- node: ['style', 'shape', 'fill', 'border', 'text'],
279
- leaf: ['style', 'shape', 'icon', 'fill', 'border', 'text'],
280
- container: ['style', 'shape', 'badge', 'fill', 'border', 'text'],
369
+ node: ['style', 'shape', 'badge', 'fill', 'border', 'corners', 'text'],
370
+ leaf: ['style', 'shape', 'icon', 'badge', 'fill', 'border', 'corners', 'text'],
371
+ container: ['style', 'shape', 'badge', 'fill', 'border', 'corners', 'text'],
281
372
  edge: ['style', 'line', 'text'],
373
+ // A shown guide's look, one line per kind (design.org 8.2).
374
+ 'guide node': ['style', 'fill', 'border', 'corners', 'text'],
375
+ 'guide edge': ['style', 'line', 'text'],
376
+ 'guide point': ['style', 'fill'],
377
+ 'guide region': ['style', 'fill', 'border'],
282
378
  };
283
379
  /**
284
380
  * The attributes whose value is a color rather than text. A color is written
@@ -335,8 +431,8 @@ export const COLOR_PARTS = {
335
431
  * The exclusions are the whole of what this table decides, and each is a place
336
432
  * the old silence hid something:
337
433
  *
338
- * - A node drawn as a picture, or with no body at all, takes no `fill:` or
339
- * `border:`. There is no outline for either to reach.
434
+ * - A node drawn as a picture, or with no body at all, takes no `fill:`,
435
+ * `border:` or `corners:`. There is no outline for any of them to reach.
340
436
  * - Neither of those takes `contents:` either, which says how a node's children
341
437
  * sit, and neither may have any.
342
438
  * - `shape:` and `icon:` each name the body, so each appears only on the kind it
@@ -345,12 +441,27 @@ export const COLOR_PARTS = {
345
441
  * - An edge takes no `gap:` or `overlap:`. Those are about where a box sits, and
346
442
  * an edge is not placed — it joins two things that are.
347
443
  */
444
+ /** The keys that state a size; `size.ts` reads their values. */
445
+ export const SIZE_KEY_NAMES = ['width', 'height', 'min-width', 'max-width', 'min-height', 'max-height'];
446
+ /** `padding-right: 4`: one side's padding, over `padding:` on that side. */
447
+ export const PADDING_SIDES = ['top', 'right', 'bottom', 'left'];
448
+ export const PADDING_SIDE_KEYS = PADDING_SIDES.map((side) => `padding-${side}`);
449
+ /** `right-edge-order: ae, ad, ac`: the lines on one side of a box, in reading order. */
450
+ export const EDGE_ORDER_KEYS = ['top-edge-order', 'bottom-edge-order', 'left-edge-order', 'right-edge-order'];
348
451
  export const ATTR_KEYS = {
349
- shape: ['style', 'gap', 'overlap', 'contents', 'badge', 'deck', 'shape', 'fill', 'border', 'text', 'url'],
350
- icon: ['style', 'gap', 'overlap', 'icon', 'text', 'url'],
351
- none: ['style', 'gap', 'overlap', 'shape', 'text', 'url'],
352
- edge: ['style', 'from', 'to', 'from-mark', 'to-mark', 'line', 'text', 'url'],
452
+ shape: ['style', 'overlap', 'layout', 'gather-children', 'padding', ...PADDING_SIDE_KEYS, ...SIZE_KEY_NAMES, 'heading', 'join-with', 'badge', 'cards', 'shape', 'fill', 'border', 'corners', 'text', 'url', 'depth', ...EDGE_ORDER_KEYS],
453
+ icon: ['style', 'overlap', 'icon', 'join-with', 'text', 'url', 'depth', ...EDGE_ORDER_KEYS],
454
+ none: ['style', 'overlap', 'layout', 'gather-children', 'padding', ...SIZE_KEY_NAMES, 'heading', 'join-with', 'shape', 'text', 'url', 'depth', ...EDGE_ORDER_KEYS],
455
+ edge: ['style', 'from', 'to', 'via', 'from-mark', 'to-mark', 'line', 'text', 'url', 'depth'],
353
456
  };
457
+ /**
458
+ * What a node's `corners:` takes, the default first: a box is drawn rounded
459
+ * unless it says otherwise. The same word and values as a line's `corners:`,
460
+ * whose default is the other one. It applies to every outline with corners — a
461
+ * rectangle and a document, whose fold stays as it is — and changes only the
462
+ * drawing, never where anything sits.
463
+ */
464
+ export const NODE_CORNERS = ['rounded', 'sharp'];
354
465
  /**
355
466
  * Every word that is an attribute *somewhere*, which is what separates a
356
467
  * misspelling from a key written on the wrong kind of thing. The two deserve
@@ -362,5 +473,5 @@ export const ATTR_KEYS = {
362
473
  * not told the word does not exist.
363
474
  */
364
475
  export const ALL_ATTR_KEYS = [
365
- ...new Set([...Object.values(ATTR_KEYS).flat(), ...DIAGRAM_KEYS]),
476
+ ...new Set([...Object.values(ATTR_KEYS).flat(), ...DIAGRAM_KEYS, ...REGION_KEYS]),
366
477
  ];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Findings: things about a drawn picture that may look wrong. An error means
3
+ * the file can't be drawn; a finding means "look at this". A render with
4
+ * findings succeeds.
5
+ *
6
+ * Every finding is measured off the solved layout, never fixed by moving
7
+ * anything: the picture is drawn as written, and this says where it may not
8
+ * read as the writer meant.
9
+ */
10
+ import { type Layout } from './model.js';
11
+ /**
12
+ * The kinds of finding, each with whether it is on when nothing says. Named in
13
+ * the language's own nouns, since the name is what a writer types to turn one
14
+ * off. Crossings are often fine, so they are off; the rest are almost always a
15
+ * fault, so an agent that never thinks to ask still hears about them.
16
+ */
17
+ export declare const CHECK_DEFAULTS: {
18
+ readonly 'text-overlap': true;
19
+ readonly 'text-overflow': true;
20
+ readonly 'edge-node-overlap': true;
21
+ readonly 'edge-overlap': true;
22
+ readonly 'crowded-edge-ends': true;
23
+ readonly 'node-overlap': true;
24
+ readonly 'edge-no-length': true;
25
+ readonly 'edge-crossings': false;
26
+ };
27
+ export type CheckKind = keyof typeof CHECK_DEFAULTS;
28
+ export declare const CHECK_KINDS: CheckKind[];
29
+ /** Which kinds run. */
30
+ export type Checks = Record<CheckKind, boolean>;
31
+ export declare function isCheckKind(word: string): word is CheckKind;
32
+ /** One finding: the statement at fault, the kind, and what happened. */
33
+ export declare class Finding {
34
+ readonly kind: CheckKind;
35
+ readonly message: string;
36
+ readonly line: number;
37
+ constructor(kind: CheckKind, message: string, line: number);
38
+ /** `diagram.reladraw:12: warning(node-overlap): …` — the form compilers and editors already read. */
39
+ format(file?: string): string;
40
+ }
41
+ /**
42
+ * `edge-crossings: on, crowded-edge-ends: off` — the inside of a `checks:`
43
+ * bracket, as `--checks` takes it, laid over `base`. A kind not mentioned
44
+ * keeps what `base` says.
45
+ */
46
+ export declare function readChecks(text: string, base: Checks): Checks;
47
+ /**
48
+ * The checks a layout asks for: the defaults, then the file's
49
+ * `diagram checks: (…)`, which arrives flattened as `checks.<kind>` and was
50
+ * refused by the parser if it named no kind.
51
+ */
52
+ export declare function fileChecks(layout: Layout): Checks;
53
+ /** One `kind: on|off`, refused by name if either word is wrong. */
54
+ export declare function setCheck(checks: Checks, kind: string, value: string, fail: (message: string) => Error): void;
55
+ /** Every finding the layout has, for the kinds that are on, in line order. */
56
+ export declare function findings(layout: Layout, checks: Checks): Finding[];
57
+ /** The one word in `words` a slip of a letter or two away from `word`, if there is one. */
58
+ export declare function nearMatch(word: string, words: readonly string[]): string | undefined;