reladraw 0.17.1 → 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/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export * from './grammar.js';
6
6
  export * from './measure.js';
7
7
  export * from './model.js';
8
8
  export { parse } from './parser.js';
9
+ export { format } from './format.js';
10
+ export { DEFAULT_FORMAT, readFormatSettings, SETTINGS, SETTINGS_FILE, settingsFileText, type FormatSettings, } from './format-settings.js';
9
11
  export { resolve, type ResolveOptions } from './resolve.js';
10
12
  export { render, type RenderOptions } from './render.js';
11
13
  export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES, type Theme } from './themes.js';
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ export * from './grammar.js';
6
6
  export * from './measure.js';
7
7
  export * from './model.js';
8
8
  export { parse } from './parser.js';
9
+ export { format } from './format.js';
10
+ export { DEFAULT_FORMAT, readFormatSettings, SETTINGS, SETTINGS_FILE, settingsFileText, } from './format-settings.js';
9
11
  export { resolve } from './resolve.js';
10
12
  export { render } from './render.js';
11
13
  export { DARK_THEME, DEFAULT_THEME, THEMES, THEME_NAMES } from './themes.js';
package/dist/lexer.d.ts CHANGED
@@ -11,6 +11,14 @@ export interface Token {
11
11
  * about the word can quote what the author typed.
12
12
  */
13
13
  joined?: string;
14
+ /**
15
+ * Where the token was written: from `start` up to `end` in the line given to
16
+ * `tokenizeLine`, quotes included. The formatter copies each token back out
17
+ * of its line by these, so nothing it writes is respelled. Absent on a token
18
+ * made up by the parser rather than read from a line.
19
+ */
20
+ start?: number;
21
+ end?: number;
14
22
  }
15
23
  /**
16
24
  * A `"""` string still open at the end of the text given. `parse` catches this
package/dist/lexer.js CHANGED
@@ -48,13 +48,13 @@ export function tokenizeLine(line, lineNumber, depth = 0) {
48
48
  break;
49
49
  if (ch === '(') {
50
50
  depth += 1;
51
- tokens.push({ text: '(', quoted: false });
51
+ tokens.push({ text: '(', quoted: false, start: i, end: i + 1 });
52
52
  i += 1;
53
53
  continue;
54
54
  }
55
55
  if (ch === ')' && depth > 0) {
56
56
  depth -= 1;
57
- tokens.push({ text: ')', quoted: false });
57
+ tokens.push({ text: ')', quoted: false, start: i, end: i + 1 });
58
58
  i += 1;
59
59
  continue;
60
60
  }
@@ -65,11 +65,12 @@ export function tokenizeLine(line, lineNumber, depth = 0) {
65
65
  const end = line.indexOf('"""', i + 3);
66
66
  if (end === -1)
67
67
  throw new OpenString('this """ string is never closed — end it with """', lineNumber);
68
- tokens.push({ text: line.slice(i + 3, end), quoted: true, triple: true });
68
+ tokens.push({ text: line.slice(i + 3, end), quoted: true, triple: true, start: i, end: end + 3 });
69
69
  i = end + 3;
70
70
  continue;
71
71
  }
72
72
  if (ch === '"') {
73
+ const start = i;
73
74
  let text = '';
74
75
  i += 1;
75
76
  let closed = false;
@@ -96,7 +97,7 @@ export function tokenizeLine(line, lineNumber, depth = 0) {
96
97
  }
97
98
  if (!closed)
98
99
  throw new SourceError('unterminated string', lineNumber);
99
- tokens.push({ text, quoted: true });
100
+ tokens.push({ text, quoted: true, start, end: i });
100
101
  continue;
101
102
  }
102
103
  const start = i;
@@ -111,6 +112,11 @@ export function tokenizeLine(line, lineNumber, depth = 0) {
111
112
  break;
112
113
  text += c;
113
114
  i += 1;
115
+ // A comma ends a word, so `rounded,path: square` reads as
116
+ // `rounded, path: square`. Not inside a word's own bracket, which keeps
117
+ // `rgb(20,20,20)` whole.
118
+ if (c === ',' && !text.includes('('))
119
+ break;
114
120
  }
115
121
  // `gap:tight` is `gap: tight` with no space, since how much whitespace there
116
122
  // is never matters. A word straight after a key is its value and is left
@@ -120,11 +126,18 @@ export function tokenizeLine(line, lineNumber, depth = 0) {
120
126
  const previous = tokens[tokens.length - 1];
121
127
  const joined = previous && isAttrKey(previous) ? null : JOINED_KEY.exec(text);
122
128
  if (joined) {
123
- tokens.push({ text: joined[1], quoted: false, joined: text });
129
+ tokens.push({ text: joined[1], quoted: false, joined: text, start, end: start + joined[1].length });
124
130
  i = start + joined[1].length;
125
131
  continue;
126
132
  }
127
- tokens.push({ text, quoted: false });
133
+ // A comma written apart from its word, `start , above`, is the comma of
134
+ // `start, above`. A bracket or a quoted text keeps its comma separate, so
135
+ // neither is changed by what follows it.
136
+ if (text === ',' && previous && !previous.quoted && !isAttrKey(previous) && !/^[()]$/.test(previous.text) && !previous.text.endsWith(',')) {
137
+ previous.text += ',';
138
+ continue;
139
+ }
140
+ tokens.push({ text, quoted: false, start, end: i });
128
141
  }
129
142
  return tokens;
130
143
  }
package/dist/lines.d.ts CHANGED
@@ -11,6 +11,13 @@ export interface Face {
11
11
  height: number;
12
12
  /** A circle is drawn in this square, so a line meets the circle. */
13
13
  round: boolean;
14
+ /**
15
+ * Any other outline but a rectangle's, as a polygon on the page, with the
16
+ * shape's middle: a line meets the polygon, and the lines from the middle
17
+ * to the box's corners cut it into its four sides.
18
+ */
19
+ outline?: Point[];
20
+ middle?: Point;
14
21
  }
15
22
  export type Side = 'top' | 'bottom' | 'left' | 'right';
16
23
  /** One piece of a drawn line, from where the last one ended: straight, or a cubic. */
@@ -69,6 +76,7 @@ export declare function planSides(layout: Layout): {
69
76
  export declare function faceOfNode(node: LayoutNode): Face;
70
77
  /** The point a share of the way along a polyline, bends included, or its start or end. */
71
78
  export declare function alongLength(trace: Point[], at: number | 'start' | 'end'): Point;
79
+ /** A face's middle: its box's, or a shape's own, which is a triangle's a third of the way from its base. */
72
80
  export declare function centerOf(face: Face): Point;
73
81
  /** The points with repeats and points in line with both neighbors dropped. */
74
82
  export declare function tidyCorners(points: Point[]): Point[];
package/dist/lines.js CHANGED
@@ -16,6 +16,7 @@ import { GAPS } from './constants.js';
16
16
  import { EDGE_ORDER_KEYS, PART_SIDES, WAYS, isPosition } from './ast.js';
17
17
  import { SourceError } from './errors.js';
18
18
  import { gapPixels } from './place.js';
19
+ import { isBoxy, isRound, lineCrossings, middleOf, rayExit, shapeOf, sideOfPoint, sideStretch, towardOutline } from './outline.js';
19
20
  /**
20
21
  * How far apart copies of one line run, like lanes on a road (6.5). A fixed
21
22
  * step, so adding a copy widens the set by one lane and moves no other lane.
@@ -464,6 +465,9 @@ function meetOutline(face, at, way) {
464
465
  if (disc >= 0)
465
466
  hits.push(-b - Math.sqrt(disc), -b + Math.sqrt(disc));
466
467
  }
468
+ else if (face.outline) {
469
+ hits.push(...lineCrossings(face.outline, at, way));
470
+ }
467
471
  else {
468
472
  const within = (value, low, size) => value >= low - 0.5 && value <= low + size + 0.5;
469
473
  for (const x of [face.x, face.x + face.width]) {
@@ -497,6 +501,10 @@ function leaving(face, from, way) {
497
501
  const t = -b + Math.sqrt(Math.max(0, b * b - (fx * fx + fy * fy - r * r)));
498
502
  return { x: from.x + way.x * t, y: from.y + way.y * t };
499
503
  }
504
+ if (face.outline) {
505
+ const t = rayExit(face.outline, from, way) ?? 0;
506
+ return { x: from.x + way.x * t, y: from.y + way.y * t };
507
+ }
500
508
  const tx = way.x > 1e-9 ? (face.x + face.width - from.x) / way.x : way.x < -1e-9 ? (face.x - from.x) / way.x : Infinity;
501
509
  const ty = way.y > 1e-9 ? (face.y + face.height - from.y) / way.y : way.y < -1e-9 ? (face.y - from.y) / way.y : Infinity;
502
510
  const t = Math.max(0, Math.min(tx, ty));
@@ -576,13 +584,20 @@ const WAY_VECTOR = {
576
584
  const MET_SIDE = { down: 'top', up: 'bottom', left: 'right', right: 'left' };
577
585
  /** The drawn rectangle of a node. */
578
586
  export function faceOfNode(node) {
579
- return {
587
+ const face = {
580
588
  x: node.x,
581
589
  y: node.y,
582
590
  width: node.width,
583
591
  height: node.height,
584
- round: node.body.kind === 'shape' && node.body.outline === 'circle',
592
+ round: node.body.kind === 'shape' && isRound(node.body.outline, node.width, node.height),
585
593
  };
594
+ if (node.body.kind === 'shape' && !isBoxy(node.body.outline) && !face.round) {
595
+ const { outline, look } = node.body;
596
+ face.outline = shapeOf(outline, look, node.width, node.height).polygon.map((p) => ({ x: node.x + p.x, y: node.y + p.y }));
597
+ const middle = middleOf(outline, look, node.width, node.height);
598
+ face.middle = { x: node.x + middle.x, y: node.y + middle.y };
599
+ }
600
+ return face;
586
601
  }
587
602
  function endOf(edge, which) {
588
603
  const node = which === 'from' ? edge.from : edge.to;
@@ -1027,8 +1042,9 @@ function distance(a, b) {
1027
1042
  return Math.hypot(b.x - a.x, b.y - a.y);
1028
1043
  }
1029
1044
  // --- points on a box ------------------------------------------------------------
1045
+ /** A face's middle: its box's, or a shape's own, which is a triangle's a third of the way from its base. */
1030
1046
  export function centerOf(face) {
1031
- return { x: face.x + face.width / 2, y: face.y + face.height / 2 };
1047
+ return face.middle ?? { x: face.x + face.width / 2, y: face.y + face.height / 2 };
1032
1048
  }
1033
1049
  /** The sides a position sits on: `bottom-right` is on the bottom and the right, `top-center` on the top only. */
1034
1050
  function cornerSides(position) {
@@ -1046,9 +1062,12 @@ function positionPoint(face, position) {
1046
1062
  x: horizontal === 'left' ? face.x : horizontal === 'right' ? face.x + face.width : face.x + face.width / 2,
1047
1063
  y: vertical === 'top' ? face.y : vertical === 'bottom' ? face.y + face.height : face.y + face.height / 2,
1048
1064
  };
1049
- // A circle has no corners; its corner is where the diagonal meets it.
1065
+ // A circle has no corners; its corner is where the diagonal meets it. On
1066
+ // another shape every point is where the line from its middle meets it.
1050
1067
  if (face.round && vertical && horizontal)
1051
1068
  return outlineToward(face, point);
1069
+ if (face.outline)
1070
+ return outlineToward(face, point);
1052
1071
  return point;
1053
1072
  }
1054
1073
  /** A point a fraction of the way along a side, from its left or top end. */
@@ -1066,6 +1085,29 @@ function onSide(face, side, along) {
1066
1085
  const angle = { right: turn, bottom: Math.PI / 2 - turn, left: Math.PI - turn, top: -Math.PI / 2 + turn }[side];
1067
1086
  return { x: center.x + radius * Math.cos(angle), y: center.y + radius * Math.sin(angle) };
1068
1087
  }
1088
+ if (face.outline) {
1089
+ // The side is the stretch of outline between the lines from the middle to
1090
+ // its two corners. `along` lands where the stretch passes it, so a line
1091
+ // down from the middle of one box's bottom meets the next square on; one
1092
+ // past the stretch's ends is placed on it in proportion instead.
1093
+ const stretch = sideStretch(face.outline, centerOf(face), face, side);
1094
+ const key = side === 'top' || side === 'bottom' ? 'x' : 'y';
1095
+ const low = key === 'x' ? face.x : face.y;
1096
+ const size = key === 'x' ? face.width : face.height;
1097
+ const values = stretch.map((p) => p[key]);
1098
+ const lo = Math.min(...values);
1099
+ const hi = Math.max(...values);
1100
+ const target = along >= lo && along <= hi ? along : lo + ((along - low) / size) * (hi - lo);
1101
+ for (let i = 1; i < stretch.length; i++) {
1102
+ const a = stretch[i - 1];
1103
+ const b = stretch[i];
1104
+ if ((a[key] - target) * (b[key] - target) <= 0 && a[key] !== b[key]) {
1105
+ const f = (target - a[key]) / (b[key] - a[key]);
1106
+ return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
1107
+ }
1108
+ }
1109
+ return Math.abs(stretch[0][key] - target) < Math.abs(stretch[stretch.length - 1][key] - target) ? stretch[0] : stretch[stretch.length - 1];
1110
+ }
1069
1111
  switch (side) {
1070
1112
  case 'top':
1071
1113
  return { x: along, y: face.y };
@@ -1089,6 +1131,8 @@ function outlineToward(face, target) {
1089
1131
  const length = Math.hypot(dx, dy);
1090
1132
  return { x: center.x + (dx / length) * radius, y: center.y + (dy / length) * radius };
1091
1133
  }
1134
+ if (face.outline)
1135
+ return towardOutline(face.outline, center, target);
1092
1136
  const scale = Math.min(dx === 0 ? Infinity : face.width / 2 / Math.abs(dx), dy === 0 ? Infinity : face.height / 2 / Math.abs(dy));
1093
1137
  return { x: center.x + dx * scale, y: center.y + dy * scale };
1094
1138
  }
@@ -1106,6 +1150,8 @@ function headingAt(face, at) {
1106
1150
  const dy = at.y - center.y;
1107
1151
  return Math.abs(dx) >= Math.abs(dy) ? SIDE_NORMAL[dx >= 0 ? 'right' : 'left'] : SIDE_NORMAL[dy >= 0 ? 'bottom' : 'top'];
1108
1152
  }
1153
+ if (face.outline)
1154
+ return SIDE_NORMAL[sideOfPoint(centerOf(face), face, at)];
1109
1155
  distances.sort((a, b) => a[0] - b[0]);
1110
1156
  return SIDE_NORMAL[distances[0][1]];
1111
1157
  }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The geometry of every outline `shape:` names: where its edge runs, where its
3
+ * middle is, and how big it grows to hold its text.
4
+ *
5
+ * One place knows each shape, and the resolver, the line planner, the checks
6
+ * and the renderer all ask it, so they cannot disagree about where a diamond's
7
+ * edge is.
8
+ *
9
+ * A shape stretches to fill its box. What the box doesn't decide is at most one
10
+ * amount and one direction (a parallelogram's slant and which way it leans),
11
+ * and those are the settings in the shape's bracket. Amounts are fixed
12
+ * distances, not fractions of the box, as the document's fold always was.
13
+ *
14
+ * The rectangle-like outlines (`rectangle`, `document`, `flow-subprocess`) and
15
+ * a round `circle` keep the exact geometry they always had; every other
16
+ * outline is worked with as a polygon, its curves sampled finely enough that a
17
+ * line meeting one lands on the drawn curve to within a fraction of a pixel.
18
+ */
19
+ /** A point, relative to whatever the caller says. */
20
+ export interface Point {
21
+ x: number;
22
+ y: number;
23
+ }
24
+ /** How each setting in a shape's bracket is read. */
25
+ type SettingKind = 'distance' | 'flag' | readonly string[];
26
+ /**
27
+ * The settings each shape takes in its bracket, by name. A shape not listed
28
+ * takes none. The first word of a direction is the default.
29
+ */
30
+ export declare const SHAPE_SETTINGS: Readonly<Record<string, Readonly<Record<string, SettingKind>>>>;
31
+ /** The settings a shape was given, read: distances in pixels, directions as words, flags as true. */
32
+ export interface ShapeLook {
33
+ fold?: number;
34
+ slant?: number;
35
+ lean?: string;
36
+ point?: number;
37
+ taper?: number;
38
+ narrow?: string;
39
+ aim?: string;
40
+ cap?: number;
41
+ lying?: boolean;
42
+ }
43
+ /**
44
+ * Read a shape's bracket, refusing a setting the shape doesn't take and a
45
+ * value that isn't one. `written` holds the dotted keys as stored, `shape.slant`.
46
+ */
47
+ export declare function readShapeLook(outline: string, written: Record<string, string>, subject: string, line: number): ShapeLook;
48
+ /** How far inside each side a subprocess's bars stand. */
49
+ export declare const SUBPROCESS_INSET: number;
50
+ /** Whether an outline is a rectangle as far as placement and lines go. */
51
+ export declare function isBoxy(outline: string): boolean;
52
+ /** Whether an outline is drawn round in a square box, so the exact circle code applies. */
53
+ export declare function isRound(outline: string, width: number, height: number): boolean;
54
+ /** One outline at one size: the polygon it is worked with, and the SVG it is drawn as. Points are relative to the box's top-left. */
55
+ export interface Shape {
56
+ /** The outline as a closed polygon, for meeting lines and testing what is inside. */
57
+ polygon: Point[];
58
+ /** The path data, relative, drawn translated to the box. */
59
+ path: string;
60
+ /** Lines drawn inside the outline: a cylinder's near rim, a subprocess's bars. */
61
+ detail: string[];
62
+ }
63
+ /**
64
+ * An outline at a size. Rectangle-like outlines and the round circle are
65
+ * included so a caller can always ask, but the renderer draws those itself.
66
+ */
67
+ export declare function shapeOf(outline: string, look: ShapeLook, w: number, h: number): Shape;
68
+ /**
69
+ * A path written relative to a box's top-left, moved to (x, y). Only the
70
+ * absolute commands `shapeOf` writes are handled: M, L, H, V, Q, T, A and Z.
71
+ */
72
+ export declare function placePath(d: string, x: number, y: number): string;
73
+ /**
74
+ * The shape's own middle, where its text sits and its lines aim: the center of
75
+ * its area. On a symmetric shape that is the middle of its box; a triangle's is
76
+ * a third of the way from its base.
77
+ */
78
+ export declare function middleOf(outline: string, look: ShapeLook, w: number, h: number): Point;
79
+ /** Whether a point is inside a polygon, by crossings. */
80
+ export declare function insideOutline(points: Point[], p: Point): boolean;
81
+ /** Whether a point is more than `distance` from every edge of a polygon. */
82
+ export declare function clearOf(points: Point[], p: Point, distance: number): boolean;
83
+ /**
84
+ * The size an outline takes to hold a block `tw` by `th` (the text with its
85
+ * padding) at its middle: a first guess that is exact for most shapes, then
86
+ * grown a step at a time until the block fits. A rectangle-like outline and
87
+ * the round circle are sized by the resolver, as they always were.
88
+ */
89
+ export declare function sizeFor(outline: string, look: ShapeLook, tw: number, th: number): {
90
+ width: number;
91
+ height: number;
92
+ };
93
+ /**
94
+ * Where the ray from `from` heading `way` first leaves a polygon, measured as
95
+ * the distance along the ray, or undefined if it never crosses it.
96
+ */
97
+ export declare function rayExit(points: Point[], from: Point, way: Point): number | undefined;
98
+ /** Every distance along the line through `from` heading `way`, either direction, at which it crosses a polygon. */
99
+ export declare function lineCrossings(points: Point[], from: Point, way: Point): number[];
100
+ /** Where the line from a polygon's middle toward `target` crosses its outline: the farthest crossing, so a concave outline is met at its rim. */
101
+ export declare function towardOutline(points: Point[], middle: Point, target: Point): Point;
102
+ /**
103
+ * The stretch of the outline that is one side: from where the line from the
104
+ * middle to one corner of the box meets it, round to where the line to the
105
+ * next corner does. Returned in reading order, left to right along a top or
106
+ * bottom side and top to bottom along a left or right one.
107
+ */
108
+ export declare function sideStretch(points: Point[], middle: Point, box: {
109
+ x: number;
110
+ y: number;
111
+ width: number;
112
+ height: number;
113
+ }, side: 'top' | 'bottom' | 'left' | 'right'): Point[];
114
+ /** The point a fraction of the way along a polyline, by length. */
115
+ export declare function alongPolyline(points: Point[], fraction: number): Point;
116
+ /** Which side of the box a point on the outline is on, by the corner rays from the middle. */
117
+ export declare function sideOfPoint(middle: Point, box: {
118
+ x: number;
119
+ y: number;
120
+ width: number;
121
+ height: number;
122
+ }, at: Point): 'top' | 'bottom' | 'left' | 'right';
123
+ export {};