reladraw 0.4.0 → 0.6.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,6 +1,7 @@
1
- import { COLOR_KEYS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, PASSAGE_AXES, TEXT_KEYS, CONTENTS_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
1
+ import { COLOR_KEYS, DEFAULT_KEYS, DEFAULT_TARGETS, DIAGRAM_KEYS, DIRECTIONS, describePlacement, SIDE_AXIS, SIDES, PASSAGE_AXES, TEXT_KEYS, CONTENTS_KEYS, PLACEMENT_KEYS, BOUNDARY_PARTS, INWARD, OPPOSITE, isDirection, isPart, isPosition, listTargets, nameTarget, } from './ast.js';
2
2
  import { SourceError } from './errors.js';
3
3
  import { isAttrKey, tokenizeLine } from './lexer.js';
4
+ import { THEME_NAMES, THEMES } from './themes.js';
4
5
  /** Parse a whole source file. One statement per line; blanks and comments drop out. */
5
6
  export function parse(source) {
6
7
  const statements = [];
@@ -27,6 +28,8 @@ function parseStatement(tokens, line) {
27
28
  return parseStyle(tokens, line);
28
29
  case 'diagram':
29
30
  return parseDiagram(tokens, line);
31
+ case 'default':
32
+ return parseDefault(tokens, line);
30
33
  default:
31
34
  throw new SourceError(substitution(keyword.text, tokens), line);
32
35
  }
@@ -458,9 +461,26 @@ function parseEdge(head, line) {
458
461
  between = { targets: read.targets, ...(axis !== undefined ? { axis } : {}) };
459
462
  return next;
460
463
  });
461
- if (tail.placements.length > 0) {
462
- throw new SourceError(`${subject}: "${describePlacement(tail.placements[0])}" places a node, and an edge is not ` +
463
- 'placed — it joins two things that are', line);
464
+ // An edge is not placed, so a direction on its line says which side of that
465
+ // node the line passes. The other placements say where a thing *is*, and
466
+ // have no reading for a line.
467
+ const passes = [];
468
+ for (const placement of tail.placements) {
469
+ const written = describePlacement(placement);
470
+ if (placement.kind !== 'offset' || placement.written !== undefined) {
471
+ throw new SourceError(`${subject}: "${written}" places a node, and an edge is not placed — it joins two things ` +
472
+ 'that are. On an edge, above, below, left of and right of say which side of a node the ' +
473
+ 'line passes', line);
474
+ }
475
+ if (placement.gap !== undefined) {
476
+ throw new SourceError(`${subject}: "${written}" gives a gap, and a gap is kept between nodes — the line passes ` +
477
+ 'as close as it reads clearly. Drop the brackets', line);
478
+ }
479
+ passes.push(placement);
480
+ }
481
+ if (passes.length > 0 && between) {
482
+ throw new SourceError(`${subject}: "between" and "${describePlacement(passes[0])}" on one edge — an edge passing ` +
483
+ 'between two things already has a side of each, so say one or the other', line);
464
484
  }
465
485
  refuseTextKey(tail.attrs, subject, 'after the arrow', line);
466
486
  return {
@@ -471,6 +491,7 @@ function parseEdge(head, line) {
471
491
  both: arrow.text === '<->',
472
492
  ...(textToken ? { text: textToken.text } : {}),
473
493
  ...(between ? { between } : {}),
494
+ ...(passes.length > 0 ? { passes } : {}),
474
495
  attrs: tail.attrs,
475
496
  line,
476
497
  };
@@ -502,12 +523,80 @@ function parseDiagram(head, line) {
502
523
  throw new SourceError('diagram sets nothing', line);
503
524
  }
504
525
  for (const key of Object.keys(attrs)) {
526
+ if (key.startsWith('text.')) {
527
+ // The theme has one text color, shared by everything, and that is all
528
+ // the diagram's text sets. A size or a wrap is about one kind of text.
529
+ if (key !== 'text.color') {
530
+ const inner = key.slice('text.'.length);
531
+ throw new SourceError(`diagram text: sets only a color — \`${inner}\` is about one kind of text, so write ` +
532
+ `\`text: (${inner}: ${attrs[key]})\` on a \`default node\` or \`default edge\``, line);
533
+ }
534
+ continue;
535
+ }
505
536
  if (!DIAGRAM_KEYS.includes(key)) {
506
537
  throw new SourceError(`diagram has no "${key}" — it takes ${DIAGRAM_KEYS.join(', ')}`, line);
507
538
  }
508
539
  }
540
+ if (attrs['text'] !== undefined) {
541
+ throw new SourceError('diagram text: takes a bracket — `text: (color: #e0e0e0)`', line);
542
+ }
543
+ const theme = attrs['theme'];
544
+ if (theme !== undefined && THEMES[theme] === undefined) {
545
+ throw new SourceError(`there is no theme called "${theme}" — the themes are ${THEME_NAMES.join(', ')}`, line);
546
+ }
509
547
  return { kind: 'diagram', attrs, line };
510
548
  }
549
+ /**
550
+ * `default <node | leaf | container | edge> <attributes>` — a style every thing
551
+ * of that kind wears without naming it. It is strict about kind where a style
552
+ * is permissive, because it names the kind it is for: a word that kind has no
553
+ * use for can only be a mistake.
554
+ */
555
+ function parseDefault(head, line) {
556
+ const target = head[1];
557
+ if (!target || target.quoted || !DEFAULT_TARGETS.includes(target.text)) {
558
+ throw new SourceError(`default needs the kind it is for — ${DEFAULT_TARGETS.join(', ')}` +
559
+ (target && !isAttrKey(target) ? `, not "${target.text}"` : ''), line);
560
+ }
561
+ const kind = target.text;
562
+ const subject = `default ${kind}`;
563
+ // `default leaf node` reads naturally and says nothing `default leaf` does not.
564
+ const extra = head[2];
565
+ if (extra && !extra.quoted && !isAttrKey(extra) && extra.text === 'node' && kind !== 'node') {
566
+ throw new SourceError(`write \`default ${kind}\` — a ${kind} is already a node`, line);
567
+ }
568
+ const attrs = attrsOnly(head, 2, line, subject);
569
+ if (Object.keys(attrs).length === 0) {
570
+ throw new SourceError(`${subject} sets nothing`, line);
571
+ }
572
+ const allowed = DEFAULT_KEYS[kind];
573
+ for (const key of new Set(Object.keys(attrs).map((k) => k.split('.')[0]))) {
574
+ if (allowed.includes(key))
575
+ continue;
576
+ throw new SourceError(`${subject} has ${key}:, ${defaultRefusal(kind, key)}`, line);
577
+ }
578
+ return { kind: 'default', target: kind, attrs, line };
579
+ }
580
+ /** Why a default cannot carry a word, and what to write instead. */
581
+ function defaultRefusal(kind, key) {
582
+ const takes = `it takes ${DEFAULT_KEYS[kind].map((k) => `${k}:`).join(', ')}`;
583
+ if (key === 'url')
584
+ return `and a destination belongs to one thing rather than to every one of a kind`;
585
+ if (key === 'badge') {
586
+ return 'and a badge is a child, so every leaf given one would become a container. ' +
587
+ 'Put the badge in a style, or write `default container badge:`';
588
+ }
589
+ if (key === 'icon') {
590
+ return 'and a picture cannot hold children, so an icon is a leaf\'s word — write `default leaf icon:`';
591
+ }
592
+ if (kind === 'edge' && (key === 'fill' || key === 'border')) {
593
+ return `which an edge does not have — an edge is colored by line: and text: (color: …)`;
594
+ }
595
+ if (kind !== 'edge' && key === 'line') {
596
+ return `which is an edge's — a node is colored by fill:, border: and text: (color: …)`;
597
+ }
598
+ return `which a default does not set — ${takes}`;
599
+ }
511
600
  function requireName(token, keyword, line) {
512
601
  if (!token || token.quoted) {
513
602
  throw new SourceError(`${keyword} needs a name`, line);
package/dist/render.d.ts CHANGED
@@ -1,31 +1,10 @@
1
1
  import { type Measurer } from './measure.js';
2
2
  import type { Layout } from './model.js';
3
+ import { type Theme } from './themes.js';
3
4
  export interface RenderOptions {
4
5
  measurer?: Measurer;
5
6
  fontSize?: number;
6
7
  theme?: Theme;
7
8
  }
8
- export interface Theme {
9
- background: string;
10
- boxFill: string;
11
- boxStroke: string;
12
- containerFill: string;
13
- /** A container is a region rather than a thing, so its outline is quieter. */
14
- containerStroke: string;
15
- text: string;
16
- mutedText: string;
17
- edge: string;
18
- /** An icon's drawn line. */
19
- iconInk: string;
20
- /** The body an icon's lines enclose. */
21
- iconShade: string;
22
- }
23
- /**
24
- * Sampled out of `examples/reference/arch.png` rather than invented,
25
- * so the benchmark render and the drawing it is measured against differ by
26
- * geometry and typography alone. A container is a shade off the page and barely
27
- * outlined; a leaf is the navy that carries the diagram's weight.
28
- */
29
- export declare const DARK_THEME: Theme;
30
9
  /** Turn solved geometry into a standalone SVG document. */
31
10
  export declare function render(layout: Layout, options?: RenderOptions): string;