reladraw 0.19.1 → 0.21.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/dist/parser.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BUNDLE_KEYS, COLOR_KEYS, POINT_KEYS, REGION_KEYS, WAYS, describePoint, fractionAxis, DEFAULT_KEYS, DEFAULT_TARGETS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, TEXT_KEYS, MARK_TEXT_KEYS, POINT_TEXT_SIDES, LINE_KEYS, LINE_VALUES, CONTENTS_KEYS, EDGE_ORDER_KEYS, LAYOUT_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, MARKS, MARK_GLYPHS, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
1
+ import { BUNDLE_KEYS, COLOR_KEYS, POINT_KEYS, REGION_KEYS, WAYS, describePoint, fractionAxis, DEFAULT_KEYS, DEFAULT_TARGETS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, TEXT_KEYS, MARK_TEXT_KEYS, POINT_TEXT_SIDES, LINE_KEYS, LINE_VALUES, BORDER_KEYS, CONTENTS_KEYS, EDGE_ORDER_KEYS, LAYOUT_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, MARKS, MARK_GLYPHS, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
2
2
  import { CHECK_DEFAULTS, CHECK_KINDS, setCheck } from './checks.js';
3
3
  import { SourceError } from './errors.js';
4
4
  import { STATEMENT_KEYWORDS } from './grammar.js';
@@ -309,6 +309,7 @@ const BRACKET_KEYS = {
309
309
  text: TEXT_KEYS,
310
310
  contents: CONTENTS_KEYS,
311
311
  line: LINE_KEYS,
312
+ border: BORDER_KEYS,
312
313
  checks: CHECK_KINDS,
313
314
  };
314
315
  /** How each bracketed key's error quotes itself back, and what it is about. */
@@ -316,6 +317,7 @@ const BRACKET_ABOUT = {
316
317
  text: { kind: 'a text', example: 'color: theme-muted' },
317
318
  contents: { kind: 'a `contents:` bracket', example: 'widths: match' },
318
319
  line: { kind: 'a line', example: 'path: square' },
320
+ border: { kind: 'a border', example: 'thickness: thick' },
319
321
  checks: { kind: 'a `checks:` bracket', example: 'edge-crossings: on' },
320
322
  };
321
323
  /**
@@ -702,7 +704,8 @@ function readAttr(tokens, at, attrs, line, subject) {
702
704
  // A line's color is stored where `line: red` puts it, so the short form
703
705
  // and the bracket are one key: a style's `line: red` merges under an
704
706
  // edge's `line: (color: blue)`, and writing both on one line is a repeat.
705
- const stored = key === 'line' && inner === 'color' ? 'line' : `${key}.${inner}`;
707
+ // A border's color the same.
708
+ const stored = (key === 'line' || key === 'border') && inner === 'color' ? key : `${key}.${inner}`;
706
709
  setOnce(attrs, stored, value, subject, line, { key: `${key}: (${inner}: …)`, value: (v) => v });
707
710
  }
708
711
  return read.next;
@@ -720,6 +723,16 @@ function readAttr(tokens, at, attrs, line, subject) {
720
723
  setOnce(attrs, key, value.text, subject, line, { key, value: quoteOf });
721
724
  return at + 2;
722
725
  }
726
+ if (key === 'title') {
727
+ // A title is a text, and every text is quoted. Unquoted, its first word
728
+ // would be the title and the rest read as stray words.
729
+ const value = tokens[at + 1];
730
+ if (!value || !value.quoted) {
731
+ throw new SourceError(`${subject}: a title is written in quotes — \`title: "How a login reaches the database"\``, line);
732
+ }
733
+ setOnce(attrs, key, value.text, subject, line, { key, value: quoteOf });
734
+ return at + 2;
735
+ }
723
736
  if (key === 'style') {
724
737
  // `style: base, critical and alarm` — several bundles, applied in the order
725
738
  // written, a later one winning where two set the same key. The list reads
@@ -768,7 +781,7 @@ function readAttr(tokens, at, attrs, line, subject) {
768
781
  });
769
782
  return next;
770
783
  }
771
- if (bracketKeys !== undefined && key !== 'text' && key !== 'line') {
784
+ if (bracketKeys !== undefined && key !== 'text' && key !== 'line' && key !== 'border') {
772
785
  // `contents: match` names the part and then says one of its two properties
773
786
  // without saying which. The brackets are what make the level shift visible,
774
787
  // so there is no unbracketed spelling to fall back to.
@@ -821,6 +834,11 @@ function readAttr(tokens, at, attrs, line, subject) {
821
834
  `\`line: (${property}: ${valueToken.text})\``, line);
822
835
  }
823
836
  }
837
+ if (key === 'border' && !valueToken.quoted && /^(thin|normal|thick|\d*\.?\d+(px)?)$/.test(valueToken.text)) {
838
+ // `border: red` is the short form of the color, as `line: red` is.
839
+ throw new SourceError(`\`border: ${valueToken.text}\` — \`border:\` on its own takes a color. Its thickness goes in its bracket: ` +
840
+ `\`border: (thickness: ${valueToken.text})\``, line);
841
+ }
824
842
  if (key === 'align' && valueToken.text === 'widths') {
825
843
  // Removed in 0.3.0. It was a size operation wearing an alignment's name,
826
844
  // and its value set had one member — a flag in a property's clothes. Its
@@ -1238,7 +1256,15 @@ function parseNode(head, line) {
1238
1256
  listed: NODE_TEXT_LISTED,
1239
1257
  });
1240
1258
  refuseNodeReads(bracket.values['reads'], subject, line);
1241
- const tail = parseTail(head, bracket.next, line, subject);
1259
+ const limits = [];
1260
+ const tail = parseTail(head, bracket.next, line, subject, (tokens, index) => {
1261
+ const word = tokens[index];
1262
+ if (word.quoted || word.text !== 'stays')
1263
+ return undefined;
1264
+ const read = readLimit(tokens, index, line, subject);
1265
+ limits.push(read.limit);
1266
+ return read.next;
1267
+ });
1242
1268
  refuseTextKey(tail.attrs, subject, 'after the name', line);
1243
1269
  return {
1244
1270
  kind: 'node',
@@ -1248,10 +1274,43 @@ function parseNode(head, line) {
1248
1274
  textAttrs: place === undefined ? bracket.values : { ...bracket.values, place },
1249
1275
  ...(titlePlacements.length > 0 ? { titlePlacements } : {}),
1250
1276
  placements: tail.placements,
1277
+ ...(limits.length > 0 ? { limits } : {}),
1251
1278
  attrs: tail.attrs,
1252
1279
  line,
1253
1280
  };
1254
1281
  }
1282
+ /**
1283
+ * `stays above laptop.photos (gap: wide)`: a limit (design.org 5.6). The words
1284
+ * after `stays` are read as a placement would be, then held to one straight
1285
+ * way and one target, since a limit says which side of one thing to keep to.
1286
+ */
1287
+ function readLimit(tokens, at, line, subject) {
1288
+ const way = tokens[at + 1];
1289
+ const straight = ['above', 'below', 'left', 'right'];
1290
+ if (!way || way.quoted || !straight.includes(way.text)) {
1291
+ const diagonal = way !== undefined && !way.quoted && isDirection(way.text);
1292
+ throw new SourceError(diagonal
1293
+ ? `${subject}: "stays ${way.text}" keeps to two sides at once — write one limit for each, such as \`stays above a stays right of a\``
1294
+ : `${subject}: "stays" is followed by the way it keeps to — stays above, stays below, stays left of or stays right of`, line);
1295
+ }
1296
+ const read = readPlacement(tokens, at + 1, line, subject, true);
1297
+ const placement = read.placement;
1298
+ if (placement.kind !== 'offset' || placement.own !== undefined || placement.gaps !== undefined) {
1299
+ throw new SourceError(`${subject}: "stays" names a way and one thing, such as \`stays above laptop.photos\``, line);
1300
+ }
1301
+ if (placement.targets.length !== 1) {
1302
+ throw new SourceError(`${subject}: "${describePlacement(placement)}" after "stays" names ${placement.targets.length} things — a limit keeps clear of one; write one for each`, line);
1303
+ }
1304
+ return {
1305
+ limit: {
1306
+ way: way.text,
1307
+ target: placement.targets[0],
1308
+ ...(placement.gap !== undefined ? { gap: placement.gap } : {}),
1309
+ line,
1310
+ },
1311
+ next: read.next,
1312
+ };
1313
+ }
1255
1314
  /**
1256
1315
  * A node's text takes the look keys and `reads:` (design.org 7.2). `at` is
1257
1316
  * read only to be refused with the side form, so it isn't listed as one it takes.
@@ -2394,6 +2453,12 @@ function checkColorWord(value, subject, line) {
2394
2453
  if (value.startsWith('theme-') && !THEME_COLORS.includes(value)) {
2395
2454
  throw new SourceError(`${subject}: there is no theme color "${value}" — the theme colors are ${THEME_COLORS.join(', ')}`, line);
2396
2455
  }
2456
+ // Hex digits without their `#` are no CSS color, so the viewer would draw nothing.
2457
+ if (/^([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value)) {
2458
+ // `bad` and `face` are spelled in hex digits too, so without a digit it is only a guess.
2459
+ const fix = /\d/.test(value) ? 'a hex color is written' : 'if it is a hex color, write';
2460
+ throw new SourceError(`${subject}: \`${value}\` is not a color — ${fix} \`#${value}\``, line);
2461
+ }
2397
2462
  }
2398
2463
  function follows(tokens, at, word) {
2399
2464
  const token = tokens[at];
package/dist/place.d.ts CHANGED
@@ -130,6 +130,8 @@ export declare class Placer {
130
130
  private readonly sizes;
131
131
  private readonly frames;
132
132
  private readonly outlines;
133
+ /** A container's box while a limit tries it out (`roomFor`), in place of its final one. */
134
+ private readonly trials;
133
135
  private readonly working;
134
136
  /** What is being worked out, innermost last, for reporting a circle. */
135
137
  private readonly trail;
@@ -151,6 +153,8 @@ export declare class Placer {
151
153
  private readonly origins;
152
154
  /** Containers whose anchor is being placed on the page, so a box outside them is measured on the page rather than in their frame. */
153
155
  private readonly onPage;
156
+ /** Boxes that anchor their frame on one axis only, when no box in it says nothing at all (`splitAnchors`). */
157
+ private readonly axisAnchors;
154
158
  /** Named points and regions being worked out, innermost last, to refuse one defined through itself. */
155
159
  private readonly expanding;
156
160
  constructor(roots: LayoutNode[], byName: Map<string, LayoutNode>, facts: Map<LayoutNode, NodeFacts>,
@@ -181,6 +185,20 @@ export declare class Placer {
181
185
  };
182
186
  /** Work out everything, and write it onto the nodes. */
183
187
  run(): void;
188
+ /**
189
+ * What a limit may relate (design.org 5.6): two things in one box that
190
+ * nothing else ties together, since the box's own spacing between them is
191
+ * the only distance it may lengthen.
192
+ */
193
+ private checkLimitTargets;
194
+ /**
195
+ * Which of the things a box holds that nothing ties to each other a member
196
+ * belongs to, on one axis: its title, what is placed against the box's
197
+ * sides, or the rest, placed against each other.
198
+ */
199
+ private groupOf;
200
+ /** A limit the box couldn't make room for: no side helps, or a stated size holds it in. */
201
+ private checkLimitsMet;
184
202
  /** Where a node starts on the page. */
185
203
  page(node: LayoutNode, axis: Axis): number;
186
204
  /**
@@ -226,6 +244,26 @@ export declare class Placer {
226
244
  private frame;
227
245
  /** A container's outline around what it holds, before anything states its size. */
228
246
  private outline;
247
+ /**
248
+ * A container's outline with room made for its children's limits
249
+ * (design.org 5.6). The only distance a limit may lengthen is between what
250
+ * the box holds and its sides, which every placement inside leaves unstated.
251
+ * Each side's room moves a limit's two ends apart at a fixed rate (all of
252
+ * it for a corner, half for the middle of a side), so one trial of each
253
+ * side gives the rate, and the room is the shortfall over the rate. A side
254
+ * that doesn't help is never used; with both helping, as for two corner
255
+ * children, the box grows evenly. A limit no side helps is left unmet, and
256
+ * `checkLimits` refuses it.
257
+ */
258
+ private roomFor;
259
+ /** The limits a container's children state on one axis. */
260
+ private limitsIn;
261
+ /** How much nearer than its gap a limit's two ends are, in a trial frame of their container. */
262
+ private shortfall;
263
+ /** The distance a limit keeps: its gap, or the usual one; beside a text, as beside any text. */
264
+ private limitGap;
265
+ /** Where a child of `node` is on one axis, in a trial frame of `node`. */
266
+ private memberAt;
229
267
  private remember;
230
268
  /**
231
269
  * Work something out, refusing a circle. The trail holds what was being
@@ -238,6 +276,14 @@ export declare class Placer {
238
276
  private unitStart;
239
277
  /** A box's start from its own placements. */
240
278
  private placed;
279
+ /**
280
+ * Opposite placements center a box between two others, but not when one of
281
+ * them is its own container's box: `inside c top-right right of c.k` would
282
+ * be neither in the corner nor a gap from `c.k` (design.org 5.3). Refused,
283
+ * naming the two ways out: the corner's other half beside the sibling, or
284
+ * the corner kept and the sibling held off with `stays`.
285
+ */
286
+ private refuseBoxAndSibling;
241
287
  private edgeOf;
242
288
  /** A box's start from one statement. */
243
289
  private apply;
@@ -314,6 +360,15 @@ export declare class Placer {
314
360
  private textOf;
315
361
  /** A child placed against its own container's box, which it does not grow. */
316
362
  framed(node: LayoutNode): boolean;
363
+ /**
364
+ * Framed on one axis: what says where it goes on that axis is against its
365
+ * container's box. `right of c.k inside c top` is framed down, so its height
366
+ * is measured from the finished box, and is content across, so the box holds
367
+ * it there (design.org 5.6).
368
+ */
369
+ framedOn(node: LayoutNode, axis: Axis): boolean;
370
+ /** Whether a placement names its container's box, any part but the text. */
371
+ private againstBox;
317
372
  /**
318
373
  * A child placed against its container's text, or against its heading,
319
374
  * which makes the title its frame's anchor.
@@ -334,7 +389,8 @@ export declare class Placer {
334
389
  /**
335
390
  * The children a container's box is the outline of, besides its title: not
336
391
  * its heading, not what is joined to its text, and not those placed against
337
- * the box.
392
+ * the box. Given an axis, a child placed against the box only on the other
393
+ * axis is content on this one.
338
394
  */
339
395
  private contentOf;
340
396
  /** Whether a node has a title: its text, what is joined to it, or a heading. */
@@ -480,6 +536,23 @@ export declare class Placer {
480
536
  * title something is placed against anchors the frame instead.
481
537
  */
482
538
  private checkAnchors;
539
+ /**
540
+ * Every box in a frame says something about where it goes, so the frame is
541
+ * anchored one axis at a time (design.org 5.3): across by the one box that
542
+ * says nothing across, up and down by the one that says nothing up and
543
+ * down. A lone direction's centering counts as saying nothing when its
544
+ * target's place on that axis is worked out from the box itself, as with
545
+ * `b right of a` and `a even with b.1`: the centering gives way to what was
546
+ * written. An axis nobody leaves free is a circle, reported when placing,
547
+ * and an axis a container is placed on by what it holds needs no anchor.
548
+ */
549
+ private splitAnchors;
550
+ /** Whether a box with nothing stated on this axis is left free there: no lone direction centers it, or the one that would is placed from the box itself. */
551
+ private loneGivesWay;
552
+ /** The boxes a placement names, through points and regions. */
553
+ private namedFrom;
554
+ /** Whether a box's place on one axis is worked out, however indirectly, from `from` or something inside it. */
555
+ private placedFrom;
483
556
  /**
484
557
  * A box in a venn's part may be placed against a box in another part, and
485
558
  * then it says where its whole part goes up and down: so it must be the