@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,696 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLEARANCE_NODE_LABEL,
|
|
3
|
+
diagram,
|
|
4
|
+
LABEL_INIT_CLEARANCE,
|
|
5
|
+
LABEL_TO_PATH_CLEARANCE,
|
|
6
|
+
} from "@cardenelabs/cdl";
|
|
7
|
+
import type { CdlDiagram, CdlEdge, NodeKind, Tone } from "@cardenelabs/cdl";
|
|
8
|
+
import { parseFocusEntry } from "../focus";
|
|
9
|
+
import type { DslDocument, DslPhase, DslStep } from "../types";
|
|
10
|
+
import { 始まりと終わりの決め方 } from "./actors";
|
|
11
|
+
import { 並べる向き, 後ろへ戻る矢印か, type GenericKind } from "./direction";
|
|
12
|
+
import { 描ける種別 } from "./kinds";
|
|
13
|
+
import { 縦列ごとの段を決める, 書いた縦列に置く } from "./lanes";
|
|
14
|
+
import { metroMarkFrameSize, placeMetro } from "./metro";
|
|
15
|
+
import { 箱の題 } from "./node-title";
|
|
16
|
+
import { slugify } from "./slug";
|
|
17
|
+
import {
|
|
18
|
+
placeStages,
|
|
19
|
+
STAGE_CARD_COLUMN_INSET,
|
|
20
|
+
STAGE_CARD_HEIGHT,
|
|
21
|
+
STAGE_CARD_PITCH,
|
|
22
|
+
STAGE_CARD_WIDTH,
|
|
23
|
+
STAGE_FIRST_CARD_CENTER_Y,
|
|
24
|
+
STAGE_LANE_GAP,
|
|
25
|
+
STAGE_LANE_Y,
|
|
26
|
+
段の箱の線の指定,
|
|
27
|
+
type StagesPlacement,
|
|
28
|
+
} from "./stages";
|
|
29
|
+
import { 泳法図で印をどう描く } from "./swimlane-marks";
|
|
30
|
+
import {
|
|
31
|
+
classifyTimelineEdge,
|
|
32
|
+
placeTimeline,
|
|
33
|
+
TIMELINE_AXIS_WIDTH,
|
|
34
|
+
TIMELINE_CARD_HEIGHT,
|
|
35
|
+
TIMELINE_DECISION_HEIGHT,
|
|
36
|
+
TIMELINE_DECISION_WIDTH,
|
|
37
|
+
TIMELINE_END_HEIGHT,
|
|
38
|
+
TIMELINE_END_WIDTH,
|
|
39
|
+
TIMELINE_NUMBER_SIZE,
|
|
40
|
+
} from "./timeline";
|
|
41
|
+
|
|
42
|
+
/** 見本帳と共通の、時間軸の横分岐に置く札の寸法。 */
|
|
43
|
+
const 時間軸の横分岐の札の大きさ = { width: 96, height: 40, fontSize: 21 } as const;
|
|
44
|
+
|
|
45
|
+
/** 時間軸の横分岐の札の下端と線の間。 */
|
|
46
|
+
const 時間軸の横分岐の札と線の間 = 20;
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* 短い横線では札の横幅が両端の節の安全域に重なるため、cdl の routePath は札の中心を
|
|
50
|
+
* `線 - 節の半高 - 節との余白 - 標準札の半高` に置く。そこから見本の位置へ戻す量を渡す。
|
|
51
|
+
*/
|
|
52
|
+
const 時間軸の横分岐の札の下げ幅 =
|
|
53
|
+
TIMELINE_DECISION_HEIGHT / 2 +
|
|
54
|
+
CLEARANCE_NODE_LABEL +
|
|
55
|
+
(LABEL_INIT_CLEARANCE - LABEL_TO_PATH_CLEARANCE) -
|
|
56
|
+
(時間軸の横分岐の札の大きさ.height / 2 + 時間軸の横分岐の札と線の間);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 段を持つ図種の共通の組み立て (#2030 で `compile.ts` から移した)。
|
|
60
|
+
*
|
|
61
|
+
* `flow` / `swimlane` / `record` / `topology` の 4 図種が通る。 `record` は段を書かない図も
|
|
62
|
+
* ここを通る (経路が 1 本なので、#2782)。
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
export type GenericOpts = {
|
|
66
|
+
kind: GenericKind;
|
|
67
|
+
/** flow / topology は 1 lane に全 actor、 swimlane / record は actor ごと lane */
|
|
68
|
+
laneId?: string;
|
|
69
|
+
laneWidth: number;
|
|
70
|
+
/** 図種固有の関係を、共通の edge に渡す既定値へ直す。本文に明記した値は後段で勝つ。 */
|
|
71
|
+
edgeDefaults?: (step: DslStep) => Partial<CdlEdge>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 分かれ道から続く線と駅の色を返す。
|
|
76
|
+
*
|
|
77
|
+
* 枝の意味を表す明示の欄は無いため、読み手が追う flow の記述順を使い、1 本目を「はい」、
|
|
78
|
+
* 2 本目以降を「いいえ」とする。枝の先も同じ結果の道筋なので色を引き継ぐ。
|
|
79
|
+
* 分かれ道より手前へ戻る線にも色は付けるが、そこで止める = 戻った本線まで枝色にしない。
|
|
80
|
+
*/
|
|
81
|
+
function metroBranchTones(doc: DslDocument): {
|
|
82
|
+
edgeToneByIndex: Map<number, Tone>;
|
|
83
|
+
nodeToneByName: Map<string, Tone>;
|
|
84
|
+
} {
|
|
85
|
+
const edgeToneByIndex = new Map<number, Tone>();
|
|
86
|
+
const nodeToneByName = new Map<string, Tone>();
|
|
87
|
+
const actorByName = new Map(doc.actors.map((actor) => [actor.name, actor]));
|
|
88
|
+
const actorIndex = new Map(doc.actors.map((actor, index) => [actor.name, index]));
|
|
89
|
+
const outgoing = new Map<string, number[]>();
|
|
90
|
+
doc.flow.forEach((step, index) => {
|
|
91
|
+
const indexes = outgoing.get(step.from) ?? [];
|
|
92
|
+
indexes.push(index);
|
|
93
|
+
outgoing.set(step.from, indexes);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
doc.actors.forEach((decision, decisionIndex) => {
|
|
97
|
+
if (decision.kind !== "decision") return;
|
|
98
|
+
const branches = outgoing.get(decision.name) ?? [];
|
|
99
|
+
branches.forEach((branchEdgeIndex, branchIndex) => {
|
|
100
|
+
const tone: Tone = branchIndex === 0 ? "success" : "error";
|
|
101
|
+
const follow = (edgeIndex: number): void => {
|
|
102
|
+
// 合流した道筋は、flow に先に書いた枝の色を保つ。
|
|
103
|
+
if (edgeToneByIndex.has(edgeIndex)) return;
|
|
104
|
+
edgeToneByIndex.set(edgeIndex, tone);
|
|
105
|
+
const targetName = doc.flow[edgeIndex]?.to;
|
|
106
|
+
if (targetName === undefined) return;
|
|
107
|
+
const target = actorByName.get(targetName);
|
|
108
|
+
const targetIndex = actorIndex.get(targetName);
|
|
109
|
+
if (target === undefined || targetIndex === undefined) return;
|
|
110
|
+
if (
|
|
111
|
+
targetIndex > decisionIndex &&
|
|
112
|
+
target.kind !== "decision" &&
|
|
113
|
+
target.kind !== "mark-start" &&
|
|
114
|
+
target.kind !== "mark-end" &&
|
|
115
|
+
!nodeToneByName.has(targetName)
|
|
116
|
+
) {
|
|
117
|
+
nodeToneByName.set(targetName, tone);
|
|
118
|
+
}
|
|
119
|
+
if (targetIndex <= decisionIndex || target.kind === "decision") return;
|
|
120
|
+
for (const nextEdgeIndex of outgoing.get(targetName) ?? []) follow(nextEdgeIndex);
|
|
121
|
+
};
|
|
122
|
+
follow(branchEdgeIndex);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return { edgeToneByIndex, nodeToneByName };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* この共通の組み立てへ回すか (#2348)。
|
|
131
|
+
*
|
|
132
|
+
* **条件を 1 箇所に集める**。 図種ごとの入口が同じ条件を手で並べていた間、
|
|
133
|
+
* 「縦列を書いた形」 が 2 図種 (状態の図 / 表の図) で抜け落ちていた =
|
|
134
|
+
* 動きを書かない図では、書いた縦列に箱が入らず、代わりに箱ごとの縦列が作られ、
|
|
135
|
+
* 「どの箱も入らない縦列です」 という **事実と逆の知らせ** だけが出ていた。
|
|
136
|
+
* その 2 図種は #2782 で `record` に畳み、経路を 1 本にしたのでここを通らない。
|
|
137
|
+
*
|
|
138
|
+
* 2 つの条件はどちらも「静止図の経路が持っていない並べ方を書いた」 ことを意味する。
|
|
139
|
+
* 静止図の経路は並びを固定で持つため、通すと書いた指定が黙って消える (#1263)。
|
|
140
|
+
*
|
|
141
|
+
* `図種ごとの理由` はその図種にしか無い条件 (泳法図の向き) を渡す。
|
|
142
|
+
* フローは `鎖でつなぐ形か` が同じ判定を内側に持つため、ここは通らない。
|
|
143
|
+
*/
|
|
144
|
+
export function 共通の組み立てへ回す(
|
|
145
|
+
kind: GenericKind,
|
|
146
|
+
doc: DslDocument,
|
|
147
|
+
図種ごとの理由 = false,
|
|
148
|
+
): boolean {
|
|
149
|
+
if (doc.animate !== undefined && doc.animate.phases.length > 0) return true;
|
|
150
|
+
if (書いた縦列に置く(kind, doc)) return true;
|
|
151
|
+
return 図種ごとの理由;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* v0.4 ... 5 preset (flow / swimlane / er / state / topology) 共通 animation compile。
|
|
156
|
+
* sequence preset と異なり header / footer / step box 構造はない、 シンプルな lane + node + edge 構造。
|
|
157
|
+
* preset kind ごとに lane 配置と layout を切替。
|
|
158
|
+
*/
|
|
159
|
+
export function compileGenericWithAnimate(doc: DslDocument, opts: GenericOpts): CdlDiagram {
|
|
160
|
+
const { kind, laneWidth } = opts;
|
|
161
|
+
const b = diagram(slugify(doc.title), { topic: doc.title, type: kind });
|
|
162
|
+
|
|
163
|
+
/*
|
|
164
|
+
* 行を持つ図で、書かなかった時の既定 (#2352 / #2782)。
|
|
165
|
+
*
|
|
166
|
+
* **行を書いた箱は表の箱 (`storage`)、書かない箱は札 (`card`)**。 畳む前は図種で
|
|
167
|
+
* 決めており (移り変わりの図は札、表の図は書かないと人の形)、同じ本文でも動きの段を
|
|
168
|
+
* 1 つ足すだけで箱が札の形から人の形に変わっていた。
|
|
169
|
+
*
|
|
170
|
+
* 書いた種類はそのまま通す。 **書いたかどうかは `kindWritten` で見る** = 書かなかった箱の
|
|
171
|
+
* `kind` には既定の `actor` が入るので、値だけでは「`actor` と書いた」 と「書かなかった」 を
|
|
172
|
+
* 分けられない。
|
|
173
|
+
*/
|
|
174
|
+
const 箱の種類 = (a: (typeof doc.actors)[number]): NodeKind => {
|
|
175
|
+
// 段の箱は札で固定するのでここを通らない。路線図と時間軸の印は書いた種類で描く
|
|
176
|
+
if (泳法図で印をどう描く(doc, a) === "描く") return 描ける種別(a.kind);
|
|
177
|
+
if (kind !== "record" || a.kindWritten === true) return 描ける種別(a.kind);
|
|
178
|
+
return (a.rows?.length ?? 0) > 0 ? "storage" : "card";
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* 始まりと終わりと途中の札 (#2349 / #2352)。
|
|
183
|
+
*
|
|
184
|
+
* **並べ方の分岐ごとに書かない**。 3 つある並べ方のうち 1 つにしか札の処理が無く、
|
|
185
|
+
* 縦列を書いた状態の図では `initial: true` を書いても札が 1 つも出なかった
|
|
186
|
+
* (知らせも無い = 縦列を足しただけで別の意味の図になっていた)。
|
|
187
|
+
*
|
|
188
|
+
* 札を出すのは状態の図だけ。 どの箱が始まり / 終わりかは `始まりと終わりの決め方` が
|
|
189
|
+
* 1 箇所で決める。 **書いた値はどの並べ方でも効き**、1 つも書いていない時の既定
|
|
190
|
+
* (並びの最初と最後) は、並びが意味を持つ並べ方でだけ効く。
|
|
191
|
+
*
|
|
192
|
+
* **札を出すのは既定の形の箱だけ** (#2352)。 印の箱 (`mark-start` / `mark-end`) は
|
|
193
|
+
* 形そのものが始点と終点を表すので、字を重ねると同じことを 2 度言う
|
|
194
|
+
* (実測 = #2349 で既定を無条件に配った時、印の箱に「初期」 の字が出て見本が壊れた)。
|
|
195
|
+
*/
|
|
196
|
+
/*
|
|
197
|
+
* **並びで決める既定を使わない** (#2782)。 書いた値だけを見る。
|
|
198
|
+
*
|
|
199
|
+
* 並びから推し量ると、1 つだけ書いた図でもう片方が勝手に決まる
|
|
200
|
+
* (実測 = 真ん中の箱に始まりを書いた図で、最後の箱に「最終」 が出た)。
|
|
201
|
+
*/
|
|
202
|
+
const 決め方 = 始まりと終わりの決め方(doc, { 並びで決める: false });
|
|
203
|
+
/*
|
|
204
|
+
* 始まりと終わりの札 (#2349 / #2352 / #2782)。
|
|
205
|
+
*
|
|
206
|
+
* **書いた箱にだけ出す**。 畳む前は移り変わりの図だけが札を出し、1 つも書かない図では
|
|
207
|
+
* 並びの最初と最後を始まり / 終わりとみなし、残りの箱に「状態」 の字を配っていた。
|
|
208
|
+
* 畳んだ後も同じ形にすると、行を並べた表の箱に「状態」 の字が出る = 書いた人が
|
|
209
|
+
* 言っていないことを図が名乗る。
|
|
210
|
+
*
|
|
211
|
+
* **札を出すのは札の形の箱だけ**。 印の箱 (`mark-start` / `mark-end`) は形そのものが
|
|
212
|
+
* 始点と終点を表すので、字を重ねると同じことを 2 度言う。
|
|
213
|
+
*/
|
|
214
|
+
const 札 = (a: (typeof doc.actors)[number], idx: number): { eyebrow?: string } => {
|
|
215
|
+
if (kind !== "record") return {};
|
|
216
|
+
if (箱の種類(a) !== "card") return {};
|
|
217
|
+
if (決め方.始まり(a, idx)) return { eyebrow: "初期" };
|
|
218
|
+
if (決め方.終わり(a, idx)) return { eyebrow: "最終" };
|
|
219
|
+
return {};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// lane / node 配置 ... preset kind に応じて切替
|
|
223
|
+
const actorToNodeId = new Map<string, string>();
|
|
224
|
+
const 段階ごとの箱か = kind === "swimlane" && doc.shape === "stages";
|
|
225
|
+
const 路線図か = kind === "swimlane" && doc.shape === "metro";
|
|
226
|
+
const 時間軸か = kind === "swimlane" && doc.shape === "timeline";
|
|
227
|
+
const 路線図の色 = 路線図か ? metroBranchTones(doc) : undefined;
|
|
228
|
+
let timelineActorIndex: Map<string, number> | undefined;
|
|
229
|
+
let timelineNumberIdByCardId: Map<string, string> | undefined;
|
|
230
|
+
let stagesPlacement: StagesPlacement | undefined;
|
|
231
|
+
if (時間軸か) {
|
|
232
|
+
const numberIdByCardId = new Map<string, string>();
|
|
233
|
+
timelineNumberIdByCardId = numberIdByCardId;
|
|
234
|
+
const actorsByName = new Map(doc.actors.map((actor) => [actor.name, actor]));
|
|
235
|
+
const numberToneByActorName = new Map(
|
|
236
|
+
doc.flow.flatMap((edge) =>
|
|
237
|
+
actorsByName.get(edge.from)?.kind === "decision" && edge.tone !== undefined
|
|
238
|
+
? [[edge.to, edge.tone] as const]
|
|
239
|
+
: []),
|
|
240
|
+
);
|
|
241
|
+
const 軸に置く段 = new Set(
|
|
242
|
+
doc.actors
|
|
243
|
+
.filter((actor) => 泳法図で印をどう描く(doc, actor) === "描く")
|
|
244
|
+
.map((actor) => actor.name),
|
|
245
|
+
);
|
|
246
|
+
const placement = placeTimeline(doc, 軸に置く段);
|
|
247
|
+
const timelineStageByName = new Map<string, number>();
|
|
248
|
+
timelineActorIndex = timelineStageByName;
|
|
249
|
+
doc.actors.forEach((actor, index) => {
|
|
250
|
+
const step = placement.steps[index];
|
|
251
|
+
if (step === undefined) throw new Error(`時間軸の段 "${actor.name}" の配置がありません`);
|
|
252
|
+
timelineStageByName.set(actor.name, step.stage);
|
|
253
|
+
});
|
|
254
|
+
b.lane("timeline-axis", {
|
|
255
|
+
width: TIMELINE_AXIS_WIDTH,
|
|
256
|
+
role: "overlay",
|
|
257
|
+
timelineAxis: true,
|
|
258
|
+
posX: placement.axisX - TIMELINE_AXIS_WIDTH / 2,
|
|
259
|
+
posY: 0,
|
|
260
|
+
posW: TIMELINE_AXIS_WIDTH,
|
|
261
|
+
posH: placement.axisHeight,
|
|
262
|
+
});
|
|
263
|
+
b.lane("timeline-steps", {
|
|
264
|
+
width: placement.width,
|
|
265
|
+
role: "overlay",
|
|
266
|
+
posX: 0,
|
|
267
|
+
posY: 0,
|
|
268
|
+
posW: placement.width,
|
|
269
|
+
posH: placement.stepsHeight,
|
|
270
|
+
});
|
|
271
|
+
doc.actors.forEach((actor, index) => {
|
|
272
|
+
const step = placement.steps[index];
|
|
273
|
+
if (step === undefined) throw new Error(`時間軸の段 "${actor.name}" の配置がありません`);
|
|
274
|
+
actorToNodeId.set(actor.name, step.cardId);
|
|
275
|
+
if (泳法図で印をどう描く(doc, actor) === "描く") {
|
|
276
|
+
const markSize = actor.kind === "decision"
|
|
277
|
+
? { w: TIMELINE_DECISION_WIDTH, h: TIMELINE_DECISION_HEIGHT }
|
|
278
|
+
: actor.kind === "mark-end"
|
|
279
|
+
? { w: TIMELINE_END_WIDTH, h: TIMELINE_END_HEIGHT }
|
|
280
|
+
: {};
|
|
281
|
+
b.node(step.cardId, {
|
|
282
|
+
lane: "timeline-axis",
|
|
283
|
+
stack: index,
|
|
284
|
+
kind: 箱の種類(actor),
|
|
285
|
+
title: 箱の題(actor),
|
|
286
|
+
...(actor.subtitlePlacement !== undefined
|
|
287
|
+
? { subtitlePlacement: actor.subtitlePlacement }
|
|
288
|
+
: {}),
|
|
289
|
+
...markSize,
|
|
290
|
+
posX: placement.axisX,
|
|
291
|
+
posY: step.y,
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const numberId = step.numberId;
|
|
296
|
+
if (numberId !== undefined) {
|
|
297
|
+
numberIdByCardId.set(step.cardId, numberId);
|
|
298
|
+
const numberTone = numberToneByActorName.get(actor.name);
|
|
299
|
+
b.node(numberId, {
|
|
300
|
+
lane: "timeline-axis",
|
|
301
|
+
stack: index,
|
|
302
|
+
kind: "timeline-number",
|
|
303
|
+
title: String(numberIdByCardId.size),
|
|
304
|
+
w: TIMELINE_NUMBER_SIZE,
|
|
305
|
+
h: TIMELINE_NUMBER_SIZE,
|
|
306
|
+
...(numberTone !== undefined ? { tone: numberTone } : {}),
|
|
307
|
+
posX: placement.axisX,
|
|
308
|
+
posY: step.y,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
b.node(step.cardId, {
|
|
312
|
+
lane: "timeline-steps",
|
|
313
|
+
stack: index,
|
|
314
|
+
kind: "card",
|
|
315
|
+
title: 箱の題(actor),
|
|
316
|
+
...(actor.lane !== undefined
|
|
317
|
+
? { subtitle: doc.lanes?.[actor.lane]?.label ?? actor.lane }
|
|
318
|
+
: {}),
|
|
319
|
+
...(actor.subtitlePlacement !== undefined
|
|
320
|
+
? { subtitlePlacement: actor.subtitlePlacement }
|
|
321
|
+
: {}),
|
|
322
|
+
...(numberId !== undefined ? { leaderTo: numberId } : {}),
|
|
323
|
+
w: step.cardWidth,
|
|
324
|
+
h: TIMELINE_CARD_HEIGHT,
|
|
325
|
+
posX: step.cardX,
|
|
326
|
+
posY: step.y,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
} else if (路線図か) {
|
|
330
|
+
const placement = placeMetro(doc);
|
|
331
|
+
for (const track of placement.tracks) {
|
|
332
|
+
b.lane(track.id, {
|
|
333
|
+
width: placement.width,
|
|
334
|
+
label: track.label,
|
|
335
|
+
metro: track.subtitle === undefined ? {} : { subtitle: track.subtitle },
|
|
336
|
+
posX: track.posX,
|
|
337
|
+
posY: track.posY,
|
|
338
|
+
posW: track.posW,
|
|
339
|
+
posH: track.posH,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
doc.actors.forEach((actor, index) => {
|
|
343
|
+
const station = placement.stations[index]!;
|
|
344
|
+
const id = slugify(actor.name) || `n${index}`;
|
|
345
|
+
const markFrameSize = metroMarkFrameSize(actor.kind);
|
|
346
|
+
actorToNodeId.set(actor.name, id);
|
|
347
|
+
b.node(id, {
|
|
348
|
+
lane: station.trackId,
|
|
349
|
+
stack: index,
|
|
350
|
+
kind: 泳法図で印をどう描く(doc, actor) === "描く" ? 箱の種類(actor) : "station",
|
|
351
|
+
title: 箱の題(actor),
|
|
352
|
+
...(actor.stationNamePosition !== undefined
|
|
353
|
+
? { stationNamePosition: actor.stationNamePosition }
|
|
354
|
+
: {}),
|
|
355
|
+
...(路線図の色?.nodeToneByName.get(actor.name) !== undefined
|
|
356
|
+
? { tone: 路線図の色.nodeToneByName.get(actor.name) }
|
|
357
|
+
: {}),
|
|
358
|
+
...(markFrameSize === undefined ? {} : { w: markFrameSize, h: markFrameSize }),
|
|
359
|
+
posX: station.posX,
|
|
360
|
+
posY: station.posY,
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
} else if (段階ごとの箱か) {
|
|
364
|
+
/*
|
|
365
|
+
* 段階ごとに同じ高さの列を左から並べ、その中へ札を積む (#2797 / #2831)。
|
|
366
|
+
*
|
|
367
|
+
* 列の見出し (`段階 N` と段階の名前) と、札の右の担当は描く側が描く。
|
|
368
|
+
* 列と札の決め方は `placeStages` が持ち、静止図と動く図を同じ配置に揃える。
|
|
369
|
+
* 種類は札 (`card`) で固定する = 札の右の字は描く側が `card` にだけ描く。
|
|
370
|
+
*/
|
|
371
|
+
const placement = placeStages(doc);
|
|
372
|
+
stagesPlacement = placement;
|
|
373
|
+
const lastStack = Math.max(0, ...[...placement.cards.values()].map((card) => card.stack));
|
|
374
|
+
const stageLaneHeight =
|
|
375
|
+
STAGE_FIRST_CARD_CENTER_Y + lastStack * STAGE_CARD_PITCH + STAGE_CARD_HEIGHT / 2;
|
|
376
|
+
for (const [columnIndex, column] of placement.columns.entries()) {
|
|
377
|
+
b.lane(column.id, {
|
|
378
|
+
width: laneWidth,
|
|
379
|
+
// cdl は段階列の面の間を 48 取る。見本の 64 にするため、
|
|
380
|
+
// lane 自体の間に残りの 16 を書き、描画側の列幅は変えない。
|
|
381
|
+
posX: columnIndex * (laneWidth + STAGE_LANE_GAP),
|
|
382
|
+
posY: STAGE_LANE_Y,
|
|
383
|
+
posW: laneWidth,
|
|
384
|
+
posH: stageLaneHeight,
|
|
385
|
+
stage: {
|
|
386
|
+
number: column.number,
|
|
387
|
+
name: column.name,
|
|
388
|
+
...(doc.stageHeader === undefined ? {} : { header: doc.stageHeader }),
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
doc.actors.forEach((a, idx) => {
|
|
393
|
+
const id = slugify(a.name) || `n${idx}`;
|
|
394
|
+
actorToNodeId.set(a.name, id);
|
|
395
|
+
const card = placement.cards.get(a.name);
|
|
396
|
+
if (card === undefined) throw new Error(`段の箱の "${a.name}" を置く列がありません`);
|
|
397
|
+
const columnName = placement.columns[card.column]?.name;
|
|
398
|
+
const writtenColumnWidth = columnName === undefined
|
|
399
|
+
? undefined
|
|
400
|
+
: doc.stageLayout?.columnWidths?.[columnName];
|
|
401
|
+
b.node(id, {
|
|
402
|
+
lane: card.columnId,
|
|
403
|
+
stack: card.stack,
|
|
404
|
+
w: writtenColumnWidth === undefined
|
|
405
|
+
? STAGE_CARD_WIDTH
|
|
406
|
+
: Math.max(1, writtenColumnWidth - STAGE_CARD_COLUMN_INSET),
|
|
407
|
+
h: STAGE_CARD_HEIGHT,
|
|
408
|
+
posX: card.column * (laneWidth + STAGE_LANE_GAP) + laneWidth / 2,
|
|
409
|
+
posY: STAGE_FIRST_CARD_CENTER_Y + card.stack * STAGE_CARD_PITCH,
|
|
410
|
+
kind: "card",
|
|
411
|
+
title: 箱の題(a),
|
|
412
|
+
...(card.subtitle === undefined ? {} : { subtitle: card.subtitle }),
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
} else if (書いた縦列に置く(kind, doc)) {
|
|
416
|
+
/*
|
|
417
|
+
* **書いた縦列に置く** (#1263)。 縦列を並べるための入れ物として使う図種でだけ効く。
|
|
418
|
+
*
|
|
419
|
+
* 縦列の並びは **`lanes:` に書いた順** を優先する (#1394)。 箱が最初に使った順で
|
|
420
|
+
* 並べていた間、`lanes:` で左から順に宣言しても箱の書き順で入れ替わっていた
|
|
421
|
+
* (実測 = 中心を先に書いた放射の図で、左端の縦列が中心の右へ回った)。
|
|
422
|
+
*
|
|
423
|
+
* `lanes:` に無い縦列は、これまでどおり箱が使った順で後ろに続ける。
|
|
424
|
+
*/
|
|
425
|
+
const 使った: string[] = [];
|
|
426
|
+
for (const a of doc.actors) {
|
|
427
|
+
const lid = a.lane;
|
|
428
|
+
if (lid !== undefined && !使った.includes(lid)) 使った.push(lid);
|
|
429
|
+
}
|
|
430
|
+
const 書いた順 = doc.lanes ? Object.keys(doc.lanes) : [];
|
|
431
|
+
const 並び = [
|
|
432
|
+
...書いた順.filter((lid) => 使った.includes(lid)),
|
|
433
|
+
...使った.filter((lid) => !書いた順.includes(lid)),
|
|
434
|
+
];
|
|
435
|
+
for (const lid of 並び) {
|
|
436
|
+
b.lane(lid, { width: laneWidth, ...(kind === "topology" ? { contain: true } : {}) });
|
|
437
|
+
}
|
|
438
|
+
// 段の決め方は `縦列ごとの段を決める` が持つ (#1394 / #2396)。
|
|
439
|
+
// 箱を並べる図種すべてが同じ決め方を使う = 2 か所に置くと片方だけ直した日に食い違う
|
|
440
|
+
const 段 = 縦列ごとの段を決める(doc.actors);
|
|
441
|
+
doc.actors.forEach((a, idx) => {
|
|
442
|
+
const id = slugify(a.name) || `n${idx}`;
|
|
443
|
+
actorToNodeId.set(a.name, id);
|
|
444
|
+
const lid = a.lane!;
|
|
445
|
+
b.node(id, {
|
|
446
|
+
lane: lid,
|
|
447
|
+
stack: 段.get(a) ?? 0,
|
|
448
|
+
kind: 箱の種類(a),
|
|
449
|
+
title: 箱の題(a),
|
|
450
|
+
...札(a, idx),
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
} else if (並べる向き(kind, doc) === "縦") {
|
|
454
|
+
// 1 lane に全 actor を縦 stack
|
|
455
|
+
const lid = opts.laneId ?? "main";
|
|
456
|
+
b.lane(lid, {
|
|
457
|
+
width: laneWidth,
|
|
458
|
+
/*
|
|
459
|
+
* **見出しは自然に縦へ積む図種だけ** (#1494)。
|
|
460
|
+
*
|
|
461
|
+
* `direction: 縦` を書いた泳法図がここへ来るようになった。 その図に題を渡すと、図の題が
|
|
462
|
+
* 縦列の見出しとしてもう 1 度出る (実測 = 「認証の流れ」 が題と見出しの 2 箇所に並んだ)。
|
|
463
|
+
*/
|
|
464
|
+
...(kind === "flow" || kind === "topology" ? { label: doc.title } : {}),
|
|
465
|
+
...(kind === "topology" ? { contain: true } : {}),
|
|
466
|
+
});
|
|
467
|
+
doc.actors.forEach((a, idx) => {
|
|
468
|
+
const id = slugify(a.name) || `n${idx}`;
|
|
469
|
+
actorToNodeId.set(a.name, id);
|
|
470
|
+
b.node(id, {
|
|
471
|
+
lane: lid,
|
|
472
|
+
stack: idx,
|
|
473
|
+
kind: 箱の種類(a),
|
|
474
|
+
title: 箱の題(a),
|
|
475
|
+
...札(a, idx),
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
} else {
|
|
479
|
+
// actor ごとに 1 lane (横並び)。 `swimlane` / `record` の既定と、
|
|
480
|
+
// `向き: 横` を書いたフローがここに来る (#1494)
|
|
481
|
+
//
|
|
482
|
+
// **見出しを付けるのは `swimlane` だけ** (#1241)。 どちらも箱を 1 つずつ持ち、
|
|
483
|
+
// その箱が既に名前を描く。 縦列にも同じ名前を渡すと **同じ字が縦に 2 つ並ぶ**
|
|
484
|
+
// (実測 = 描いた絵に `Alpha` `Beta` が 2 度出る)。
|
|
485
|
+
//
|
|
486
|
+
// `swimlane` は縦列そのものが「誰の担当か」 を読ませる図なので見出しが要る。
|
|
487
|
+
// `record` の縦列は箱を並べるための入れ物で、読む人に見せる意味を持たない
|
|
488
|
+
// (組立て API 側も見出しを空のまま置く)。
|
|
489
|
+
const 見出しを付ける = kind === "swimlane";
|
|
490
|
+
doc.actors.forEach((a, idx) => {
|
|
491
|
+
const lid = `lane-${slugify(a.name) || idx}`;
|
|
492
|
+
b.lane(lid, { width: laneWidth, ...(見出しを付ける ? { label: a.name } : {}) });
|
|
493
|
+
const id = slugify(a.name) || `n${idx}`;
|
|
494
|
+
actorToNodeId.set(a.name, id);
|
|
495
|
+
b.node(id, {
|
|
496
|
+
lane: lid,
|
|
497
|
+
stack: 0,
|
|
498
|
+
kind: 箱の種類(a),
|
|
499
|
+
title: 箱の題(a),
|
|
500
|
+
...札(a, idx),
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// edge ... flow の各 step を edge として登録
|
|
506
|
+
//
|
|
507
|
+
// 解決できない名前の矢印は **落とす** (#1209)。 以前は名前をそのまま id として使っており、
|
|
508
|
+
// 存在しない node を指す図ができて描画の直前で落ちていた
|
|
509
|
+
// (実測 = `unknown-ref: edge "e0-v-c" の from "v" が node に存在しません`)。
|
|
510
|
+
// 書いた人には `flow-actor-missing` の知らせが届く。
|
|
511
|
+
// 箱の並び。 後ろへ戻る矢印を見分けるために使う (#1260)
|
|
512
|
+
const 箱の並び = new Map([...actorToNodeId.values()].map((id, i) => [id, i]));
|
|
513
|
+
const edgeIds: string[] = [];
|
|
514
|
+
doc.flow.forEach((s, idx) => {
|
|
515
|
+
// 名前は入口で正規化済 (`canonicalizeFlowActors`)。 ここで slug を受け直すと、
|
|
516
|
+
// 動きを書いていない図の組み立てと扱いが割れる
|
|
517
|
+
const fromId = actorToNodeId.get(s.from);
|
|
518
|
+
const toId = actorToNodeId.get(s.to);
|
|
519
|
+
if (fromId === undefined || toId === undefined) return;
|
|
520
|
+
const edgeId = `e${idx}-${fromId}-${toId}`;
|
|
521
|
+
// 多重度と名前と両端は 1 か所で作る (#2105)。 札に `(1:N)` と添えるだけだった間、
|
|
522
|
+
// 段を持つ図には端の形が 1 つも付かなかった。
|
|
523
|
+
//
|
|
524
|
+
// **端の既定は持たない** (#2591 / #2595 / #2782)。 畳む前は移り変わりの図にだけ
|
|
525
|
+
// 「実線に開いた矢」 を無条件で渡していたが、畳んだ後も渡すと多重度から導いた端を
|
|
526
|
+
// 上から潰す = 箱どうしの個数が消える。 書いた端だけを渡す形に倒す
|
|
527
|
+
const 関係 = opts.edgeDefaults?.(s);
|
|
528
|
+
const timelineEdgeKind = timelineActorIndex !== undefined
|
|
529
|
+
? classifyTimelineEdge(timelineActorIndex.get(s.from)!, timelineActorIndex.get(s.to)!)
|
|
530
|
+
: undefined;
|
|
531
|
+
const timelineHorizontalBranch = timelineEdgeKind === "branch"
|
|
532
|
+
&& timelineActorIndex?.get(s.from) === timelineActorIndex?.get(s.to);
|
|
533
|
+
const edgeFromId =
|
|
534
|
+
timelineEdgeKind === "advance" ? (timelineNumberIdByCardId!.get(fromId) ?? fromId) : fromId;
|
|
535
|
+
const edgeToId =
|
|
536
|
+
timelineEdgeKind === "advance" ? (timelineNumberIdByCardId!.get(toId) ?? toId) : toId;
|
|
537
|
+
const timelineEnd = 時間軸か && doc.actors.find((actor) => actor.name === s.to)?.kind === "mark-end";
|
|
538
|
+
b.edge(edgeFromId, edgeToId, {
|
|
539
|
+
id: edgeId,
|
|
540
|
+
label: 関係?.label ?? s.label,
|
|
541
|
+
...(関係?.tone !== undefined ? { tone: 関係.tone } : {}),
|
|
542
|
+
...(関係?.style !== undefined ? { style: 関係.style } : {}),
|
|
543
|
+
...(路線図の色?.edgeToneByIndex.get(idx) !== undefined
|
|
544
|
+
? { tone: 路線図の色.edgeToneByIndex.get(idx) }
|
|
545
|
+
: {}),
|
|
546
|
+
...(timelineEdgeKind === "back"
|
|
547
|
+
? { routing: "back-detour" as const, fromSide: "right" as const, toSide: "right" as const }
|
|
548
|
+
: 路線図か
|
|
549
|
+
? { routing: "metro" as const }
|
|
550
|
+
: stagesPlacement !== undefined
|
|
551
|
+
? 段の箱の線の指定(stagesPlacement, s)
|
|
552
|
+
: 後ろへ戻る矢印か(kind, fromId, toId, 箱の並び, 書いた縦列に置く(kind, doc))
|
|
553
|
+
? { routing: "back-detour" as const }
|
|
554
|
+
: {}),
|
|
555
|
+
...(関係?.sub ? { sub: 関係.sub } : {}),
|
|
556
|
+
...(関係?.head ? { head: 関係.head } : {}),
|
|
557
|
+
...(関係?.headFill ? { headFill: 関係.headFill } : {}),
|
|
558
|
+
...(路線図か && s.style !== "dashed" ? { head: "none" as const } : {}),
|
|
559
|
+
...(関係?.tailHead ? { tailHead: 関係.tailHead } : {}),
|
|
560
|
+
...(関係?.tailHeadFill ? { tailHeadFill: 関係.tailHeadFill } : {}),
|
|
561
|
+
...(関係?.headLabel ? { headLabel: 関係.headLabel } : {}),
|
|
562
|
+
...(関係?.tailLabel ? { tailLabel: 関係.tailLabel } : {}),
|
|
563
|
+
...(s.side ? { side: s.side } : {}),
|
|
564
|
+
...(s.fromSide !== undefined ? { fromSide: s.fromSide } : {}),
|
|
565
|
+
...(s.toSide !== undefined ? { toSide: s.toSide } : {}),
|
|
566
|
+
...(timelineHorizontalBranch && s.label !== ""
|
|
567
|
+
? {
|
|
568
|
+
overlay: true,
|
|
569
|
+
labelCollision: "author" as const,
|
|
570
|
+
labelSize: 時間軸の横分岐の札の大きさ,
|
|
571
|
+
labelOffsetY: 時間軸の横分岐の札の下げ幅,
|
|
572
|
+
}
|
|
573
|
+
: timelineEdgeKind !== undefined && s.label !== ""
|
|
574
|
+
? { overlay: true }
|
|
575
|
+
: {}),
|
|
576
|
+
...(timelineEdgeKind === "advance" && !timelineEnd ? { head: "none" as const } : {}),
|
|
577
|
+
});
|
|
578
|
+
edgeIds.push(edgeId);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// state 登録
|
|
582
|
+
for (const st of doc.animate?.states ?? []) {
|
|
583
|
+
b.state(st.name, { initial: st.initial });
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// phase 注入
|
|
587
|
+
for (const p of doc.animate?.phases ?? []) {
|
|
588
|
+
b.phase(
|
|
589
|
+
slugify(p.name) || p.name,
|
|
590
|
+
{
|
|
591
|
+
duration: p.durationMs,
|
|
592
|
+
title: p.name,
|
|
593
|
+
body: p.body ?? "",
|
|
594
|
+
},
|
|
595
|
+
(pb) => {
|
|
596
|
+
const activateIds = resolveHighlightGeneric(p, doc, actorToNodeId, edgeIds);
|
|
597
|
+
if (timelineNumberIdByCardId !== undefined) {
|
|
598
|
+
const activeNumberIds = new Set<string>();
|
|
599
|
+
for (const id of activateIds) {
|
|
600
|
+
const numberId = timelineNumberIdByCardId.get(id);
|
|
601
|
+
if (numberId !== undefined) activeNumberIds.add(numberId);
|
|
602
|
+
}
|
|
603
|
+
for (const numberId of timelineNumberIdByCardId.values()) {
|
|
604
|
+
if (activeNumberIds.has(numberId)) activateIds.push(numberId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (activateIds.length > 0) {
|
|
608
|
+
pb.activate(...activateIds);
|
|
609
|
+
}
|
|
610
|
+
for (const t of p.tweens ?? []) {
|
|
611
|
+
pb.tween(t.state, t.from, t.to);
|
|
612
|
+
}
|
|
613
|
+
for (const s of p.sets ?? []) {
|
|
614
|
+
pb.set(s.state, s.value);
|
|
615
|
+
}
|
|
616
|
+
if (p.badge) {
|
|
617
|
+
pb.badge(p.badge);
|
|
618
|
+
}
|
|
619
|
+
return pb;
|
|
620
|
+
},
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const built = b.build();
|
|
625
|
+
if (時間軸か) {
|
|
626
|
+
const endIds = new Set(
|
|
627
|
+
doc.actors.filter((actor) => actor.kind === "mark-end").map((actor) => actorToNodeId.get(actor.name)),
|
|
628
|
+
);
|
|
629
|
+
for (const edge of built.edges) {
|
|
630
|
+
// `head` を書かないことが既定の三角を表すため、終わりへ入る線には欄自体を残さない。
|
|
631
|
+
if (endIds.has(edge.to) && edge.head === undefined) delete edge.head;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return built;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* 5 preset 共通 ... highlight item (actor 名 or "A→B") を node/edge id に解決。
|
|
639
|
+
* sequence と異なり header/footer/step box はないのでシンプル。
|
|
640
|
+
*/
|
|
641
|
+
export function resolveHighlightGeneric(
|
|
642
|
+
phase: DslPhase,
|
|
643
|
+
doc: DslDocument,
|
|
644
|
+
actorToNodeId: Map<string, string>,
|
|
645
|
+
edgeIds: string[],
|
|
646
|
+
): string[] {
|
|
647
|
+
const out: string[] = [];
|
|
648
|
+
const knownNames = new Set(actorToNodeId.keys());
|
|
649
|
+
for (const raw of phase.highlight ?? []) {
|
|
650
|
+
const entry = parseFocusEntry(raw, knownNames);
|
|
651
|
+
// 矢印あり → edge を特定
|
|
652
|
+
if (entry.kind === "edge") {
|
|
653
|
+
const fromId = actorToNodeId.get(entry.from) ?? slugify(entry.from);
|
|
654
|
+
const toId = actorToNodeId.get(entry.to) ?? slugify(entry.to);
|
|
655
|
+
// edge id は `e{idx}-{fromId}-{toId}` の形。 末尾一致で見る。
|
|
656
|
+
// 部分一致で見ると、 名前に `-` を含む箱 (`api-gateway`) の id が別の矢印の id に
|
|
657
|
+
// 混ざって当たる (実測 = 箱を光らせたい指定で矢印が光った)
|
|
658
|
+
for (const edgeId of edgeIds) {
|
|
659
|
+
if (edgeId.endsWith(`-${fromId}-${toId}`)) {
|
|
660
|
+
out.push(edgeId);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
// actor 名 → node id。 見つからなければ slug の形でも探す。
|
|
666
|
+
// 順序図だけが slug を受理する状態にすると、 同じ記述が図種で別の意味になる
|
|
667
|
+
// (実測 = `api-gateway` が順序図では光り、 フローでは何も光らなかった)
|
|
668
|
+
const nodeId = actorToNodeId.get(entry.name) ?? slugLookup(actorToNodeId, entry.name);
|
|
669
|
+
if (nodeId) {
|
|
670
|
+
out.push(nodeId);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return out;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* 名前が見つからない時に、 slug の形でも探す。
|
|
678
|
+
*
|
|
679
|
+
* 記法は表示名で書くが、 書く人は id の形 (`api-gateway`) で書くこともある。 図種によって
|
|
680
|
+
* 受理する / しないが分かれると、 同じ記述が別の意味になる。
|
|
681
|
+
*
|
|
682
|
+
* 2 つ以上の名前が同じ slug になる時は解決しない。 どちらを指したか決められないため、
|
|
683
|
+
* 黙ってどちらかを選ぶより光らせない方が書いた人が気付ける。
|
|
684
|
+
*/
|
|
685
|
+
export function slugLookup(
|
|
686
|
+
byName: ReadonlyMap<string, string>,
|
|
687
|
+
wanted: string,
|
|
688
|
+
): string | undefined {
|
|
689
|
+
let hit: string | undefined;
|
|
690
|
+
for (const [name, id] of byName) {
|
|
691
|
+
if (slugify(name) !== wanted) continue;
|
|
692
|
+
if (hit !== undefined) return undefined;
|
|
693
|
+
hit = id;
|
|
694
|
+
}
|
|
695
|
+
return hit;
|
|
696
|
+
}
|