@cardenelabs/dragon 0.28.0 → 0.32.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 +391 -16
- package/dist/index.cjs +13479 -7403
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2360 -208
- package/dist/index.d.ts +2360 -208
- package/dist/index.js +13462 -7405
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/compile/actor-ids.ts +297 -0
- package/src/compile/actor-option-notice.ts +379 -0
- package/src/compile/actors.ts +103 -0
- package/src/compile/c4.ts +74 -0
- package/src/compile/chain.ts +93 -0
- package/src/compile/chart-fields.ts +193 -0
- package/src/compile/deep-rewrite.ts +30 -0
- package/src/compile/derived-source.ts +18 -0
- package/src/compile/direction.ts +102 -0
- package/src/compile/edge-option-notice.ts +112 -0
- package/src/compile/er-relation.ts +200 -0
- package/src/compile/flow.ts +54 -0
- package/src/compile/flowchart.ts +344 -0
- package/src/compile/funnel.ts +87 -0
- package/src/compile/gantt.ts +337 -0
- package/src/compile/generic.ts +696 -0
- package/src/compile/hierarchy.ts +92 -0
- package/src/compile/journey.ts +93 -0
- package/src/compile/kinds.ts +148 -0
- package/src/compile/lanes.ts +138 -0
- package/src/compile/metro.ts +271 -0
- package/src/compile/mind.ts +393 -0
- package/src/compile/node-title.ts +24 -0
- package/src/compile/notice.ts +141 -0
- package/src/compile/order.ts +62 -0
- package/src/compile/parts-lookup.ts +17 -0
- package/src/compile/parts.ts +2709 -0
- package/src/compile/placement-apply.ts +689 -0
- package/src/compile/placement.ts +15 -0
- package/src/compile/quadrant.ts +265 -0
- package/src/compile/record.ts +75 -0
- package/src/compile/row-marks.ts +128 -0
- package/src/compile/rows.ts +40 -0
- package/src/compile/sequence.ts +305 -0
- package/src/compile/slug.ts +37 -0
- package/src/compile/stages.ts +247 -0
- package/src/compile/subtitle.ts +35 -0
- package/src/compile/swimlane-marks.ts +87 -0
- package/src/compile/swimlane.ts +82 -0
- package/src/compile/timeline.ts +213 -0
- package/src/compile/topology.ts +44 -0
- package/src/compile/tree.ts +69 -0
- package/src/compile/value-chart.ts +337 -0
- package/src/compile/values.ts +432 -0
- package/src/compile/word-state.ts +82 -0
- package/src/compile.ts +1806 -6971
- package/src/focus.ts +2 -2
- package/src/index.ts +42 -9
- package/src/json-line-map.ts +30 -0
- package/src/json-parser.ts +1760 -168
- package/src/keywords.ts +251 -17
- package/src/near-name.ts +42 -0
- package/src/notation-lint.ts +41 -64
- package/src/relative-pos.ts +71 -2
- package/src/schema.ts +1 -1
- package/src/schemas/diagram.json +680 -23
- package/src/types.ts +337 -36
- package/src/v05/parser.ts +2553 -354
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { DslDocument } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* 図表の欄を読む小道具 (#2030 で `compile.ts` から移した)。
|
|
4
|
+
*
|
|
5
|
+
* 値の図 (`pie` / `bar` / `line` ほか 6 種) ・ 漏斗 ・ 道のり ・ 四象限 の 4 系統が使う。
|
|
6
|
+
* 数と語の読み方を 1 箇所に置くのは、図種ごとに別の読み方をすると同じ本文が図種で
|
|
7
|
+
* 別の値に化けるため。
|
|
8
|
+
*
|
|
9
|
+
* **葉に置く** = ここが `compile.ts` を取り込まないので、どちらから呼んでも輪にならない。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 割合の書き方から数値を読む。 読めなければ `null`。
|
|
14
|
+
*
|
|
15
|
+
* 受けるのは `"45%"` / `"45"` / `"45.5%"` と、 前後の空白。 `"四割"` や `"0.45"` のような
|
|
16
|
+
* 別の言い方は読まない = **黙って 0 にすると、 その分だけ欠けた円が「正しい図」 として出る**。
|
|
17
|
+
* 読めなかったことは呼出側が警告に出す。
|
|
18
|
+
*/
|
|
19
|
+
export function parseShareValue(raw: string | undefined): number | null {
|
|
20
|
+
if (raw === undefined) return null;
|
|
21
|
+
const m = raw.trim().match(/^(-?\d+(?:\.\d+)?)\s*%?$/);
|
|
22
|
+
if (m === null) return null;
|
|
23
|
+
const v = Number(m[1]);
|
|
24
|
+
return Number.isFinite(v) ? v : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 状態を読む欄かどうか (`{名前}`)。
|
|
29
|
+
*
|
|
30
|
+
* **`{名前}` そのものだけを受ける**。 `{v} 件` のような混ざった形は、描画側が数として
|
|
31
|
+
* 読めず既定値に落ちて印が付くだけになる (`render/payload-binding.ts` は解いた文字列を
|
|
32
|
+
* そのまま数にする)。 書けたのに効かない形を作らない。
|
|
33
|
+
*
|
|
34
|
+
* `%` を付けた形も受けない。 同じ理由で `"45%"` は数に直せるが `"{v}%"` は直せない。
|
|
35
|
+
*
|
|
36
|
+
* 名前に使えるのは英数字と `_` で、読む側 (cdl の `interpolate`) と同じ範囲に合わせる。
|
|
37
|
+
* 決まった accessor (`.sum` 等) は付けてよい。
|
|
38
|
+
*/
|
|
39
|
+
export function parseBoundValue(raw: string | undefined): string | null {
|
|
40
|
+
if (raw === undefined) return null;
|
|
41
|
+
const t = raw.trim();
|
|
42
|
+
return /^\{\w+(?:\.(?:length|sum|max|min|avg)|\[\d+\])?\}$/.test(t) ? t : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 図表の数の欄を読む。 数そのものか、状態を読む `{名前}` を返す。
|
|
47
|
+
*
|
|
48
|
+
* 数として解けない `{名前}` は、そのまま図表の中身に渡して描画側が段ごとに解く。
|
|
49
|
+
* 受け取る側の型 (`BoundNumber`) は元から 2 通りを想定している = 入口だけが塞がっていた。
|
|
50
|
+
*/
|
|
51
|
+
export function parseChartValue(raw: string | undefined): number | string | null {
|
|
52
|
+
const n = parseShareValue(raw);
|
|
53
|
+
if (n !== null) return n;
|
|
54
|
+
return parseBoundValue(raw);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 状態を読む欄が指している名前 (`{v.sum}` なら `v`)。 欄でなければ null */
|
|
58
|
+
export function 参照する名前(value: number | string | null): string | null {
|
|
59
|
+
if (typeof value !== "string") return null;
|
|
60
|
+
const m = value.match(/^\{(\w+)/);
|
|
61
|
+
return m ? m[1]! : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 図表の箱の大きさ (#1260)。
|
|
66
|
+
*
|
|
67
|
+
* **組立て API と同じ値を使う**。 別の値にすると、同じ内容を書いても描いた図の大きさが変わる
|
|
68
|
+
* (実測 = 記法の `funnel` は 640x368、組立て API は 560x480 で、描いた図の viewBox が
|
|
69
|
+
* 785x488 対 712x600 になっていた)。
|
|
70
|
+
*
|
|
71
|
+
* 組立て API 側は中身の件数で変えない (実測 = 2 / 4 / 8 件のどれでも同じ値)。 そのため
|
|
72
|
+
* こちらも定数で持つ。 `gantt` だけは件数で高さを変える = 8 件目から最後の帯が枠の外に
|
|
73
|
+
* 出るため (`compileGantt` の実測)、組立て API の固定 360 より正しい。
|
|
74
|
+
*
|
|
75
|
+
* **格子 (16) の倍数にする**。 描画側 (`cdl` の `chart()` preset) は高さを 16 の倍数へ切り上げる。
|
|
76
|
+
* 揃えないと下端が格子から外れ、 正しい記法でも位置の警告が出る (review 指摘、 360 のまま
|
|
77
|
+
* 5 型が該当していた)。
|
|
78
|
+
*/
|
|
79
|
+
export const 図表の大きさ = {
|
|
80
|
+
funnel: { w: 560, h: 480 },
|
|
81
|
+
// 木は幅を持たない (cdl#970)。 必要な幅は葉の数で変わるので、描画側の `木の札の幅` が出す。
|
|
82
|
+
// 値を残すと、描画側が割付を変えた時にここだけが古い幅を持つ
|
|
83
|
+
tree: { h: 480 },
|
|
84
|
+
mind: { w: 720, h: 480 },
|
|
85
|
+
journey: { w: 720, h: 480 },
|
|
86
|
+
quadrant: { w: 640, h: 480 },
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 記法に札の寸法があればそれを使い、無ければ図種ごとの既定をそのまま返す (#2854 C13)。
|
|
91
|
+
* `図表の大きさ` 自体は変更しない。欄を書かない既存の図が byte-level で同じ寸法を保つため。
|
|
92
|
+
*/
|
|
93
|
+
export function 図表の大きさを読む(
|
|
94
|
+
doc: DslDocument,
|
|
95
|
+
fallback: { w: number; h: number },
|
|
96
|
+
): { w: number; h: number } {
|
|
97
|
+
return doc.figureSize === undefined
|
|
98
|
+
? fallback
|
|
99
|
+
: { w: doc.figureSize.width, h: doc.figureSize.height };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 図表の欄が `{名前}` で読む値を、**1 か所で** 確かめる (#1200)。
|
|
104
|
+
*
|
|
105
|
+
* 図表には数の欄 (割合 / 段の人数) と語の欄 (気持ち / 区画) があり、どちらも `{名前}` で
|
|
106
|
+
* 状態を読める。 確かめることは欄の種類で違うが、**土台は同じ** = 同じ名前を 2 回書いた時に
|
|
107
|
+
* 後ろが効くこと、段で状態に入る値 (切り替え / 補間) も見ること、の 2 つ。
|
|
108
|
+
*
|
|
109
|
+
* #1198 と #1201 では欄ごとに検査を書き足しており、同じ土台を 3 度書いていた。 3 度とも
|
|
110
|
+
* review で同じ形の穴を指摘されている (最初の宣言で判定する / 段で入る値を見落とす)。
|
|
111
|
+
* 土台を 1 つにして、欄ごとの違いだけを外から渡す。
|
|
112
|
+
*
|
|
113
|
+
* ## 確かめること
|
|
114
|
+
*
|
|
115
|
+
* | 欄 | 通す値 | 段で入る値 |
|
|
116
|
+
* |---|---|---|
|
|
117
|
+
* | 数 | 数として読める (空文字は弾く、`Number("")` が 0 を返すため) | 切り替え先が数 |
|
|
118
|
+
* | 語 | 語表にある語 | 切り替え先が語表にあり、補間されない (補間の行き先は数) |
|
|
119
|
+
*
|
|
120
|
+
* 自動で決まる値 (`values:`) は式の評価結果で必ず数になるため、数の欄からは参照できて
|
|
121
|
+
* 語の欄からは参照できない。 式そのものの不備 (語を読む / 名前が無い) は
|
|
122
|
+
* `value-unresolved` の警告が別に出る (実測で確認済)。
|
|
123
|
+
*
|
|
124
|
+
* ## 責務境界 (#1198 / #1200)
|
|
125
|
+
*
|
|
126
|
+
* **見るのは組み立ての時点で決まっている範囲だけ**。 記法の値は実行時に決まるため、ここで
|
|
127
|
+
* 全部を判定しようとすると式の評価を組み立て側で再現することになる。 実際に #1199 の review で
|
|
128
|
+
* 4 round 続けて同じ形の指摘が出て収束せず、境界を決めて切り分けた (穴を 1 つ塞ぐと別の形が
|
|
129
|
+
* 出る = 塞ぎ方ではなく責務の置き場所の問題だった)。
|
|
130
|
+
*
|
|
131
|
+
* | 見る | 見ない | 見ない理由 |
|
|
132
|
+
* |---|---|---|
|
|
133
|
+
* | 名前が宣言されているか | 段の行き先が負になる形 | 描画側が問題なく描く (負の大きさも `NaN` も出ないことを実測) |
|
|
134
|
+
* | 宣言の時点で読める値か | 式が実行時に返す値 | 式の不備は `value-unresolved` の警告が別に出る (実測で確認) |
|
|
135
|
+
* | 段で状態に入る値 | | |
|
|
136
|
+
*
|
|
137
|
+
* 見ない範囲は描画側が受け持つ = 解けない値は既定値で描いて `data-cdl-unresolved` を付ける。
|
|
138
|
+
*/
|
|
139
|
+
export function 図表の欄から参照できる名前(
|
|
140
|
+
doc: DslDocument,
|
|
141
|
+
欄: {
|
|
142
|
+
読めるか: (v: number | string) => boolean;
|
|
143
|
+
補間で壊れるか: boolean;
|
|
144
|
+
自動の値を許すか: boolean;
|
|
145
|
+
},
|
|
146
|
+
): Set<string> {
|
|
147
|
+
// 同じ名前を 2 回宣言した時は後ろが効く。 描画側が後の宣言を有効値として扱うため、
|
|
148
|
+
// 前の宣言で判定すると「読めると判定したのに読めない値が入る」 状態になる (実測)
|
|
149
|
+
const 実効 = new Map<string, number | string>();
|
|
150
|
+
for (const s of doc.animate?.states ?? []) 実効.set(s.name, s.initial);
|
|
151
|
+
|
|
152
|
+
const 壊れる = new Set<string>();
|
|
153
|
+
for (const p of doc.animate?.phases ?? []) {
|
|
154
|
+
for (const st of p.sets ?? []) if (!欄.読めるか(st.value)) 壊れる.add(st.state);
|
|
155
|
+
if (欄.補間で壊れるか) for (const tw of p.tweens ?? []) 壊れる.add(tw.state);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const out = new Set<string>();
|
|
159
|
+
for (const [名前, 値] of 実効) {
|
|
160
|
+
if (!欄.読めるか(値)) continue;
|
|
161
|
+
if (壊れる.has(名前)) continue;
|
|
162
|
+
out.add(名前);
|
|
163
|
+
}
|
|
164
|
+
if (欄.自動の値を許すか) for (const v of doc.values ?? []) out.add(v.name);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 数の欄が読める値か。 空文字と空白だけは弾く (`Number("")` は 0 を返す) */
|
|
169
|
+
export function 数として読めるか(v: number | string): boolean {
|
|
170
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
171
|
+
const t = v.trim();
|
|
172
|
+
if (t === "") return false;
|
|
173
|
+
return Number.isFinite(Number(t));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function 数の欄から参照できる名前(doc: DslDocument): Set<string> {
|
|
177
|
+
return 図表の欄から参照できる名前(doc, {
|
|
178
|
+
読めるか: 数として読めるか,
|
|
179
|
+
// 補間の行き先は数なので、数の欄では壊れない
|
|
180
|
+
補間で壊れるか: false,
|
|
181
|
+
自動の値を許すか: true,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function 語の欄から参照できる名前(doc: DslDocument, 語表: Map<string, string>): Set<string> {
|
|
186
|
+
return 図表の欄から参照できる名前(doc, {
|
|
187
|
+
読めるか: (v) => 語表.has(String(v).trim()),
|
|
188
|
+
// 補間の行き先は数。 語の欄が読む状態を補間すると、その段で語が数に変わる
|
|
189
|
+
補間で壊れるか: true,
|
|
190
|
+
// 式の評価結果は数になるため、語の欄からは読めない
|
|
191
|
+
自動の値を許すか: false,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 入れ子の中の文字だけを書き換える (#2034 で `compile.ts` から移した)。
|
|
3
|
+
*
|
|
4
|
+
* **葉に置く** = 入口 (`compileToCdl`) と部品の取り込み (`mergePartIntoDiagram`) の両方が呼ぶ。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 形 (`shape`) と数値の表示 (`readout`) の入れ子にある文字の葉を、全て書き換え関数に通す。
|
|
9
|
+
* 文字でない葉 (数 / 真偽 / null) はそのまま残す。
|
|
10
|
+
* 同じ object を 2 度辿らないよう `WeakSet` で覚える = 今の形は木なので輪にはならないが、
|
|
11
|
+
* 輪ができた時に戻らなくなるのを防ぐため。
|
|
12
|
+
*/
|
|
13
|
+
export function deepRewriteStrings(
|
|
14
|
+
value: unknown,
|
|
15
|
+
rewrite: (s: string | undefined) => string | undefined,
|
|
16
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
17
|
+
): unknown {
|
|
18
|
+
if (typeof value === "string") return rewrite(value) ?? value;
|
|
19
|
+
if (value === null || typeof value !== "object") return value;
|
|
20
|
+
if (seen.has(value)) return value;
|
|
21
|
+
seen.add(value);
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.map((v) => deepRewriteStrings(v, rewrite, seen));
|
|
24
|
+
}
|
|
25
|
+
const out: Record<string, unknown> = {};
|
|
26
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
27
|
+
out[k] = deepRewriteStrings(v, rewrite, seen);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 値の宣言がどの行から来たかを控える (#2034 で `compile.ts` から移した)。
|
|
3
|
+
*
|
|
4
|
+
* **葉に置く** = 入口 (`compileToCdl`) と部品の取り込み (`mergePartIntoDiagram`) の両方が呼ぶ。
|
|
5
|
+
* 見本を重ねる間だけ別表で宣言元を持ち回るため、どちらの側も同じ表へ書き足す。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** `derived` の同名宣言を、engine が読む順のまま行番号の列として残す。 */
|
|
9
|
+
export function recordDerivedSourceLine(
|
|
10
|
+
sourceLines: Map<string, number[]> | undefined,
|
|
11
|
+
id: string,
|
|
12
|
+
line: number,
|
|
13
|
+
): void {
|
|
14
|
+
if (!sourceLines) return;
|
|
15
|
+
const lines = sourceLines.get(id) ?? [];
|
|
16
|
+
lines.push(line);
|
|
17
|
+
sourceLines.set(id, lines);
|
|
18
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { DslDocument, PresetType } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* 図を並べる向きを決める小道具 (#2030 で `compile.ts` から移した)。
|
|
4
|
+
*
|
|
5
|
+
* 向きを書ける図種と、その既定、書いた向きの読み取り、後ろへ戻る矢印の判定。
|
|
6
|
+
*
|
|
7
|
+
* **葉に置く** = 図種ごとの組み立て器と、`compile.ts` に残る「書いたのに効かない」 の知らせの
|
|
8
|
+
* 両方が呼ぶ。 どちらかの側に置くと、もう片方から逆向きの取り込みが生まれる。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** 段の有無で経路が分かれる図種。 組み立ての入口が渡す種類と同じ綴り */
|
|
12
|
+
export type GenericKind = "flow" | "swimlane" | "record" | "topology";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 向きを選べる図種 (#1494、 #2524 で `flowchart` を足した)。
|
|
16
|
+
*
|
|
17
|
+
* **並び方そのものが読み方を担う図種は外す**。 行を持つ図は「1 縦列 1 箱」、クラス図は
|
|
18
|
+
* 設計が格子に置く形、順序図は 1 枚の板で、どれも向きを入れ替えると図の意味が変わる。
|
|
19
|
+
*
|
|
20
|
+
* `topology` も外す = 入れ物 (`contain`) を持つ図で、縦列の中に箱を囲む作りが向きと結びついている。
|
|
21
|
+
*
|
|
22
|
+
* 分かれ道の図は #2513 で足した時に外していた。 描く側の入口が向きを受けなかったためで、
|
|
23
|
+
* 図の意味が向きと結びついているからではない。 入口が受けるようになったので足す
|
|
24
|
+
* ([cardene777/cdl#880](https://github.com/cardene777/cdl/issues/880))。
|
|
25
|
+
*/
|
|
26
|
+
export const 向きを選べる図種: ReadonlySet<PresetType> = new Set<PresetType>([
|
|
27
|
+
"flow",
|
|
28
|
+
"swimlane",
|
|
29
|
+
"flowchart",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 縦列を書いた図でも向きが勝つ図種 (#2524)。
|
|
34
|
+
*
|
|
35
|
+
* 他の図種は、全ての箱が縦列を書いた時に **縦列のほうが勝つ**。 書いた縦列に箱を置く形が
|
|
36
|
+
* 向きより具体的な指定だからで、その時は書いた向きが捨てられたと知らせる。
|
|
37
|
+
*
|
|
38
|
+
* 分かれ道の図だけは逆にする。 この図種は **縦列が役割** で、書く人はほぼ必ず全ての箱に
|
|
39
|
+
* 役割を書く。 縦列が勝つ形にすると向きの行が 1 度も効かず、選べるようにした意味が消える。
|
|
40
|
+
*/
|
|
41
|
+
export const 縦列より向きが勝つ図種: ReadonlySet<PresetType> = new Set<PresetType>(["flowchart"]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* その図種の既定の向き (#2424 で `flow` を横にした)。
|
|
45
|
+
*
|
|
46
|
+
* **フローは横に並べる**。 縦に積むと描く広さが幅より高くなり、一覧の台は幅に合わせて
|
|
47
|
+
* 伸ばすので縦に長い図になる (実測 = 見本の頁の「フロー」 が 576×920 で、器 1150×630px に
|
|
48
|
+
* 収まらなかった)。 横に並べると 1 人 1 縦列になり、幅の側へ伸びる。
|
|
49
|
+
*
|
|
50
|
+
* `topology` だけ縦のまま = 入れ物 (`contain`) を持つ図で、縦列の中に箱を囲む作りが
|
|
51
|
+
* 向きと結びついている。
|
|
52
|
+
*
|
|
53
|
+
* **動きを書かないフローはここを読まない**。 その形は箱を鎖のように繋ぐ別の組み立てを通る
|
|
54
|
+
* (`鎖でつなぐ形か`)。 既定を変えても鎖の並びは 1 件も動かない。
|
|
55
|
+
*/
|
|
56
|
+
export function 既定の向き(kind: PresetType): "縦" | "横" {
|
|
57
|
+
return kind === "topology" ? "縦" : "横";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 実際に使う向き (#1494)。
|
|
62
|
+
*
|
|
63
|
+
* 書いていない図と、効かない図種に書いた図は既定のまま = **書かない図の並びは 1 つも動かない**。
|
|
64
|
+
*/
|
|
65
|
+
export function 並べる向き(kind: PresetType, doc: DslDocument): "縦" | "横" {
|
|
66
|
+
if (doc.direction === undefined) return 既定の向き(kind);
|
|
67
|
+
if (!向きを選べる図種.has(kind)) return 既定の向き(kind);
|
|
68
|
+
return doc.direction;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 後ろへ戻る矢印か (#1260 / #2782)。
|
|
73
|
+
*
|
|
74
|
+
* 前の箱へ戻る矢印は **箱の上を回して** 描く (`routing: "back-detour"`)。 組立て API 側が
|
|
75
|
+
* そうしており、記法側で付けないと **描いた図の高さが変わる**
|
|
76
|
+
* (実測 = viewBox が 404 対 486 で、戻る矢印が箱の右横を回っていた)。
|
|
77
|
+
*
|
|
78
|
+
* 判定は並び順。 指す先が指す元より前にあれば戻る矢印
|
|
79
|
+
* (実測 = `stateMachine()` は c -> a と c -> b の 2 本だけに付け、a -> b と b -> c には付けない)。
|
|
80
|
+
*
|
|
81
|
+
* **縦列を書いた図には付けない** (#2782)。 回すのは「書いた順に並ぶから、戻る線が箱を
|
|
82
|
+
* 貫く」 ための手当てで、縦列と段を書いた図は書いた人が置き場所を決めている。
|
|
83
|
+
* 畳む前の表の図は縦列を書く形が多く、そこへ回し始めると **線が往復して戻る弧になり**
|
|
84
|
+
* 関係の無い箱を貫いた (実測 = `利用者 -> 会員証` が同じ点へ 2 度戻る形になり、
|
|
85
|
+
* 配置の知らせに `overlap node ∩ edge-path` が出た)。
|
|
86
|
+
*
|
|
87
|
+
* **行を持つ図だけに付ける**。 他の図種の組立て API は付けない。
|
|
88
|
+
*/
|
|
89
|
+
export function 後ろへ戻る矢印か(
|
|
90
|
+
kind: GenericKind,
|
|
91
|
+
fromId: string,
|
|
92
|
+
toId: string,
|
|
93
|
+
箱の並び: ReadonlyMap<string, number>,
|
|
94
|
+
縦列を書いた: boolean,
|
|
95
|
+
): boolean {
|
|
96
|
+
if (kind !== "record") return false;
|
|
97
|
+
if (縦列を書いた) return false;
|
|
98
|
+
const 元 = 箱の並び.get(fromId);
|
|
99
|
+
const 先 = 箱の並び.get(toId);
|
|
100
|
+
if (元 === undefined || 先 === undefined) return false;
|
|
101
|
+
return 先 < 元;
|
|
102
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 矢印に書いた飾りが効かない時の、伝える欄の割り出しと呼び名 (#2366 で 1 箇所にまとめた)。
|
|
3
|
+
*
|
|
4
|
+
* 同じ判定を 3 経路が使う。
|
|
5
|
+
*
|
|
6
|
+
* | 経路 | 図種 | 起点 |
|
|
7
|
+
* |---|---|---|
|
|
8
|
+
* | 板の言づて | `sequence` / `solidity` | #2356 |
|
|
9
|
+
* | 骨格の図 | `tree` / `mind` | #2364 |
|
|
10
|
+
* | ガントチャート | `gantt` | #2366 |
|
|
11
|
+
*
|
|
12
|
+
* **葉に置く**。 板と骨格の判定は `compile.ts` に、ガントチャートの知らせは `compile/gantt.ts` にあり、
|
|
13
|
+
* 取り込む向きが逆なので、共通部分をどちらからも取り込める場所へ置く。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 効かない矢印の欄の呼び名 (#2356)。 **並べた順に知らせへ出す**。
|
|
18
|
+
*
|
|
19
|
+
* 呼び名を持たない欄は名前をそのまま出すので、ここに足し忘れても知らせは消えない。
|
|
20
|
+
* 複数の欄が 1 つの呼び名を持つ形 (名前のずらしが 3 欄) は、1 度だけ出す。
|
|
21
|
+
*
|
|
22
|
+
* **図種をまたいで 1 つの表にする** (#2364)。 同じ欄を図種ごとに別の文言で呼ぶと、
|
|
23
|
+
* 図種を変えた読み手が同じ飾りだと気付けない。 どの欄を伝えるかは図種ごとの除外が決める。
|
|
24
|
+
*/
|
|
25
|
+
export const 効かない矢印の欄の呼び名: readonly (readonly [string, string])[] = [
|
|
26
|
+
["label", "文字"],
|
|
27
|
+
["tone", "色味"],
|
|
28
|
+
["style", "線の種類"],
|
|
29
|
+
["sub", "添え字"],
|
|
30
|
+
["tailSub", "出どころ側の添え字"],
|
|
31
|
+
["side", "寄せ"],
|
|
32
|
+
// 名前のずらしも板には載せる先が無い (#1971)。 位置のずらし (`pos`) は名前のずらしに足す欄なので同じ扱い
|
|
33
|
+
["labelOffsetX", "名前のずらし"],
|
|
34
|
+
["labelOffsetY", "名前のずらし"],
|
|
35
|
+
["layoutPos", "名前のずらし"],
|
|
36
|
+
["posX1", "線の固定座標"],
|
|
37
|
+
["posY1", "線の固定座標"],
|
|
38
|
+
["posX2", "線の固定座標"],
|
|
39
|
+
["posY2", "線の固定座標"],
|
|
40
|
+
["waypoints", "線の固定座標"],
|
|
41
|
+
["labelPlate", "名前の下地"],
|
|
42
|
+
// 多重度も板には描く先が無い (#2107)。 他の図種は `reportCardinalityNotHonored` が知らせる
|
|
43
|
+
["cardinality", "多重度"],
|
|
44
|
+
["head", "端の形"],
|
|
45
|
+
["tailHead", "端の形"],
|
|
46
|
+
["headFill", "端の塗り"],
|
|
47
|
+
["tailHeadFill", "端の塗り"],
|
|
48
|
+
["relation", "関係の種類"],
|
|
49
|
+
["guard", "条件"],
|
|
50
|
+
["role", "役目"],
|
|
51
|
+
["overlay", "線への重ね"],
|
|
52
|
+
["widthBind", "値への追随"],
|
|
53
|
+
["strokeBind", "値への追随"],
|
|
54
|
+
["dashOffsetBind", "値への追随"],
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 出どころと行き先だけを使う図で、効かないと伝えない矢印の欄 (#2364 / #2366)。
|
|
59
|
+
*
|
|
60
|
+
* 骨格の図 (`tree` / `mind`) は矢印を親子のつながりとして描き、
|
|
61
|
+
* ガントチャート (`gantt`) は矢印を前後の関係として読む。 どちらも向きしか使わないので除外は同じ。
|
|
62
|
+
*
|
|
63
|
+
* | 区分 | 欄 |
|
|
64
|
+
* |---|---|
|
|
65
|
+
* | 矢印が使う | 出どころ / 行き先 と、行番号と書いた場所 |
|
|
66
|
+
* | 別の知らせが受け持つ | 部品の端 (`part-node-ignored`) |
|
|
67
|
+
*
|
|
68
|
+
* 文字 (`label`) は除外しない = どちらの図種も描かないので伝える。
|
|
69
|
+
* 空の時だけ書いていない扱いにするのは `効かない矢印の欄を並べる` が受け持つ。
|
|
70
|
+
*
|
|
71
|
+
* **多重度 (`cardinality`) も除外しない**。 同じ行に 2 件並べない決まりがあり、
|
|
72
|
+
* 文字を書いた矢印ではこちらの知らせが先に出るため、多重度だけ別の知らせに回すと 1 行に 2 件並ぶ。
|
|
73
|
+
*/
|
|
74
|
+
export const 向きだけを使う図が伝えない矢印の欄: ReadonlySet<string> = new Set([
|
|
75
|
+
"no",
|
|
76
|
+
"from",
|
|
77
|
+
"to",
|
|
78
|
+
"pos",
|
|
79
|
+
"fromPartNode",
|
|
80
|
+
"toPartNode",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 矢印に書いた欄のうち、効かないものの呼び名を並べる (#2366)。
|
|
85
|
+
*
|
|
86
|
+
* **伝える欄を並べず、描く欄を除いた残り全部を返す**。 効かない欄を並べる形にすると、
|
|
87
|
+
* 矢印に欄を足した日にその欄だけが一覧から漏れ、書いた人には「書いたのに何も起きない」
|
|
88
|
+
* としか見えない。 除外側を書けば、足し忘れは「余計に知らせる」 側へ倒れる。
|
|
89
|
+
*
|
|
90
|
+
* 空の文字は書いていない扱いにする = 記法は矢印の行に必ず文字の欄を作るため、
|
|
91
|
+
* 空でも「書いた」 とみなすと全ての矢印に知らせが出る。
|
|
92
|
+
*
|
|
93
|
+
* @param 矢印 記法から読んだ矢印 1 本
|
|
94
|
+
* @param 伝えない欄 その図種が描く欄と、別の知らせが受け持つ欄
|
|
95
|
+
* @returns 知らせに出す呼び名。 空なら伝えるものが無い
|
|
96
|
+
*/
|
|
97
|
+
export function 効かない矢印の欄を並べる(矢印: object, 伝えない欄: ReadonlySet<string>): string[] {
|
|
98
|
+
const 残り = new Set(
|
|
99
|
+
Object.entries(矢印 as Record<string, unknown>)
|
|
100
|
+
.filter(([欄, 値]) => 値 !== undefined && !伝えない欄.has(欄))
|
|
101
|
+
.map(([欄]) => 欄),
|
|
102
|
+
);
|
|
103
|
+
if (((矢印 as { label?: string }).label ?? "") === "") 残り.delete("label");
|
|
104
|
+
const 効かない: string[] = [];
|
|
105
|
+
for (const [欄, 呼び名] of 効かない矢印の欄の呼び名) {
|
|
106
|
+
if (!残り.delete(欄)) continue;
|
|
107
|
+
if (!効かない.includes(呼び名)) 効かない.push(呼び名);
|
|
108
|
+
}
|
|
109
|
+
// 呼び名を持たない欄は名前をそのまま出す = 読みにくい名前でも、黙って落とすよりは伝わる
|
|
110
|
+
効かない.push(...[...残り].sort());
|
|
111
|
+
return 効かない;
|
|
112
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { ER_CARDINALITY_HEAD } from "@cardenelabs/cdl";
|
|
2
|
+
import type { EdgeHead, ErRelationCardinality } from "@cardenelabs/cdl";
|
|
3
|
+
import type { DslStep } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ER の関係 1 本を、記法から組み立て API の形へ直す (#2105)。
|
|
7
|
+
*
|
|
8
|
+
* ER は段を書くかどうかで組み立てが 2 つに分かれる。 段の無い図は描画側の `er()` を通り、
|
|
9
|
+
* 段を持つ図は段を書ける共通の組み立て (`compileGenericWithAnimate`) を通る。
|
|
10
|
+
* 多重度の語をそれぞれの組み立てが別々に読んでいた間、同じ語が 26 通りのうち 18 通りで
|
|
11
|
+
* 別の図になっていた (欄に書いた語が端に届かない / 段を持つ図に端が付かない)。
|
|
12
|
+
*
|
|
13
|
+
* **語を読むのはここ 1 か所** にし、両方の組み立てがここから作る。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 多重度の語と、それを見つける形。 語は描画側の `ErRelationCardinality` と同じ 6 語 */
|
|
17
|
+
export const CARDINALITY_PATTERNS: Array<[RegExp, ErRelationCardinality]> = [
|
|
18
|
+
[/1:1/, "1:1"],
|
|
19
|
+
[/1:N/i, "1:N"],
|
|
20
|
+
[/N:1/i, "N:1"],
|
|
21
|
+
[/N:M/i, "N:M"],
|
|
22
|
+
[/0\.\.1/, "0..1"],
|
|
23
|
+
[/1\.\.\*/, "1..*"],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** 多重度から端の形が決まる語。 知らせに並べる時は手で書かず、描画側の表から導く (#2107) */
|
|
27
|
+
export const 端の形が決まる語 = Object.keys(ER_CARDINALITY_HEAD) as ErRelationCardinality[];
|
|
28
|
+
|
|
29
|
+
// cardinality token を「単語の途中でない」 境界で囲んだ RegExp を作る (parse / strip で共有する SSOT)。
|
|
30
|
+
// 前後が identifier 文字 (英数字 + アンダースコア) なら token とみなさない = `column:Metadata` の `n:M` /
|
|
31
|
+
// `10:11:12` の `1:1` / `field_1:N` の `1:N` を cardinality と誤認して壊すのを防ぐ
|
|
32
|
+
// (cc-codex #879 Round 9/10/11)。 `_` を含むのは ER label が DB schema 由来で snake_case 命名が多く、
|
|
33
|
+
// `_` 直後に cardinality 様の部分列が来る label が現実的に起こるため (`field_1:N` / `parent_N:M_child`)。
|
|
34
|
+
// strip と parse で別々に pattern.test / replace すると境界規則が drift するため、 この 1 関数を両経路で使う。
|
|
35
|
+
export function boundedCardinalityRegExp(pattern: RegExp, extraFlags = ""): RegExp {
|
|
36
|
+
const base = pattern.flags.includes("i") ? "i" : "";
|
|
37
|
+
return new RegExp(`(?<![A-Za-z0-9_])(?:${pattern.source})(?![A-Za-z0-9_])`, base + extraFlags);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseCardinalityFromLabel(label: string): ErRelationCardinality | null {
|
|
41
|
+
for (const [pattern, card] of CARDINALITY_PATTERNS) {
|
|
42
|
+
if (boundedCardinalityRegExp(pattern).test(label)) return card;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// stripCardinality が「水平空白」 として畳んでよい文字を明示列挙する (space / tab / 全角空白 U+3000)。
|
|
48
|
+
// 改行系 (LF / CR / U+2028 line separator / U+2029 paragraph separator / vertical tab / form feed) は
|
|
49
|
+
// 含めない = これらは label の行構造として保持する (cc-codex #879 Round 5/6 指摘 = `\s` / `[^\S\r\n]`
|
|
50
|
+
// では Unicode 行区切りや CRLF を誤って畳んでしまう)。 括弧除去側と正規化側で同じ class を共有する。
|
|
51
|
+
export const HORIZONTAL_WS = " \\t\\u3000";
|
|
52
|
+
|
|
53
|
+
export const HWS = `[${HORIZONTAL_WS}]`;
|
|
54
|
+
|
|
55
|
+
export function stripCardinality(label: string): string {
|
|
56
|
+
let r = label;
|
|
57
|
+
let removed = false;
|
|
58
|
+
for (const [pattern] of CARDINALITY_PATTERNS) {
|
|
59
|
+
// cardinality token を「それを囲む括弧ごと 1 単位」 で除去する。
|
|
60
|
+
// まず `(1:N)` のように token を直接包む括弧つき形を除去し、 次に裸の token を除去する。
|
|
61
|
+
// 括弧を token 単位で消すことで、 label 中の cardinality と無関係な正当な括弧 (例
|
|
62
|
+
// `fn() now` の `()`) を壊さない (cc-codex #879 Round 4 指摘 = 空括弧の全域除去は過剰)。
|
|
63
|
+
// 括弧と token の間は水平空白のみ許容し、 改行を挟む形 (`(\n1:N\n)`) は括弧除去の対象外にする
|
|
64
|
+
// (改行を消費して行構造を壊すのを防ぐ、 Round 6 Finding 2)。
|
|
65
|
+
const src = pattern.source;
|
|
66
|
+
const flags = pattern.flags.includes("i") ? "gi" : "g";
|
|
67
|
+
const before = r;
|
|
68
|
+
r = r.replace(new RegExp(`\\(${HWS}*${src}${HWS}*\\)`, flags), "");
|
|
69
|
+
// 裸 token 除去 = parse と同じ単語境界付き matcher (boundedCardinalityRegExp) を global で適用する。
|
|
70
|
+
// 前後が英数字なら token とみなさないため、 timestamp (`10:11:12`) / 比率 (`10:11`) / alphabet 埋め込み
|
|
71
|
+
// (`column:Metadata`) を壊さず、 同一 token の複数出現 (`1:N and 1:N`) は全て消す。 parse 側と境界規則を
|
|
72
|
+
// 単一 SSOT にすることで strip/parse の乖離 (strip は消すが parse は残す等) を構造的に防ぐ
|
|
73
|
+
// (cc-codex #879 Round 9/10 = 数字境界だけ / strip 側だけの修正では 2 経路 drift + alphabet 埋め込み穴)。
|
|
74
|
+
r = r.replace(boundedCardinalityRegExp(pattern, "g"), "");
|
|
75
|
+
if (r !== before) removed = true;
|
|
76
|
+
}
|
|
77
|
+
// token を除去していない label は空白を一切いじらない (無条件適用でも改行 / 複数空白を保持する、
|
|
78
|
+
// cc-codex #879 Round 5 指摘 = 無条件正規化は改行を含む label を破壊した)。
|
|
79
|
+
if (!removed) return label;
|
|
80
|
+
// 除去で生じた水平空白 (space / tab / 全角空白) のみ単一化する (例 "A 1:N B" → "A B" → "A B")。
|
|
81
|
+
// 改行系は HWS に含めないため保持される。
|
|
82
|
+
// - 各行内の連続水平空白を単一化
|
|
83
|
+
// - 改行 (LF / CR) の前後の水平空白を除去 (改行直前の trailing 空白も落とす)
|
|
84
|
+
r = r
|
|
85
|
+
.replace(new RegExp(`${HWS}{2,}`, "g"), " ")
|
|
86
|
+
.replace(new RegExp(`${HWS}*([\\r\\n])${HWS}*`, "g"), "$1")
|
|
87
|
+
.replace(new RegExp(`^${HWS}+|${HWS}+$`, "g"), "");
|
|
88
|
+
// fallback = cardinality 除去後に「視覚的に意味のある文字」 が残らない場合は元 label を返す
|
|
89
|
+
// (Round 6 Finding 1 = 除去後に空白/不可視文字だけ残ると不可視 label になるのを防ぐ)。
|
|
90
|
+
//
|
|
91
|
+
// 「意味のある文字」 の判定は個別の空白/不可視文字を列挙 (denylist) すると際限が無く、
|
|
92
|
+
// Round 7 で `\s` → `\p{White_Space}` に変えたら NEL は拾えたが BOM を落とす等のいたちごっこに
|
|
93
|
+
// なった (cc-codex #879 Round 7/8/9)。 そこで Unicode の「見えない文字」 を 4 カテゴリで構造的に
|
|
94
|
+
// 判定する = 以下のいずれでもない可視文字が 1 つでもあれば意味あり。
|
|
95
|
+
// - White_Space ... 全空白 (space / tab / NBSP / NEL / 全角空白 / 各種 Unicode space / 改行系)
|
|
96
|
+
// - Cf (Format) ... BOM / ZWSP / ZWNJ / ZWJ / WORD JOINER / soft hyphen 等
|
|
97
|
+
// - Cc (Control) ... 制御文字
|
|
98
|
+
// - Default_Ignorable_Code_Point ... variation selector (Mn) / Hangul filler (Lo) 等、 Cf に
|
|
99
|
+
// 入らない不可視文字 (Cf/Cc/White_Space だけでは取りこぼすと Round 9 で判明)
|
|
100
|
+
// 4 カテゴリで Unicode の非表示文字を網羅する (Braille blank U+2800 や通常文字は content 維持)。
|
|
101
|
+
const hasVisible = /[^\p{White_Space}\p{Cf}\p{Cc}\p{Default_Ignorable_Code_Point}]/u.test(r);
|
|
102
|
+
return hasVisible ? r : label;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 欄に書いた語が 6 語のどれかそのものなら、その語。 大文字小文字は見本を見つける形と同じく問わない */
|
|
106
|
+
function 欄の語(書いた: string): ErRelationCardinality | null {
|
|
107
|
+
const 字 = 書いた.trim();
|
|
108
|
+
for (const [pattern, card] of CARDINALITY_PATTERNS) {
|
|
109
|
+
if (new RegExp(`^(?:${pattern.source})$`, pattern.flags).test(字)) return card;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 矢印の欄に書いた多重度を読む。 空白だけの欄は書かなかったものとして `undefined` を返す。
|
|
116
|
+
*
|
|
117
|
+
* `語` は 6 語のどれかそのものならその語、そうでなければ `null` (名前に `(字)` と添えるだけで端を決めない形)。
|
|
118
|
+
* 組み立てが名前と端を決める時と、効かない形を知らせる時 (#2107) の両方がこれを読む = 判定が割れない
|
|
119
|
+
*/
|
|
120
|
+
export function 書いた多重度を読む(
|
|
121
|
+
欄: string | undefined,
|
|
122
|
+
): { 字: string; 語: ErRelationCardinality | null } | undefined {
|
|
123
|
+
const 字 = 欄?.trim();
|
|
124
|
+
if (!字) return undefined;
|
|
125
|
+
return { 字, 語: 欄の語(字) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 組み立て API の `er().relation` に渡す指定のうち、多重度と名前と端に関わる分 */
|
|
129
|
+
export type ERの関係の指定 = {
|
|
130
|
+
label?: string;
|
|
131
|
+
cardinality?: ErRelationCardinality;
|
|
132
|
+
head?: EdgeHead;
|
|
133
|
+
tailHead?: EdgeHead;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 記法の 1 行から、組み立て API に渡す指定を作る。
|
|
138
|
+
*
|
|
139
|
+
* **語は欄 (`cardinality`) を先に読み、無ければ名前から読む**。 名前に書いた語はどちらの場合も名前から外す
|
|
140
|
+
* (名前が語だけなら、名前を書かなかったものとして扱う = 組み立て API が語を名前として出す)。
|
|
141
|
+
*
|
|
142
|
+
* 欄に 6 語以外を書いた時 (`2..5` 等) は、名前に `(2..5)` と添えて端は決めない。 組み立て API の語は
|
|
143
|
+
* 6 語に限られ、それ以外を渡す口が無い。 書いた字を消さずに読み手へ見せる形にし、
|
|
144
|
+
* 描かない端があることは組み立ての知らせ (`cardinality-not-honored`、#2107) が伝える。
|
|
145
|
+
*/
|
|
146
|
+
export function ERの関係の指定を作る(
|
|
147
|
+
s: Pick<DslStep, "label" | "cardinality" | "head" | "tailHead">,
|
|
148
|
+
): ERの関係の指定 {
|
|
149
|
+
const 端 = {
|
|
150
|
+
...(s.head !== undefined ? { head: s.head } : {}),
|
|
151
|
+
...(s.tailHead !== undefined ? { tailHead: s.tailHead } : {}),
|
|
152
|
+
};
|
|
153
|
+
const 書いた = 書いた多重度を読む(s.cardinality);
|
|
154
|
+
if (書いた && 書いた.語 === null) {
|
|
155
|
+
return { label: s.label ? `${s.label} (${書いた.字})` : `(${書いた.字})`, ...端 };
|
|
156
|
+
}
|
|
157
|
+
const 語 = 書いた?.語 ?? parseCardinalityFromLabel(s.label);
|
|
158
|
+
if (語 === null) return { ...(s.label ? { label: s.label } : {}), ...端 };
|
|
159
|
+
// 欄に書いた時も名前の語は外す。 残すと、名前と名前の下の行に語が 2 度並ぶ (欄の語が勝つ)
|
|
160
|
+
const 名前 = 語を外した名前(s.label);
|
|
161
|
+
return { ...(名前 ? { label: 名前 } : {}), cardinality: 語, ...端 };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 名前から多重度の語を外す。 語だけの名前は空にする */
|
|
165
|
+
function 語を外した名前(label: string): string {
|
|
166
|
+
if (parseCardinalityFromLabel(label) === null) return label;
|
|
167
|
+
const 外した = stripCardinality(label);
|
|
168
|
+
// 外すと見える字が残らない時、`stripCardinality` は元の名前を返す = 名前は語だけだった
|
|
169
|
+
return 外した === label ? "" : 外した;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 指定を矢印の欄にする。 **組み立て API の `er().relation` と同じ規則**。
|
|
174
|
+
*
|
|
175
|
+
* 段を持つ図は `er()` を通らないので、ここに規則を写す。 写した規則が組み立て API とずれていないことは
|
|
176
|
+
* 検査 (`er-cardinality-notation.test.ts`) が 6 語の全ての組で突き合わせる。
|
|
177
|
+
*
|
|
178
|
+
* | 欄 | 決まり方 |
|
|
179
|
+
* |---|---|
|
|
180
|
+
* | 名前 (`label`) | 書いた名前。 無ければ語 |
|
|
181
|
+
* | 名前の下の行 (`sub`) | 名前と語の両方があり、端を明示していない時だけ語 |
|
|
182
|
+
* | 両端 (`tailHead` / `head`) | 明示が勝つ。 無ければ語から描画側の表 (`ER_CARDINALITY_HEAD`) で引く |
|
|
183
|
+
*/
|
|
184
|
+
export function ERの関係の矢印(指定: ERの関係の指定): {
|
|
185
|
+
label: string;
|
|
186
|
+
sub?: string;
|
|
187
|
+
head?: EdgeHead;
|
|
188
|
+
tailHead?: EdgeHead;
|
|
189
|
+
} {
|
|
190
|
+
const 表の端 = 指定.cardinality ? ER_CARDINALITY_HEAD[指定.cardinality] : undefined;
|
|
191
|
+
const 端を明示した = 指定.tailHead !== undefined || 指定.head !== undefined;
|
|
192
|
+
const head = 指定.head ?? 表の端?.head;
|
|
193
|
+
const tailHead = 指定.tailHead ?? 表の端?.tail;
|
|
194
|
+
return {
|
|
195
|
+
label: 指定.label ?? 指定.cardinality ?? "",
|
|
196
|
+
...(指定.label && 指定.cardinality && !端を明示した ? { sub: 指定.cardinality } : {}),
|
|
197
|
+
...(head !== undefined ? { head } : {}),
|
|
198
|
+
...(tailHead !== undefined ? { tailHead } : {}),
|
|
199
|
+
};
|
|
200
|
+
}
|