@cardenelabs/dragon 0.28.0 → 0.30.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 +78 -5
- package/dist/index.cjs +9246 -8698
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +186 -66
- package/dist/index.d.ts +186 -66
- package/dist/index.js +9247 -8700
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/compile/actor-ids.ts +306 -0
- package/src/compile/actors.ts +74 -0
- package/src/compile/c4.ts +77 -0
- package/src/compile/chain.ts +54 -0
- package/src/compile/chart-fields.ts +178 -0
- package/src/compile/class.ts +100 -0
- package/src/compile/deep-rewrite.ts +30 -0
- package/src/compile/derived-source.ts +18 -0
- package/src/compile/direction.ts +63 -0
- package/src/compile/er-relation.ts +200 -0
- package/src/compile/er.ts +44 -0
- package/src/compile/flow.ts +54 -0
- package/src/compile/funnel.ts +75 -0
- package/src/compile/gantt.ts +232 -0
- package/src/compile/generic.ts +284 -0
- package/src/compile/hierarchy.ts +92 -0
- package/src/compile/journey.ts +84 -0
- package/src/compile/kinds.ts +156 -0
- package/src/compile/lanes.ts +56 -0
- package/src/compile/mind.ts +343 -0
- package/src/compile/node-title.ts +24 -0
- package/src/compile/notice.ts +94 -0
- package/src/compile/parts-lookup.ts +17 -0
- package/src/compile/parts.ts +2635 -0
- package/src/compile/placement-apply.ts +673 -0
- package/src/compile/placement.ts +15 -0
- package/src/compile/quadrant.ts +110 -0
- package/src/compile/rows.ts +40 -0
- package/src/compile/sequence.ts +226 -0
- package/src/compile/slug.ts +29 -0
- package/src/compile/solidity.ts +39 -0
- package/src/compile/state.ts +71 -0
- package/src/compile/subtitle.ts +22 -0
- package/src/compile/swimlane.ts +83 -0
- package/src/compile/topology.ts +50 -0
- package/src/compile/tree.ts +60 -0
- package/src/compile/value-chart.ts +183 -0
- package/src/compile/values.ts +432 -0
- package/src/compile/word-state.ts +82 -0
- package/src/compile.ts +513 -6858
- package/src/index.ts +8 -7
- package/src/json-line-map.ts +30 -0
- package/src/json-parser.ts +356 -123
- package/src/near-name.ts +42 -0
- package/src/relative-pos.ts +71 -2
- package/src/schema.ts +1 -1
- package/src/schemas/diagram.json +38 -4
- package/src/types.ts +1 -1
- package/src/v05/parser.ts +91 -72
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
tree: { w: 720, h: 480 },
|
|
82
|
+
mind: { w: 720, h: 480 },
|
|
83
|
+
journey: { w: 720, h: 480 },
|
|
84
|
+
quadrant: { w: 640, h: 480 },
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 図表の欄が `{名前}` で読む値を、**1 か所で** 確かめる (#1200)。
|
|
89
|
+
*
|
|
90
|
+
* 図表には数の欄 (割合 / 段の人数) と語の欄 (気持ち / 区画) があり、どちらも `{名前}` で
|
|
91
|
+
* 状態を読める。 確かめることは欄の種類で違うが、**土台は同じ** = 同じ名前を 2 回書いた時に
|
|
92
|
+
* 後ろが効くこと、段で状態に入る値 (切り替え / 補間) も見ること、の 2 つ。
|
|
93
|
+
*
|
|
94
|
+
* #1198 と #1201 では欄ごとに検査を書き足しており、同じ土台を 3 度書いていた。 3 度とも
|
|
95
|
+
* review で同じ形の穴を指摘されている (最初の宣言で判定する / 段で入る値を見落とす)。
|
|
96
|
+
* 土台を 1 つにして、欄ごとの違いだけを外から渡す。
|
|
97
|
+
*
|
|
98
|
+
* ## 確かめること
|
|
99
|
+
*
|
|
100
|
+
* | 欄 | 通す値 | 段で入る値 |
|
|
101
|
+
* |---|---|---|
|
|
102
|
+
* | 数 | 数として読める (空文字は弾く、`Number("")` が 0 を返すため) | 切り替え先が数 |
|
|
103
|
+
* | 語 | 語表にある語 | 切り替え先が語表にあり、補間されない (補間の行き先は数) |
|
|
104
|
+
*
|
|
105
|
+
* 自動で決まる値 (`values:`) は式の評価結果で必ず数になるため、数の欄からは参照できて
|
|
106
|
+
* 語の欄からは参照できない。 式そのものの不備 (語を読む / 名前が無い) は
|
|
107
|
+
* `value-unresolved` の警告が別に出る (実測で確認済)。
|
|
108
|
+
*
|
|
109
|
+
* ## 責務境界 (#1198 / #1200)
|
|
110
|
+
*
|
|
111
|
+
* **見るのは組み立ての時点で決まっている範囲だけ**。 記法の値は実行時に決まるため、ここで
|
|
112
|
+
* 全部を判定しようとすると式の評価を組み立て側で再現することになる。 実際に #1199 の review で
|
|
113
|
+
* 4 round 続けて同じ形の指摘が出て収束せず、境界を決めて切り分けた (穴を 1 つ塞ぐと別の形が
|
|
114
|
+
* 出る = 塞ぎ方ではなく責務の置き場所の問題だった)。
|
|
115
|
+
*
|
|
116
|
+
* | 見る | 見ない | 見ない理由 |
|
|
117
|
+
* |---|---|---|
|
|
118
|
+
* | 名前が宣言されているか | 段の行き先が負になる形 | 描画側が問題なく描く (負の大きさも `NaN` も出ないことを実測) |
|
|
119
|
+
* | 宣言の時点で読める値か | 式が実行時に返す値 | 式の不備は `value-unresolved` の警告が別に出る (実測で確認) |
|
|
120
|
+
* | 段で状態に入る値 | | |
|
|
121
|
+
*
|
|
122
|
+
* 見ない範囲は描画側が受け持つ = 解けない値は既定値で描いて `data-cdl-unresolved` を付ける。
|
|
123
|
+
*/
|
|
124
|
+
export function 図表の欄から参照できる名前(
|
|
125
|
+
doc: DslDocument,
|
|
126
|
+
欄: {
|
|
127
|
+
読めるか: (v: number | string) => boolean;
|
|
128
|
+
補間で壊れるか: boolean;
|
|
129
|
+
自動の値を許すか: boolean;
|
|
130
|
+
},
|
|
131
|
+
): Set<string> {
|
|
132
|
+
// 同じ名前を 2 回宣言した時は後ろが効く。 描画側が後の宣言を有効値として扱うため、
|
|
133
|
+
// 前の宣言で判定すると「読めると判定したのに読めない値が入る」 状態になる (実測)
|
|
134
|
+
const 実効 = new Map<string, number | string>();
|
|
135
|
+
for (const s of doc.animate?.states ?? []) 実効.set(s.name, s.initial);
|
|
136
|
+
|
|
137
|
+
const 壊れる = new Set<string>();
|
|
138
|
+
for (const p of doc.animate?.phases ?? []) {
|
|
139
|
+
for (const st of p.sets ?? []) if (!欄.読めるか(st.value)) 壊れる.add(st.state);
|
|
140
|
+
if (欄.補間で壊れるか) for (const tw of p.tweens ?? []) 壊れる.add(tw.state);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const out = new Set<string>();
|
|
144
|
+
for (const [名前, 値] of 実効) {
|
|
145
|
+
if (!欄.読めるか(値)) continue;
|
|
146
|
+
if (壊れる.has(名前)) continue;
|
|
147
|
+
out.add(名前);
|
|
148
|
+
}
|
|
149
|
+
if (欄.自動の値を許すか) for (const v of doc.values ?? []) out.add(v.name);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 数の欄が読める値か。 空文字と空白だけは弾く (`Number("")` は 0 を返す) */
|
|
154
|
+
export function 数として読めるか(v: number | string): boolean {
|
|
155
|
+
if (typeof v === "number") return Number.isFinite(v);
|
|
156
|
+
const t = v.trim();
|
|
157
|
+
if (t === "") return false;
|
|
158
|
+
return Number.isFinite(Number(t));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function 数の欄から参照できる名前(doc: DslDocument): Set<string> {
|
|
162
|
+
return 図表の欄から参照できる名前(doc, {
|
|
163
|
+
読めるか: 数として読めるか,
|
|
164
|
+
// 補間の行き先は数なので、数の欄では壊れない
|
|
165
|
+
補間で壊れるか: false,
|
|
166
|
+
自動の値を許すか: true,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function 語の欄から参照できる名前(doc: DslDocument, 語表: Map<string, string>): Set<string> {
|
|
171
|
+
return 図表の欄から参照できる名前(doc, {
|
|
172
|
+
読めるか: (v) => 語表.has(String(v).trim()),
|
|
173
|
+
// 補間の行き先は数。 語の欄が読む状態を補間すると、その段で語が数に変わる
|
|
174
|
+
補間で壊れるか: true,
|
|
175
|
+
// 式の評価結果は数になるため、語の欄からは読めない
|
|
176
|
+
自動の値を許すか: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { diagram, classDiagram } from "@cardenelabs/cdl";
|
|
2
|
+
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
3
|
+
import type { DslDocument } from "../types";
|
|
4
|
+
|
|
5
|
+
import { 箱の題 } from "./node-title";
|
|
6
|
+
|
|
7
|
+
import { slugify } from "./slug";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* クラス図の組み立て。
|
|
11
|
+
*
|
|
12
|
+
* 各クラスを 1 つの箱 (`storage`) にする。 描画側は題 (クラス名) と区切り線と行
|
|
13
|
+
* (項目 / 手続き) を UML のクラス箱として描く。
|
|
14
|
+
*
|
|
15
|
+
* **クラスごとに縦列を 1 本作り、横に並べる** (#1263)。 組立て API 側がそう並べており、
|
|
16
|
+
* 1 本にまとめると同じ内容でも横並びが縦並びになる (実測 = 見本は 3 縦列 450 幅)。
|
|
17
|
+
* 縦列に見出しは付けない = クラスの名前は箱が既に描いており、縦列は並べるための入れ物
|
|
18
|
+
* (`er` / `state` と同じ扱い、#1241)。
|
|
19
|
+
*
|
|
20
|
+
* 箱の種類は `storage` に強制する。 行と小見出しは `applyV05Extensions` が後から載せる。
|
|
21
|
+
*
|
|
22
|
+
* 矢印は継承や保有を表す (`extends` / `aggregates` 等を書き手が説明に書く)。
|
|
23
|
+
*/
|
|
24
|
+
export function compileClass(doc: DslDocument): CdlDiagram {
|
|
25
|
+
/*
|
|
26
|
+
* **組み立て器 (`classDiagram`) に渡す** (#1466)。
|
|
27
|
+
*
|
|
28
|
+
* 以前は箱と矢印を直に組んでいたため、行頭の印・端の塗り・印が付く側・段の配置が
|
|
29
|
+
* 1 つも出なかった = 画面の図と記法の図が別物になっていた。 組み立て器へ渡せば、
|
|
30
|
+
* 意匠の決まり (`CLASS_RELATION_LOOK`) を 1 箇所から引ける。
|
|
31
|
+
*/
|
|
32
|
+
const b = classDiagram({ id: slugify(doc.title), topic: doc.title });
|
|
33
|
+
// 登場人物が 0 人なら枠も作らない。 先に作ると中身の無い枠が 1 つ残る (#1096)
|
|
34
|
+
if (doc.actors.length === 0)
|
|
35
|
+
return diagram(slugify(doc.title), { topic: doc.title, type: "class" }).build();
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* 縦列は `lane:` の順、段は `stack:` で決まる (#1466)。
|
|
39
|
+
*
|
|
40
|
+
* 書かない図は宣言した順に横 1 列 = 従来どおり。 1 つでも書けば格子に置く =
|
|
41
|
+
* **箱の 1 つの辺には関係を 1 本まで** を守るには段が要る。
|
|
42
|
+
*/
|
|
43
|
+
const 列番号 = new Map<string, number>();
|
|
44
|
+
for (const a of doc.actors) {
|
|
45
|
+
if (a.lane === undefined) continue;
|
|
46
|
+
if (!列番号.has(a.lane)) 列番号.set(a.lane, 列番号.size);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const a of doc.actors) {
|
|
50
|
+
/*
|
|
51
|
+
* 行を持ち物と振る舞いに割る (#1466)。 **1 行ずつ括弧の有無で見る**。
|
|
52
|
+
*
|
|
53
|
+
* 区切りの行 (`───`) の位置では割らない = 振る舞いしか持たない箱は区切りを書けず、
|
|
54
|
+
* 全部が持ち物に落ちる (実測 = `Auditable` の `audit()` が四角の印で出た)。
|
|
55
|
+
* 区切りの行そのものは、組み立て器が群の間を空の行で作るので捨てる。
|
|
56
|
+
*/
|
|
57
|
+
const rows = (a.rows ?? []).filter((r) => !/^[─-]+$/.test(r.trim()));
|
|
58
|
+
const 振る舞い = (r: string): boolean => r.includes("(");
|
|
59
|
+
const attributes = rows.filter((r) => !振る舞い(r));
|
|
60
|
+
const methods = rows.filter(振る舞い);
|
|
61
|
+
b.class({
|
|
62
|
+
id: slugify(a.name),
|
|
63
|
+
title: 箱の題(a),
|
|
64
|
+
...(attributes.length > 0 ? { attributes: [...attributes] } : {}),
|
|
65
|
+
...(methods.length > 0 ? { methods: [...methods] } : {}),
|
|
66
|
+
...(a.eyebrow ? { stereotype: a.eyebrow } : {}),
|
|
67
|
+
...(a.lane !== undefined ? { col: 列番号.get(a.lane) ?? 0 } : {}),
|
|
68
|
+
...(a.stack !== undefined ? { row: a.stack } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const s2 of doc.flow) {
|
|
73
|
+
b.relation({
|
|
74
|
+
from: slugify(s2.from),
|
|
75
|
+
to: slugify(s2.to),
|
|
76
|
+
// 種類を書かない矢印は「使う」 扱い = 端が開いた矢になり、線と印の組が最も素直
|
|
77
|
+
type: s2.relation ?? "uses",
|
|
78
|
+
...(s2.label ? { label: s2.label } : {}),
|
|
79
|
+
...(s2.sub ? { cardinality: s2.sub } : {}),
|
|
80
|
+
// 出どころ側の多重度 (#1771)。 engine が出どころの端に添える (cdl#825)
|
|
81
|
+
...(s2.tailSub ? { tailCardinality: s2.tailSub } : {}),
|
|
82
|
+
...(s2.tone ? { tone: s2.tone } : {}),
|
|
83
|
+
...(s2.style ? { style: s2.style } : {}),
|
|
84
|
+
...(s2.head ? { head: s2.head } : {}),
|
|
85
|
+
...(s2.headFill ? { headFill: s2.headFill } : {}),
|
|
86
|
+
...(s2.tailHead ? { tailHead: s2.tailHead } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const built = b.build();
|
|
91
|
+
/*
|
|
92
|
+
* 記法が段を書いた図では、組み立て器が作る 1 つの段を捨てる (#1466)。
|
|
93
|
+
*
|
|
94
|
+
* 段の注入 (`injectPhasesFallback`) は「段が 1 つも無い」 図にだけ効く。 組み立て器へ
|
|
95
|
+
* 渡すようにしたことで自動の段が 1 つ付き、書いた段が届かなくなった (実測 = 6 段書いた
|
|
96
|
+
* 図が 1 段で出た)。
|
|
97
|
+
*/
|
|
98
|
+
const 書いた段がある = (doc.animate?.phases.length ?? 0) > 0;
|
|
99
|
+
return 書いた段がある ? { ...built, phases: [] } : built;
|
|
100
|
+
}
|
|
@@ -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,63 @@
|
|
|
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" | "er" | "state" | "topology";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 向きを選べる図種 (#1494)。
|
|
16
|
+
*
|
|
17
|
+
* **並び方そのものが読み方を担う図種は外す**。 表の図は「1 縦列 1 表」、クラス図と状態の図は
|
|
18
|
+
* 設計が格子に置く形、順序図は 1 枚の板で、どれも向きを入れ替えると図の意味が変わる。
|
|
19
|
+
*
|
|
20
|
+
* `topology` も外す = 入れ物 (`contain`) を持つ図で、縦列の中に箱を囲む作りが向きと結びついている。
|
|
21
|
+
*/
|
|
22
|
+
export const 向きを選べる図種: ReadonlySet<PresetType> = new Set<PresetType>(["flow", "swimlane"]);
|
|
23
|
+
|
|
24
|
+
/** その図種の既定の向き。 書かなかった時は今までどおりの並びになる。 */
|
|
25
|
+
export function 既定の向き(kind: PresetType): "縦" | "横" {
|
|
26
|
+
return kind === "flow" || kind === "topology" ? "縦" : "横";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 実際に使う向き (#1494)。
|
|
31
|
+
*
|
|
32
|
+
* 書いていない図と、効かない図種に書いた図は既定のまま = **書かない図の並びは 1 つも動かない**。
|
|
33
|
+
*/
|
|
34
|
+
export function 並べる向き(kind: PresetType, doc: DslDocument): "縦" | "横" {
|
|
35
|
+
if (doc.direction === undefined) return 既定の向き(kind);
|
|
36
|
+
if (!向きを選べる図種.has(kind)) return 既定の向き(kind);
|
|
37
|
+
return doc.direction;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 後ろへ戻る矢印か (#1260)。
|
|
42
|
+
*
|
|
43
|
+
* 状態の図は、後ろの状態へ戻る矢印を **箱の上を回して** 描く (`routing: "back-detour"`)。
|
|
44
|
+
* 組立て API 側がそうしており、記法側で付けないと **描いた図の高さが変わる**
|
|
45
|
+
* (実測 = viewBox が 404 対 486 で、戻る矢印が箱の右横を回っていた)。
|
|
46
|
+
*
|
|
47
|
+
* 判定は並び順。 指す先が指す元より前にあれば戻る矢印
|
|
48
|
+
* (実測 = `stateMachine()` は c -> a と c -> b の 2 本だけに付け、a -> b と b -> c には付けない)。
|
|
49
|
+
*
|
|
50
|
+
* **状態の図だけに付ける**。 他の図種の組立て API は付けない (実測 = `er()` は付けなかった)。
|
|
51
|
+
*/
|
|
52
|
+
export function 後ろへ戻る矢印か(
|
|
53
|
+
kind: GenericKind,
|
|
54
|
+
fromId: string,
|
|
55
|
+
toId: string,
|
|
56
|
+
箱の並び: ReadonlyMap<string, number>,
|
|
57
|
+
): boolean {
|
|
58
|
+
if (kind !== "state") return false;
|
|
59
|
+
const 元 = 箱の並び.get(fromId);
|
|
60
|
+
const 先 = 箱の並び.get(toId);
|
|
61
|
+
if (元 === undefined || 先 === undefined) return false;
|
|
62
|
+
return 先 < 元;
|
|
63
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { er } from "@cardenelabs/cdl";
|
|
2
|
+
import { 箱の題 } from "./node-title";
|
|
3
|
+
import type { CdlDiagram } from "@cardenelabs/cdl";
|
|
4
|
+
import type { DslDocument } from "../types";
|
|
5
|
+
|
|
6
|
+
import { ERの関係の指定を作る } from "./er-relation";
|
|
7
|
+
import { compileGenericWithAnimate } from "./generic";
|
|
8
|
+
|
|
9
|
+
import { slugify } from "./slug";
|
|
10
|
+
|
|
11
|
+
export function compileEr(doc: DslDocument): CdlDiagram {
|
|
12
|
+
// v0.4 ... animation あり時 builder 直接経路 (entity を box として配置)
|
|
13
|
+
if (doc.animate && doc.animate.phases.length > 0) {
|
|
14
|
+
// 460 は preset 側の旧既定に合わせた値だった。 preset が箱 400 + 余白 25 × 2 = 450 を
|
|
15
|
+
// 宣言するようになった (cardene777/cdl#359) ので、 同じ図が animate の有無で 10 world
|
|
16
|
+
// ずれないようここも 450 にする。
|
|
17
|
+
return compileGenericWithAnimate(doc, { kind: "er", laneWidth: 450 });
|
|
18
|
+
}
|
|
19
|
+
// er preset ... actors を entity に、 流れ を relation に
|
|
20
|
+
const erBuilder = er({
|
|
21
|
+
id: slugify(doc.title),
|
|
22
|
+
topic: doc.title,
|
|
23
|
+
});
|
|
24
|
+
for (const a of doc.actors) {
|
|
25
|
+
// entity rows は DSL では宣言できないので、 actor 名のみ entity 化
|
|
26
|
+
// v0.3 で「列定義」 ブロックを追加検討
|
|
27
|
+
erBuilder.entity({
|
|
28
|
+
id: slugify(a.name) || a.name,
|
|
29
|
+
title: 箱の題(a),
|
|
30
|
+
rows: [], // v0.2 では rows なし
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
for (const s of doc.flow) {
|
|
34
|
+
// 多重度と名前と端は、段を持つ図と同じ 1 か所で決める (#2105)。
|
|
35
|
+
// 語を書かない関係に `1:N` を補わない = 書いていない語を字と端に出さない
|
|
36
|
+
erBuilder.relation({
|
|
37
|
+
from: slugify(s.from),
|
|
38
|
+
to: slugify(s.to),
|
|
39
|
+
...ERの関係の指定を作る(s),
|
|
40
|
+
...(s.tone ? { tone: s.tone } : {}),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return erBuilder.build();
|
|
44
|
+
}
|