@cardenelabs/dragon 0.34.0 → 0.35.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 +39 -720
- package/dist/cli.cjs +4529 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +4523 -0
- package/dist/cli.js.map +1 -0
- package/dist/export.cjs +3912 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +36 -0
- package/dist/export.d.ts +36 -0
- package/dist/export.js +3907 -0
- package/dist/export.js.map +1 -0
- package/dist/fonts.cjs +7 -0
- package/dist/fonts.cjs.map +1 -0
- package/dist/fonts.js +4 -0
- package/dist/fonts.js.map +1 -0
- package/dist/index.cjs +4105 -20356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -5522
- package/dist/index.d.ts +227 -5522
- package/dist/index.js +4091 -20283
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -0
- package/dist/schema-BBWfniwH.d.cts +112 -0
- package/dist/schema-BBWfniwH.d.ts +112 -0
- package/package.json +39 -24
- package/src/cli.ts +237 -0
- package/src/export/capture.ts +277 -0
- package/src/export/fonts.ts +68 -0
- package/src/export/html.ts +32 -0
- package/src/export/index.ts +146 -0
- package/src/field-messages.ts +104 -0
- package/src/figure-text.ts +42 -0
- package/src/index.ts +30 -283
- package/src/layout/architecture.ts +381 -0
- package/src/layout/measure.ts +21 -0
- package/src/layout/scene.ts +353 -0
- package/src/layout/sequence.ts +189 -0
- package/src/load-fonts.ts +8 -0
- package/src/look/fonts.ts +33 -0
- package/src/look/palettes.json +209 -0
- package/src/look/values.ts +155 -0
- package/src/markdown.ts +35 -0
- package/src/messages.ts +122 -0
- package/src/parser.ts +342 -310
- package/src/player-global.ts +4 -0
- package/src/player.ts +271 -0
- package/src/printer.ts +76 -0
- package/src/render/icons.ts +41 -0
- package/src/render/motion.ts +106 -0
- package/src/render/parts.ts +279 -0
- package/src/render/svg.ts +341 -0
- package/src/schema.ts +161 -16
- package/src/schemas/diagram.schema.json +150 -0
- package/src/schemas/validate-diagram.d.ts +12 -0
- package/src/schemas/validate-diagram.js +8 -0
- package/src/timeline.ts +166 -0
- package/src/tokenize.ts +20 -281
- package/src/version.ts +55 -0
- package/src/view.ts +16 -0
- package/src/words.ts +79 -0
- package/examples/quick-start.md +0 -142
- package/src/canvas-bounds.ts +0 -52
- package/src/color.ts +0 -197
- package/src/compile/actor-ids.ts +0 -297
- package/src/compile/actor-option-notice.ts +0 -379
- package/src/compile/actors.ts +0 -103
- package/src/compile/c4.ts +0 -74
- package/src/compile/chain.ts +0 -93
- package/src/compile/chart-fields.ts +0 -193
- package/src/compile/deep-rewrite.ts +0 -30
- package/src/compile/derived-source.ts +0 -18
- package/src/compile/direction.ts +0 -102
- package/src/compile/edge-option-notice.ts +0 -112
- package/src/compile/er-relation.ts +0 -200
- package/src/compile/flow.ts +0 -54
- package/src/compile/flowchart.ts +0 -344
- package/src/compile/funnel.ts +0 -87
- package/src/compile/gantt.ts +0 -337
- package/src/compile/generic.ts +0 -696
- package/src/compile/hierarchy.ts +0 -92
- package/src/compile/journey.ts +0 -93
- package/src/compile/kinds.ts +0 -139
- package/src/compile/lanes.ts +0 -138
- package/src/compile/metro.ts +0 -271
- package/src/compile/mind.ts +0 -393
- package/src/compile/node-title.ts +0 -24
- package/src/compile/notice.ts +0 -141
- package/src/compile/order.ts +0 -57
- package/src/compile/parts-lookup.ts +0 -17
- package/src/compile/parts.ts +0 -2709
- package/src/compile/placement-apply.ts +0 -689
- package/src/compile/placement.ts +0 -15
- package/src/compile/quadrant.ts +0 -265
- package/src/compile/record.ts +0 -75
- package/src/compile/row-marks.ts +0 -128
- package/src/compile/rows.ts +0 -40
- package/src/compile/sequence.ts +0 -305
- package/src/compile/slug.ts +0 -37
- package/src/compile/stages.ts +0 -247
- package/src/compile/subtitle.ts +0 -35
- package/src/compile/swimlane-marks.ts +0 -87
- package/src/compile/swimlane.ts +0 -82
- package/src/compile/timeline.ts +0 -213
- package/src/compile/topology.ts +0 -44
- package/src/compile/tree.ts +0 -69
- package/src/compile/value-chart.ts +0 -337
- package/src/compile/values.ts +0 -432
- package/src/compile/word-state.ts +0 -82
- package/src/compile.ts +0 -3168
- package/src/focus.ts +0 -46
- package/src/input-size.ts +0 -181
- package/src/json-line-map.ts +0 -30
- package/src/json-parser.ts +0 -4306
- package/src/keywords.ts +0 -417
- package/src/near-name.ts +0 -42
- package/src/notation-lint.ts +0 -342
- package/src/relative-pos.ts +0 -251
- package/src/schemas/diagram.json +0 -3088
- package/src/types.ts +0 -1009
- package/src/v05/index.ts +0 -9
- package/src/v05/input-table.generated.ts +0 -138
- package/src/v05/parser-types.ts +0 -32
- package/src/v05/parser.ts +0 -6668
- package/src/v05/readout-table.generated.ts +0 -1088
- package/src/v05/removed-syntax.ts +0 -115
- package/src/value-syntax.ts +0 -313
- package/src/write-position.ts +0 -270
package/src/timeline.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 段ごとの始まりと長さと、線、運ぶ物、線の名前、段の番号が見える時刻の表。SVG の動きはこの表から組む。
|
|
3
|
+
*
|
|
4
|
+
* 段の長さは本文の字数で決め、段で起きることが全部済んでから少し留まる長さを下限にする。
|
|
5
|
+
* 運ぶ物は主役 (封筒と札は 1 つ、硬貨は別の 1 つ) ごとに途切れず見え続ける。着いた後は
|
|
6
|
+
* 次の同じ主役が出発するまで着いた先に留まり、次が無ければ終わりまで留まる。
|
|
7
|
+
* 線の名前はその段の間だけ出し、同じ段に何本あっても同時に見えるのは 2 つまでにする。
|
|
8
|
+
*/
|
|
9
|
+
import { MOTION } from "./look/values";
|
|
10
|
+
import type { Diagram } from "./schema";
|
|
11
|
+
|
|
12
|
+
/** 秒で表した、見えている間。`end` の時刻は含まない。 */
|
|
13
|
+
export interface Span {
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 線を引いている間。`start` で引き始め、`drawn` で引き終わり、`end` (段の終わり) まで濃く残す。 */
|
|
19
|
+
export interface EdgeTime extends Span {
|
|
20
|
+
step: number;
|
|
21
|
+
event: number;
|
|
22
|
+
drawn: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 運ぶ物が見えている間。`depart` に出発し `arrive` に着く。出発を待つ間と、着いた先で留まる間も含む。 */
|
|
26
|
+
export interface CarrierTime extends Span {
|
|
27
|
+
step: number;
|
|
28
|
+
event: number;
|
|
29
|
+
who: "main" | "coin";
|
|
30
|
+
depart: number;
|
|
31
|
+
arrive: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LabelTime extends Span {
|
|
35
|
+
step: number;
|
|
36
|
+
event: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 処理、箱の字の書き換え、中身の帯の値が起きる時刻。 */
|
|
40
|
+
export interface EventTime {
|
|
41
|
+
step: number;
|
|
42
|
+
event: number;
|
|
43
|
+
at: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Timeline {
|
|
47
|
+
/** 1 周の長さ */
|
|
48
|
+
total: number;
|
|
49
|
+
/** 繰り返す前に消えていく所の始まり */
|
|
50
|
+
last: number;
|
|
51
|
+
steps: Span[];
|
|
52
|
+
edges: EdgeTime[];
|
|
53
|
+
carriers: CarrierTime[];
|
|
54
|
+
labels: LabelTime[];
|
|
55
|
+
badges: { step: number; appears: number }[];
|
|
56
|
+
events: EventTime[];
|
|
57
|
+
/** 段ごとの止め絵の時刻。その段で起きることが全部見えている */
|
|
58
|
+
stills: number[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface Planned {
|
|
62
|
+
edges: { event: number; a: number; b: number; carries: boolean; coin: boolean; named: boolean }[];
|
|
63
|
+
events: { event: number; at: number }[];
|
|
64
|
+
busy: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 段の中で起きることの時刻 (段の始まりから)。線は少しずつずらして引き、書き換えは直前の線が着いてから起こす。 */
|
|
68
|
+
function plan(step: Diagram["steps"][number]): Planned {
|
|
69
|
+
const out: Planned = { edges: [], events: [], busy: 0 };
|
|
70
|
+
let cursor = MOTION.edgeAt;
|
|
71
|
+
let arrived: number | undefined;
|
|
72
|
+
let changes = 0;
|
|
73
|
+
step.events.forEach((event, e) => {
|
|
74
|
+
if (event.type === "edge") {
|
|
75
|
+
const carries = event.carry.kind !== "none";
|
|
76
|
+
const a = cursor;
|
|
77
|
+
const b = a + (carries ? MOTION.drawCarry : MOTION.draw);
|
|
78
|
+
out.edges.push({ event: e, a, b, carries, coin: event.carry.kind === "coin", named: event.label !== undefined });
|
|
79
|
+
cursor = a + MOTION.stagger;
|
|
80
|
+
arrived = b;
|
|
81
|
+
changes = 0;
|
|
82
|
+
out.busy = Math.max(out.busy, b);
|
|
83
|
+
} else if (event.type === "action") {
|
|
84
|
+
out.events.push({ event: e, at: cursor });
|
|
85
|
+
out.busy = Math.max(out.busy, cursor + MOTION.action);
|
|
86
|
+
cursor += MOTION.action;
|
|
87
|
+
changes = 0;
|
|
88
|
+
} else {
|
|
89
|
+
const at = (arrived ?? cursor) + MOTION.change * changes++;
|
|
90
|
+
out.events.push({ event: e, at });
|
|
91
|
+
out.busy = Math.max(out.busy, at);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildTimeline(diagram: Diagram): Timeline {
|
|
98
|
+
const plans = diagram.steps.map(plan);
|
|
99
|
+
const steps: Span[] = [];
|
|
100
|
+
let t = MOTION.intro;
|
|
101
|
+
diagram.steps.forEach((step, s) => {
|
|
102
|
+
const read = Math.min(MOTION.stepMax, Math.max(MOTION.stepMin, MOTION.stepBase + [...step.body].length * MOTION.perChar));
|
|
103
|
+
const length = Math.max(read, (plans[s]?.busy ?? 0) + MOTION.dwell);
|
|
104
|
+
steps.push({ start: t, end: t + length });
|
|
105
|
+
t += length;
|
|
106
|
+
});
|
|
107
|
+
const end = t;
|
|
108
|
+
const total = Math.ceil(end + MOTION.outro);
|
|
109
|
+
const last = total - MOTION.tail;
|
|
110
|
+
|
|
111
|
+
const edges: EdgeTime[] = [];
|
|
112
|
+
const labels: LabelTime[] = [];
|
|
113
|
+
const events: EventTime[] = [];
|
|
114
|
+
const departures: { step: number; event: number; who: "main" | "coin"; a: number; b: number }[] = [];
|
|
115
|
+
plans.forEach((p, s) => {
|
|
116
|
+
const span = steps[s] as Span;
|
|
117
|
+
const named: LabelTime[] = [];
|
|
118
|
+
for (const edge of p.edges) {
|
|
119
|
+
const a = span.start + edge.a;
|
|
120
|
+
edges.push({ step: s, event: edge.event, start: a, drawn: span.start + edge.b, end: span.end });
|
|
121
|
+
if (edge.carries) departures.push({ step: s, event: edge.event, who: edge.coin ? "coin" : "main", a, b: span.start + edge.b });
|
|
122
|
+
if (edge.named) named.push({ step: s, event: edge.event, start: a + MOTION.labelDelay, end: span.end });
|
|
123
|
+
}
|
|
124
|
+
// 3 つ目の名前が出る時に 1 つ目を消す
|
|
125
|
+
named.forEach((label, k) => {
|
|
126
|
+
const later = named[k + 2];
|
|
127
|
+
if (later) label.end = Math.min(label.end, later.start);
|
|
128
|
+
});
|
|
129
|
+
labels.push(...named);
|
|
130
|
+
for (const event of p.events) events.push({ step: s, event: event.event, at: span.start + event.at });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const first = steps[0]?.start ?? 0;
|
|
134
|
+
const hasMain = departures.some((d) => d.who === "main");
|
|
135
|
+
const carriers: CarrierTime[] = [];
|
|
136
|
+
for (const who of ["main", "coin"] as const) {
|
|
137
|
+
const list = departures.filter((d) => d.who === who);
|
|
138
|
+
list.forEach((d, k) => {
|
|
139
|
+
const next = list[k + 1];
|
|
140
|
+
// 主役の最初の運ぶ物は、最初の段の始まりから出発点で待つ (何も見えない時間を作らないため)
|
|
141
|
+
const waits = k === 0 && (who === "main" || !hasMain);
|
|
142
|
+
carriers.push({
|
|
143
|
+
step: d.step,
|
|
144
|
+
event: d.event,
|
|
145
|
+
who,
|
|
146
|
+
depart: d.a,
|
|
147
|
+
arrive: d.b,
|
|
148
|
+
start: waits ? Math.min(first, d.a) : d.a,
|
|
149
|
+
end: next ? Math.max(next.a, d.b) : last,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
carriers.sort((x, y) => x.depart - y.depart);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
total,
|
|
157
|
+
last,
|
|
158
|
+
steps,
|
|
159
|
+
edges,
|
|
160
|
+
carriers,
|
|
161
|
+
labels,
|
|
162
|
+
badges: steps.map((span, s) => ({ step: s, appears: span.start })),
|
|
163
|
+
events,
|
|
164
|
+
stills: steps.map((span) => span.end - MOTION.still),
|
|
165
|
+
};
|
|
166
|
+
}
|
package/src/tokenize.ts
CHANGED
|
@@ -1,289 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 記法を 1 行ずつに切る。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* ## なぜ package 側に置くか
|
|
8
|
-
*
|
|
9
|
-
* 「記法をどう切るか」 は parser と同じ知識で、色名の別名と矢印の書き方を `keywords.ts`、
|
|
10
|
-
* 正規の色名を `@cardenelabs/cdl` から直接引ける。 画面側に置くと語彙を写すことになり、
|
|
11
|
-
* 語が増えた時に片方だけ古くなる (#1293 / #1304 / #1306 で 3 度踏んだ形)。
|
|
12
|
-
*
|
|
13
|
-
* ## 何を切るか
|
|
14
|
-
*
|
|
15
|
-
* | 種類 | 例 | 図での意味 |
|
|
16
|
-
* |---|---|---|
|
|
17
|
-
* | `項目名` | `title:` / `kind:` / `- 名前:` | 構造。 色ではなく太さで示す |
|
|
18
|
-
* | `説明文` | `"ログイン"` / `: 検索` | 箱と矢印に出る文字 |
|
|
19
|
-
* | `矢印` | `->` / `→` | 繋がりそのもの |
|
|
20
|
-
* | `色名` | `success` / `成功` / `dotted-flow` | **その色で描く** (`色` に解決後の名前が入る) |
|
|
21
|
-
* | `値の参照` | `{count}` | 段で動く値 |
|
|
22
|
-
* | `注記` | `# ...` | 読み飛ばしてよい部分 |
|
|
23
|
-
*
|
|
24
|
-
* 色名だけ `色` を返すのは、画面が「その色名が図で引く色」 で描けるようにするため。
|
|
25
|
-
* 本文の `(成功)` と図の緑が一致すると、どの語がどの見た目に効くかが読める。
|
|
26
|
-
*
|
|
27
|
-
* ## 重なりを作らない
|
|
28
|
-
*
|
|
29
|
-
* 返す並びは開始位置の昇順で、**互いに重ならない**。 使う側は並びをそのまま
|
|
30
|
-
* `<span>` や装飾に変換するだけでよく、重なりの解決を持たなくて済む。
|
|
31
|
-
*
|
|
32
|
-
* 値の参照は説明文の中にも出る (`value: "{count}"`)。 その場合は説明文を参照の前後で
|
|
33
|
-
* 分割して返す = 参照だけ別の色にできる。
|
|
34
|
-
*/
|
|
35
|
-
import { TONE_ALIAS, ARROW_PATTERNS } from "./keywords";
|
|
36
|
-
import { TONES, EDGE_STYLES } from "@cardenelabs/cdl";
|
|
37
|
-
import type { Tone } from "@cardenelabs/cdl";
|
|
38
|
-
|
|
39
|
-
/** 分解した部分の種類 (#1310) */
|
|
40
|
-
export type トークンの種類 = "項目名" | "説明文" | "矢印" | "色名" | "値の参照" | "注記";
|
|
41
|
-
|
|
42
|
-
/** 分解した部分 1 つ。 位置は本文の先頭からの文字数 (`slice` にそのまま渡せる) */
|
|
43
|
-
export type トークン = {
|
|
44
|
-
種類: トークンの種類;
|
|
45
|
-
開始: number;
|
|
46
|
-
終わり: number;
|
|
47
|
-
/** `色名` の時だけ入る、解決後の色の名前。 画面はこれで「その色」 を引く */
|
|
48
|
-
色?: Tone;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 線種の一覧 (#1310 → #1466)。
|
|
53
|
-
*
|
|
54
|
-
* 描画側 (`EDGE_STYLES`) から導く。 手で並べると、描画側が線種を増やした時に書けないままになる
|
|
55
|
-
* (実測 = `dashed` を足した時に 3 箇所のうち 2 箇所が古いままだった)。
|
|
56
|
-
*/
|
|
57
|
-
const 線種 = EDGE_STYLES;
|
|
58
|
-
|
|
59
|
-
/** 色名として読める語 (小文字で引く)。 正規の色名と別名の両方 */
|
|
60
|
-
const 色名の表 = new Map<string, Tone>([
|
|
61
|
-
...TONES.map((t) => [t.toLowerCase(), t] as [string, Tone]),
|
|
62
|
-
...Object.entries(TONE_ALIAS).map(([k, v]) => [k.toLowerCase(), v] as [string, Tone]),
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* 矢印の書き方。 並べ替えない。
|
|
67
|
-
*
|
|
68
|
-
* `->` と `->>` は同じ位置から始まるため、どちらを取るかを決める必要がある。 それは
|
|
69
|
-
* `重なりを外す` が「同じ開始なら長い方」 として 1 箇所で決める。 ここでも長い順に
|
|
70
|
-
* 並べると **同じ規則が 2 箇所に分かれ、片方を外しても検査が通る** (実測)。
|
|
71
|
-
*/
|
|
72
|
-
const 矢印の表 = [...new Set(ARROW_PATTERNS)];
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 色名として読める語なら、解決後の名前を返す。
|
|
76
|
-
*
|
|
77
|
-
* 判定は `resolveTone` と同じ (別名を引き、小文字に寄せる) が、引用符は外さない =
|
|
78
|
-
* 分解器は本文の位置を返すため、外した後の文字列を作ってはいけない。
|
|
79
|
-
*/
|
|
80
|
-
function 色名として読む(語: string): Tone | undefined {
|
|
81
|
-
return 色名の表.get(語.toLowerCase());
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 線種として読める語か */
|
|
85
|
-
function 線種として読むか(語: string): boolean {
|
|
86
|
-
return (線種 as readonly string[]).includes(語.toLowerCase());
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/*
|
|
90
|
-
* 箱の種類 (`service` / `database` 等) は色分けしない (#1310 review r1-f1)。
|
|
91
|
-
*
|
|
92
|
-
* 一度は種類名を `項目名` として扱ったが、**箱の名前まで巻き込む**。 受理する 108 種には
|
|
93
|
-
* `user` / `api` / `actor` / `card` / `state` / `service` が含まれ、`- User` や `- API` の
|
|
94
|
-
* ような普通の名前と衝突する (実測で両方とも項目名になった)。
|
|
95
|
-
*
|
|
96
|
-
* 正しく分けるには「種類の位置に書かれているか」 を見る必要があり、それは 5 種
|
|
97
|
-
* (項目名 / 説明文 / 矢印 / 色名 / 値の参照) の外側になる。 語だけで判定できないものは
|
|
98
|
-
* 色分けしない。
|
|
4
|
+
* 空行とコメントの行 (`//` で始まる行) は捨てる。行の頭と終わりの空白は意味を持たないので落とし、
|
|
5
|
+
* 誤りを示すために元の行の番号と、残した字が始まる列を持たせる。
|
|
6
|
+
* 行と列は 1 から数え、列は字の数で数える (UTF-16 の単位では数えない)。
|
|
99
7
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const t of [...候補].sort((a, b) => a.開始 - b.開始 || b.終わり - a.終わり)) {
|
|
105
|
-
if (t.終わり <= t.開始) continue;
|
|
106
|
-
const 直前 = out[out.length - 1];
|
|
107
|
-
if (直前 !== undefined && t.開始 < 直前.終わり) continue;
|
|
108
|
-
out.push(t);
|
|
109
|
-
}
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** `{名前}` を拾う。 名前は英数字と `_` だけ (描画側が置き換える時に見る範囲と揃える) */
|
|
114
|
-
function 値の参照を拾う(src: string, 起点: number, 本文: string, 積む: (t: トークン) => void): void {
|
|
115
|
-
for (const m of 本文.matchAll(/\{[A-Za-z_][\w]*\}/g)) {
|
|
116
|
-
積む({ 種類: "値の参照", 開始: 起点 + m.index, 終わり: 起点 + m.index + m[0].length });
|
|
117
|
-
}
|
|
8
|
+
export interface SourceLine {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
text: string;
|
|
118
12
|
}
|
|
119
13
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* 割らずに 1 つの説明文として返すと、`value: "{count}"` の参照に色を当てられない。
|
|
124
|
-
*/
|
|
125
|
-
function 説明文を拾う(src: string, 起点: number, 本文: string, 積む: (t: トークン) => void): void {
|
|
126
|
-
for (const m of 本文.matchAll(/"[^"]*"|'[^']*'/g)) {
|
|
127
|
-
const 開始 = 起点 + m.index;
|
|
128
|
-
const 終わり = 開始 + m[0].length;
|
|
129
|
-
const 参照: トークン[] = [];
|
|
130
|
-
値の参照を拾う(src, 開始, m[0], (t) => 参照.push(t));
|
|
131
|
-
if (参照.length === 0) {
|
|
132
|
-
積む({ 種類: "説明文", 開始, 終わり });
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
let 位置 = 開始;
|
|
136
|
-
for (const r of 参照.sort((a, b) => a.開始 - b.開始)) {
|
|
137
|
-
if (r.開始 > 位置) 積む({ 種類: "説明文", 開始: 位置, 終わり: r.開始 });
|
|
138
|
-
積む(r);
|
|
139
|
-
位置 = r.終わり;
|
|
140
|
-
}
|
|
141
|
-
if (位置 < 終わり) 積む({ 種類: "説明文", 開始: 位置, 終わり });
|
|
142
|
-
}
|
|
14
|
+
/** `text` の `index` 番目 (UTF-16 の位置) にある字が、行の何列目かを返す。 */
|
|
15
|
+
export function columnOf(source: SourceLine, index: number): number {
|
|
16
|
+
return source.column + Array.from(source.text.slice(0, index)).length;
|
|
143
17
|
}
|
|
144
18
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
let 起点 = 0;
|
|
156
|
-
for (const 行 of src.split("\n")) {
|
|
157
|
-
分解する1行(src, 起点, 行, 積む);
|
|
158
|
-
起点 += 行.length + 1;
|
|
159
|
-
}
|
|
160
|
-
return 重なりを外す(候補);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function 分解する1行(
|
|
164
|
-
src: string,
|
|
165
|
-
起点: number,
|
|
166
|
-
行: string,
|
|
167
|
-
積む: (t: トークン) => void,
|
|
168
|
-
): void {
|
|
169
|
-
const 字下げ = 行.length - 行.trimStart().length;
|
|
170
|
-
const 本体 = 行.trimStart();
|
|
171
|
-
if (本体 === "") return;
|
|
172
|
-
|
|
173
|
-
// 注記は行ごと。 `#` を色番号 (`#f59e0b`) と取り違えないよう、行の頭だけを見る
|
|
174
|
-
if (本体.startsWith("#")) {
|
|
175
|
-
積む({ 種類: "注記", 開始: 起点 + 字下げ, 終わり: 起点 + 行.length });
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 説明文と値の参照を先に取る = 引用符の中の `->` や色名を拾わないため
|
|
180
|
-
説明文を拾う(src, 起点, 行, 積む);
|
|
181
|
-
値の参照を拾う(src, 起点, 行, 積む);
|
|
182
|
-
|
|
183
|
-
// 項目名 = 行頭 (`- ` の後も含む) の `名前:`
|
|
184
|
-
const 見出し = 本体.match(/^(-\s*)?([^\s:{}[\]"']+)\s*:/);
|
|
185
|
-
if (見出し) {
|
|
186
|
-
const 前 = 見出し[1]?.length ?? 0;
|
|
187
|
-
const 名 = 見出し[2] ?? "";
|
|
188
|
-
const 開始 = 起点 + 字下げ + 前;
|
|
189
|
-
積む({ 種類: "項目名", 開始, 終わり: 開始 + 名.length + 1 });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 矢印
|
|
193
|
-
for (const 記号 of 矢印の表) {
|
|
194
|
-
let i = 行.indexOf(記号);
|
|
195
|
-
while (i >= 0) {
|
|
196
|
-
積む({ 種類: "矢印", 開始: 起点 + i, 終わり: 起点 + i + 記号.length });
|
|
197
|
-
i = 行.indexOf(記号, i + 記号.length);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// 色名と線種。 語の切れ目で区切って 1 語ずつ見る = 説明文の一部を拾わないよう、
|
|
202
|
-
// 引用符の中は上で説明文として先に取ってある (重なりを外す時に落ちる)
|
|
203
|
-
for (const m of 行.matchAll(/[^\s(),:{}[\]"']+/g)) {
|
|
204
|
-
const 語 = m[0];
|
|
205
|
-
const 色 = 色名として読む(語);
|
|
206
|
-
if (色 !== undefined) {
|
|
207
|
-
積む({ 種類: "色名", 開始: 起点 + m.index, 終わり: 起点 + m.index + 語.length, 色 });
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (線種として読むか(語)) {
|
|
211
|
-
積む({ 種類: "色名", 開始: 起点 + m.index, 終わり: 起点 + m.index + 語.length });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* JSON を分解する (#1310)。
|
|
218
|
-
*
|
|
219
|
-
* 記法と違い鍵は英語で、記法の語彙は出ない。 したがって **鍵 / 文字列 / 数を汎用の色で塗る**。
|
|
220
|
-
*
|
|
221
|
-
* 例外は `tone` と `color` の値で、こちらは記法と同じ語彙を持つため色名として扱う。
|
|
222
|
-
* 同じ `"success"` が入口によって別の色になる状態を作らないため。
|
|
223
|
-
*/
|
|
224
|
-
export function JSONを分解する(src: string): トークン[] {
|
|
225
|
-
const 候補: トークン[] = [];
|
|
226
|
-
// 鍵 (`"name":`) と、その値が色名を取る欄かを見る
|
|
227
|
-
for (const m of src.matchAll(/"((?:[^"\\]|\\.)*)"(\s*:)?/g)) {
|
|
228
|
-
const 開始 = m.index;
|
|
229
|
-
const 終わり = 開始 + m[0].length;
|
|
230
|
-
if (m[2] !== undefined) {
|
|
231
|
-
候補.push({ 種類: "項目名", 開始, 終わり });
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
候補.push({ 種類: "説明文", 開始, 終わり });
|
|
235
|
-
}
|
|
236
|
-
// 数値も文字列と同じ「値」の色にする。 JSON の数値文法に合わせ、符号・小数・指数を拾う。
|
|
237
|
-
// 文字列の中に現れた数字は、先に積んだ説明文と重なるため `重なりを外す` で落ちる。
|
|
238
|
-
for (const m of src.matchAll(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g)) {
|
|
239
|
-
候補.push({ 種類: "説明文", 開始: m.index, 終わり: m.index + m[0].length });
|
|
240
|
-
}
|
|
241
|
-
// 色名を取る欄の値を色名に格上げする
|
|
242
|
-
for (const m of src.matchAll(/"(tone|color)"\s*:\s*"((?:[^"\\]|\\.)*)"/g)) {
|
|
243
|
-
const 値の開始 = m.index + m[0].lastIndexOf('"' + m[2] + '"');
|
|
244
|
-
const 値 = m[2] ?? "";
|
|
245
|
-
const 色 = 色名として読む(値);
|
|
246
|
-
if (色 === undefined) continue;
|
|
247
|
-
const i = 候補.findIndex((t) => t.開始 === 値の開始);
|
|
248
|
-
if (i >= 0) 候補[i] = { 種類: "色名", 開始: 値の開始, 終わり: 値の開始 + 値.length + 2, 色 };
|
|
249
|
-
}
|
|
250
|
-
// 値の参照は文字列の中に出る (`"{count}"`)
|
|
251
|
-
const 参照: トークン[] = [];
|
|
252
|
-
値の参照を拾う(src, 0, src, (t) => 参照.push(t));
|
|
253
|
-
for (const r of 参照) {
|
|
254
|
-
const i = 候補.findIndex((t) => t.開始 < r.開始 && r.終わり <= t.終わり && t.種類 === "説明文");
|
|
255
|
-
const 元 = i < 0 ? undefined : 候補[i];
|
|
256
|
-
if (元 === undefined) continue;
|
|
257
|
-
候補.splice(
|
|
258
|
-
i,
|
|
259
|
-
1,
|
|
260
|
-
{ 種類: "説明文", 開始: 元.開始, 終わり: r.開始 },
|
|
261
|
-
r,
|
|
262
|
-
{ 種類: "説明文", 開始: r.終わり, 終わり: 元.終わり },
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
return 重なりを外す(候補);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* 分解した並びを、本文を覆う区間の列に広げる (#1310)。
|
|
270
|
-
*
|
|
271
|
-
* 分解器は色が付く部分だけを返すため、間の文字が抜ける。 画面はそのまま並べると本文が
|
|
272
|
-
* 欠けるので、**色の付かない部分も種類なしの区間として挟む**。
|
|
273
|
-
*
|
|
274
|
-
* 返す区間は本文を過不足なく覆う (連結すると元の本文に戻る)。
|
|
275
|
-
*/
|
|
276
|
-
export function 区間に広げる(
|
|
277
|
-
src: string,
|
|
278
|
-
tokens: トークン[],
|
|
279
|
-
): Array<{ 文字: string; 種類?: トークンの種類; 色?: Tone }> {
|
|
280
|
-
const out: Array<{ 文字: string; 種類?: トークンの種類; 色?: Tone }> = [];
|
|
281
|
-
let 位置 = 0;
|
|
282
|
-
for (const t of tokens) {
|
|
283
|
-
if (t.開始 > 位置) out.push({ 文字: src.slice(位置, t.開始) });
|
|
284
|
-
out.push({ 文字: src.slice(t.開始, t.終わり), 種類: t.種類, 色: t.色 });
|
|
285
|
-
位置 = t.終わり;
|
|
286
|
-
}
|
|
287
|
-
if (位置 < src.length) out.push({ 文字: src.slice(位置) });
|
|
288
|
-
return out;
|
|
19
|
+
export function tokenize(notation: string): SourceLine[] {
|
|
20
|
+
const lines: SourceLine[] = [];
|
|
21
|
+
notation.split(/\r?\n/).forEach((raw, index) => {
|
|
22
|
+
const text = raw.trim();
|
|
23
|
+
if (text === "" || text.startsWith("//")) return;
|
|
24
|
+
const lead = raw.length - raw.trimStart().length;
|
|
25
|
+
lines.push({ line: index + 1, column: Array.from(raw.slice(0, lead)).length + 1, text });
|
|
26
|
+
});
|
|
27
|
+
return lines;
|
|
289
28
|
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 原本の形式の版。
|
|
3
|
+
*
|
|
4
|
+
* 道具より新しい版の図は描かずに断る。古い版の図は、版を 1 つずつ上げる変換を通して読み、
|
|
5
|
+
* 原本の file を書き換えるのは移行の命令 (`migrateFile`) だけにする。
|
|
6
|
+
* 今は版 1 しか無いため、版を上げる変換はまだ持たない。版 2 を作る時に、版 1 から 2 への変換と
|
|
7
|
+
* 古い版を読んだ時の警告をここに足す。
|
|
8
|
+
*/
|
|
9
|
+
import { FIELD_MESSAGES } from "./field-messages";
|
|
10
|
+
import { type CheckOptions, type Diagram, type FieldError, validateDiagram } from "./schema";
|
|
11
|
+
import type { NotationLang } from "./words";
|
|
12
|
+
|
|
13
|
+
/** この道具が読める原本の版 */
|
|
14
|
+
export const FORMAT_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
export type Read = { ok: true; diagram: Diagram; warnings: string[] } | { ok: false; errors: FieldError[] };
|
|
17
|
+
|
|
18
|
+
export interface Migrated {
|
|
19
|
+
changed: boolean;
|
|
20
|
+
from: number;
|
|
21
|
+
to: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function newerThanTool(input: unknown): number | undefined {
|
|
25
|
+
if (typeof input !== "object" || input === null) return undefined;
|
|
26
|
+
const version = (input as { version?: unknown }).version;
|
|
27
|
+
return typeof version === "number" && version > FORMAT_VERSION ? version : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const newerError = (version: number, lang: NotationLang): FieldError => ({
|
|
31
|
+
pointer: "/version",
|
|
32
|
+
message: FIELD_MESSAGES[lang].newer(version, FORMAT_VERSION),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** 原本を読む。道具より新しい版は描かずに断り、形の誤りは検査の誤りとして返す。誤りの文は `options.lang` (渡さなければ日本語) で出す。 */
|
|
36
|
+
export function readDiagram(input: unknown, options: CheckOptions = {}): Read {
|
|
37
|
+
const newer = newerThanTool(input);
|
|
38
|
+
if (newer !== undefined) return { ok: false, errors: [newerError(newer, options.lang ?? "ja")] };
|
|
39
|
+
const checked = validateDiagram(input, options);
|
|
40
|
+
if (!checked.ok) return checked;
|
|
41
|
+
return { ok: true, diagram: input as Diagram, warnings: [] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 原本の file を今の版へ移す。今の版の file には触れない。
|
|
46
|
+
*
|
|
47
|
+
* file を読むので、画面で描く入口からは呼ばない。node の file の命令は呼んだ時に初めて読み込む。
|
|
48
|
+
*/
|
|
49
|
+
export async function migrateFile(path: string): Promise<Migrated> {
|
|
50
|
+
const { readFile } = await import("node:fs/promises");
|
|
51
|
+
const input: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
52
|
+
const read = readDiagram(input);
|
|
53
|
+
if (!read.ok) throw new Error(`${path} を移せない: ${read.errors.map((error) => `${error.pointer} ${error.message}`).join(" / ")}`);
|
|
54
|
+
return { changed: false, from: read.diagram.version, to: FORMAT_VERSION };
|
|
55
|
+
}
|
package/src/view.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 見せ方を字の大きさで選ぶ。
|
|
3
|
+
*
|
|
4
|
+
* 全体を枠の幅へ収めた時の箱の名前の字が 10.5px 以上なら、全体を止めて中だけ動かす (`fixed`)。
|
|
5
|
+
* 下回るなら、動く所へ寄って追い、隅の全体図で今どこかを示す (`follow`)。
|
|
6
|
+
* 書き手が原本の `view` で決めた時は、字の大きさに関わらずそれに従う。
|
|
7
|
+
*/
|
|
8
|
+
import { READABLE_PX, TYPE } from "./look/values";
|
|
9
|
+
import type { Rendered } from "./render/svg";
|
|
10
|
+
import type { ViewMode } from "./schema";
|
|
11
|
+
|
|
12
|
+
export function chooseView(rendered: Pick<Rendered, "layout" | "view">, frameWidth: number): { mode: ViewMode; textPx: number } {
|
|
13
|
+
const textPx = (Math.max(0, frameWidth) / rendered.layout.width) * TYPE.boxName.size;
|
|
14
|
+
if (rendered.view) return { mode: rendered.view, textPx };
|
|
15
|
+
return { mode: textPx >= READABLE_PX ? "fixed" : "follow", textPx };
|
|
16
|
+
}
|
package/src/words.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 記法の語。役割ごとに日本語と英語の語を組で持つ (#2997)。
|
|
3
|
+
*
|
|
4
|
+
* 英語の語は原本の JSON が使う名前に揃え、記法と JSON で覚える語を 1 組にする。
|
|
5
|
+
* 1 つの file の言葉は最初の行の図の種類の語で決まり、読む仕組みと書き戻す仕組みは
|
|
6
|
+
* その言葉の語だけをこの表から引く。
|
|
7
|
+
*
|
|
8
|
+
* 型が組の欠けを止める。原本の JSON の値 (図の種類、運ぶ物、見せ方、名前の側) に語が無い時と、
|
|
9
|
+
* どちらかの言葉の語が無い役割がある時は、型の検査が落ちる。
|
|
10
|
+
*/
|
|
11
|
+
import type { Carry, Diagram, LabelSide, ViewMode } from "./schema";
|
|
12
|
+
|
|
13
|
+
export type NotationLang = "ja" | "en";
|
|
14
|
+
|
|
15
|
+
type Pair = Readonly<Record<NotationLang, string>>;
|
|
16
|
+
|
|
17
|
+
export const WORDS = {
|
|
18
|
+
/** 最初の行の図の種類 */
|
|
19
|
+
kind: { sequence: { ja: "時系列図", en: "sequence" }, architecture: { ja: "構成図", en: "architecture" } } satisfies Record<Diagram["kind"], Pair>,
|
|
20
|
+
/** 段の見出しより前の行の頭 */
|
|
21
|
+
head: {
|
|
22
|
+
region: { ja: "区画", en: "region" },
|
|
23
|
+
strip: { ja: "帯", en: "strip" },
|
|
24
|
+
view: { ja: "見せ方", en: "view" },
|
|
25
|
+
} satisfies Record<string, Pair>,
|
|
26
|
+
/** 行の終わりの `[<鍵> <値>]` の鍵 */
|
|
27
|
+
attr: {
|
|
28
|
+
color: { ja: "色", en: "color" },
|
|
29
|
+
icon: { ja: "印", en: "icon" },
|
|
30
|
+
region: { ja: "区画", en: "region" },
|
|
31
|
+
label: { ja: "名前", en: "label" },
|
|
32
|
+
} satisfies Record<string, Pair>,
|
|
33
|
+
/** 線の上を走る物 */
|
|
34
|
+
carry: {
|
|
35
|
+
envelope: { ja: "封筒", en: "envelope" },
|
|
36
|
+
tag: { ja: "札", en: "tag" },
|
|
37
|
+
coin: { ja: "硬貨", en: "coin" },
|
|
38
|
+
none: { ja: "なし", en: "none" },
|
|
39
|
+
} satisfies Record<Carry["kind"], Pair>,
|
|
40
|
+
/** 見せ方の値 */
|
|
41
|
+
view: { fixed: { ja: "止める", en: "fixed" }, follow: { ja: "寄る", en: "follow" } } satisfies Record<ViewMode, Pair>,
|
|
42
|
+
/** 線の名前を置く側 */
|
|
43
|
+
side: {
|
|
44
|
+
above: { ja: "上", en: "above" },
|
|
45
|
+
below: { ja: "下", en: "below" },
|
|
46
|
+
left: { ja: "左", en: "left" },
|
|
47
|
+
right: { ja: "右", en: "right" },
|
|
48
|
+
} satisfies Record<LabelSide, Pair>,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
export type WordGroup = keyof typeof WORDS;
|
|
52
|
+
|
|
53
|
+
/** `group` の役割の `value` を、`lang` の言葉の語にする */
|
|
54
|
+
export function wordOf<G extends WordGroup>(group: G, value: keyof (typeof WORDS)[G], lang: NotationLang): string {
|
|
55
|
+
return ((WORDS[group] as Record<string, Pair>)[value as string] as Pair)[lang];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `lang` の言葉の語 `word` が、`group` のどの値を指すか。語でなければ `undefined` */
|
|
59
|
+
export function valueOf<G extends WordGroup>(group: G, word: string, lang: NotationLang): keyof (typeof WORDS)[G] | undefined {
|
|
60
|
+
const found = Object.entries(WORDS[group] as Record<string, Pair>).find(([, pair]) => pair[lang] === word);
|
|
61
|
+
return found?.[0] as keyof (typeof WORDS)[G] | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const otherLang = (lang: NotationLang): NotationLang => (lang === "ja" ? "en" : "ja");
|
|
65
|
+
|
|
66
|
+
/** 記法の最初の行 (空行とコメントの行を除く) の図の種類の語から、言葉を決める。決まらなければ `undefined` */
|
|
67
|
+
export function languageOf(headLine: string): NotationLang | undefined {
|
|
68
|
+
const first = headLine.trim().split(/\s+/)[0] ?? "";
|
|
69
|
+
return (["ja", "en"] as const).find((lang) => valueOf("kind", first, lang) !== undefined);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 記法の言葉。最初の行の図の種類の語で決まり、決まらなければ `undefined` */
|
|
73
|
+
export function notationLanguage(notation: string): NotationLang | undefined {
|
|
74
|
+
const head = notation
|
|
75
|
+
.split(/\r?\n/)
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.find((line) => line !== "" && !line.startsWith("//"));
|
|
78
|
+
return head === undefined ? undefined : languageOf(head);
|
|
79
|
+
}
|