@cardenelabs/dragon 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -720
- package/dist/cli.cjs +4529 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +4523 -0
- package/dist/cli.js.map +1 -0
- package/dist/export.cjs +3912 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +36 -0
- package/dist/export.d.ts +36 -0
- package/dist/export.js +3907 -0
- package/dist/export.js.map +1 -0
- package/dist/fonts.cjs +7 -0
- package/dist/fonts.cjs.map +1 -0
- package/dist/fonts.js +4 -0
- package/dist/fonts.js.map +1 -0
- package/dist/index.cjs +4105 -20356
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -5522
- package/dist/index.d.ts +227 -5522
- package/dist/index.js +4091 -20283
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -0
- package/dist/schema-BBWfniwH.d.cts +112 -0
- package/dist/schema-BBWfniwH.d.ts +112 -0
- package/package.json +39 -24
- package/src/cli.ts +237 -0
- package/src/export/capture.ts +277 -0
- package/src/export/fonts.ts +68 -0
- package/src/export/html.ts +32 -0
- package/src/export/index.ts +146 -0
- package/src/field-messages.ts +104 -0
- package/src/figure-text.ts +42 -0
- package/src/index.ts +30 -283
- package/src/layout/architecture.ts +381 -0
- package/src/layout/measure.ts +21 -0
- package/src/layout/scene.ts +353 -0
- package/src/layout/sequence.ts +189 -0
- package/src/load-fonts.ts +8 -0
- package/src/look/fonts.ts +33 -0
- package/src/look/palettes.json +209 -0
- package/src/look/values.ts +155 -0
- package/src/markdown.ts +35 -0
- package/src/messages.ts +122 -0
- package/src/parser.ts +342 -310
- package/src/player-global.ts +4 -0
- package/src/player.ts +271 -0
- package/src/printer.ts +76 -0
- package/src/render/icons.ts +41 -0
- package/src/render/motion.ts +106 -0
- package/src/render/parts.ts +279 -0
- package/src/render/svg.ts +341 -0
- package/src/schema.ts +161 -16
- package/src/schemas/diagram.schema.json +150 -0
- package/src/schemas/validate-diagram.d.ts +12 -0
- package/src/schemas/validate-diagram.js +8 -0
- package/src/timeline.ts +166 -0
- package/src/tokenize.ts +20 -281
- package/src/version.ts +55 -0
- package/src/view.ts +16 -0
- package/src/words.ts +79 -0
- package/examples/quick-start.md +0 -142
- package/src/canvas-bounds.ts +0 -52
- package/src/color.ts +0 -197
- package/src/compile/actor-ids.ts +0 -297
- package/src/compile/actor-option-notice.ts +0 -379
- package/src/compile/actors.ts +0 -103
- package/src/compile/c4.ts +0 -74
- package/src/compile/chain.ts +0 -93
- package/src/compile/chart-fields.ts +0 -193
- package/src/compile/deep-rewrite.ts +0 -30
- package/src/compile/derived-source.ts +0 -18
- package/src/compile/direction.ts +0 -102
- package/src/compile/edge-option-notice.ts +0 -112
- package/src/compile/er-relation.ts +0 -200
- package/src/compile/flow.ts +0 -54
- package/src/compile/flowchart.ts +0 -344
- package/src/compile/funnel.ts +0 -87
- package/src/compile/gantt.ts +0 -337
- package/src/compile/generic.ts +0 -696
- package/src/compile/hierarchy.ts +0 -92
- package/src/compile/journey.ts +0 -93
- package/src/compile/kinds.ts +0 -139
- package/src/compile/lanes.ts +0 -138
- package/src/compile/metro.ts +0 -271
- package/src/compile/mind.ts +0 -393
- package/src/compile/node-title.ts +0 -24
- package/src/compile/notice.ts +0 -141
- package/src/compile/order.ts +0 -57
- package/src/compile/parts-lookup.ts +0 -17
- package/src/compile/parts.ts +0 -2709
- package/src/compile/placement-apply.ts +0 -689
- package/src/compile/placement.ts +0 -15
- package/src/compile/quadrant.ts +0 -265
- package/src/compile/record.ts +0 -75
- package/src/compile/row-marks.ts +0 -128
- package/src/compile/rows.ts +0 -40
- package/src/compile/sequence.ts +0 -305
- package/src/compile/slug.ts +0 -37
- package/src/compile/stages.ts +0 -247
- package/src/compile/subtitle.ts +0 -35
- package/src/compile/swimlane-marks.ts +0 -87
- package/src/compile/swimlane.ts +0 -82
- package/src/compile/timeline.ts +0 -213
- package/src/compile/topology.ts +0 -44
- package/src/compile/tree.ts +0 -69
- package/src/compile/value-chart.ts +0 -337
- package/src/compile/values.ts +0 -432
- package/src/compile/word-state.ts +0 -82
- package/src/compile.ts +0 -3168
- package/src/focus.ts +0 -46
- package/src/input-size.ts +0 -181
- package/src/json-line-map.ts +0 -30
- package/src/json-parser.ts +0 -4306
- package/src/keywords.ts +0 -417
- package/src/near-name.ts +0 -42
- package/src/notation-lint.ts +0 -342
- package/src/relative-pos.ts +0 -251
- package/src/schemas/diagram.json +0 -3088
- package/src/types.ts +0 -1009
- package/src/v05/index.ts +0 -9
- package/src/v05/input-table.generated.ts +0 -138
- package/src/v05/parser-types.ts +0 -32
- package/src/v05/parser.ts +0 -6668
- package/src/v05/readout-table.generated.ts +0 -1088
- package/src/v05/removed-syntax.ts +0 -115
- package/src/value-syntax.ts +0 -313
- package/src/write-position.ts +0 -270
package/src/notation-lint.ts
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 記法の検査 (図を書く人向けの修正システム)。
|
|
3
|
-
*
|
|
4
|
-
* cdl / dragon の記法で書いた図 (`CdlDiagram`) を決まった規則で検査し、直し方を修正案
|
|
5
|
-
* (`suggestion`) として返す純粋関数。 言語モデルは使わない。
|
|
6
|
-
*
|
|
7
|
-
* 検知システム (`check:cdl` / `check:dragon` / `check:kind`) は開発者向けで実装の不具合を
|
|
8
|
-
* 見つける。 こちらは図を書く人向けで、読む人へ伝わらない書き方 (図の説明に入り込んだ実装の
|
|
9
|
-
* 書き方、無い部品を指す参照、値や項目が空の図表など) を見つける。
|
|
10
|
-
*
|
|
11
|
-
* 使い方:
|
|
12
|
-
* import { lintDiagram, autoFix } from "@cardenelabs/dragon";
|
|
13
|
-
* const report = lintDiagram(diagram);
|
|
14
|
-
* // report.issues = LintIssue[]
|
|
15
|
-
* // report.autoFixableCount = 自動修正で実際に解消できる指摘の数
|
|
16
|
-
* const patched = autoFix(diagram); // 自動修正を当てた新しい図 (元の図は変えない)
|
|
17
|
-
*
|
|
18
|
-
* 道具:
|
|
19
|
-
* node packages/dragon/scripts/dragon-lint.mjs apps/playground-spa/src/topics/catalog/presets.cdl.ts
|
|
20
|
-
*/
|
|
21
|
-
import type { CdlDiagram, CdlNode } from "@cardenelabs/cdl";
|
|
22
|
-
|
|
23
|
-
export type LintSeverity = "warn" | "info";
|
|
24
|
-
|
|
25
|
-
export type LintIssue = {
|
|
26
|
-
/** rule 識別子 */
|
|
27
|
-
rule: string;
|
|
28
|
-
severity: LintSeverity;
|
|
29
|
-
/** 該当対象 (node id / edge id / diagram id) */
|
|
30
|
-
target: string;
|
|
31
|
-
/**
|
|
32
|
-
* 書き手向けの指摘の文。 書き手が打ち込む識別子と書き手の値は `` ` `` で囲み、 残りは日本語で書く
|
|
33
|
-
* (画面側の `lint-message-words.test.ts` が全規則の文を判定に通す)
|
|
34
|
-
*/
|
|
35
|
-
message: string;
|
|
36
|
-
/** 修正案。 自動修正できる指摘は自動修正が書く値そのもの、 できない指摘は直し方の文 */
|
|
37
|
-
suggestion?: string;
|
|
38
|
-
/** autoFix() が本 issue を自動解消できるか */
|
|
39
|
-
autoFixable: boolean;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type LintReport = {
|
|
43
|
-
diagramId: string;
|
|
44
|
-
issues: LintIssue[];
|
|
45
|
-
/** autoFix() が実際に解消できる issue の数 */
|
|
46
|
-
autoFixableCount: number;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 図の説明に入り込んだ実装の書き方と、 読む人に何が伝わらないか。
|
|
51
|
-
*
|
|
52
|
-
* 文は理由だけを持ち、 直し方の例を書かない。 直し方は修正案 (`suggestion`) が自動修正の
|
|
53
|
-
* 出力から作る。 例を字で書くと、 自動修正の出力を変えた日に例だけが古い文型で残る
|
|
54
|
-
* (実測 = #1934 で出力を変えた後も `「〜 を示す図」 のように` が残っていた、 #1940)。
|
|
55
|
-
*/
|
|
56
|
-
const REDUNDANT_TOPIC_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [
|
|
57
|
-
{ pattern: /\bpreset\s*\(/i, hint: "`preset (…)` は作り手の書き方で、 読む人には何を示す図かが伝わらない" },
|
|
58
|
-
{ pattern: /render\s*未実装/, hint: "`render 未実装` は作り手向けの控えで、 カタログに出す説明には要らない" },
|
|
59
|
-
{
|
|
60
|
-
pattern: /SVG\s+(polyline|arc|rect|path)/i,
|
|
61
|
-
hint: "`SVG` の描き方の名前 (`polyline` / `arc` / `rect` / `path`) は実装の詳細で、 何を示す図かが伝わらない",
|
|
62
|
-
},
|
|
63
|
-
{ pattern: /\bpolygon\b/i, hint: "`polygon` は実装の言葉で、 何を示す図かが伝わらない" },
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 検査対象 diagram の全 rule を実行し LintReport を返す。
|
|
68
|
-
* 全 rule は純粋 (副作用なし / LLM 呼び出しなし)。
|
|
69
|
-
*/
|
|
70
|
-
export function lintDiagram(d: CdlDiagram): LintReport {
|
|
71
|
-
const issues: LintIssue[] = [];
|
|
72
|
-
|
|
73
|
-
issues.push(...ruleTopicRedundancy(d));
|
|
74
|
-
issues.push(...ruleEmptyChartData(d));
|
|
75
|
-
issues.push(...ruleGanttUnknownDependsOn(d));
|
|
76
|
-
issues.push(...ruleMindMapParentReference(d));
|
|
77
|
-
issues.push(...ruleTreeParentReference(d));
|
|
78
|
-
issues.push(...ruleQuadrantMissingItems(d));
|
|
79
|
-
issues.push(...ruleFunnelMonotonicCount(d));
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
diagramId: d.id,
|
|
83
|
-
issues,
|
|
84
|
-
autoFixableCount: issues.filter((i) => i.autoFixable).length,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* lintDiagram で detected な issue のうち autoFixable=true のものを機械的に適用して
|
|
90
|
-
* 修正済 CdlDiagram を返す。 手動修正必要な issue は残る (次回 lint 時に再検出)。
|
|
91
|
-
*/
|
|
92
|
-
export function autoFix(d: CdlDiagram): CdlDiagram {
|
|
93
|
-
const patched: CdlDiagram = {
|
|
94
|
-
...d,
|
|
95
|
-
topic: applyTopicAutoFix(d.topic),
|
|
96
|
-
nodes: d.nodes.map((n) => ({ ...n })),
|
|
97
|
-
};
|
|
98
|
-
return patched;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** 先頭に置かれる型の名前。 型の名前は実装の語なので、説明の頭に残っていたら落とす */
|
|
102
|
-
const 先頭の型の名前 =
|
|
103
|
-
/^\s*(line chart|pie chart|bar chart|chart|flow|swimlane|sequence|topology|er|stateMachine2?|infrastructure|classDiagram|tree|userJourney|mindMap|funnel|quadrant|gantt|flowchart|network)\b/i;
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 図の説明から実装の語を落とす。
|
|
107
|
-
*
|
|
108
|
-
* **落とすだけで、文は書かない** (#2687)。
|
|
109
|
-
*
|
|
110
|
-
* 以前は先頭が型の名前なら `${shows}を示す${name}` に丸ごと書き換えていた。
|
|
111
|
-
* 出てくるのは必ず図種の定義文 (「全体に対する内訳の割合を示す円グラフ」) で、
|
|
112
|
-
* その図が何を示しているかは 1 文字も入らない。
|
|
113
|
-
* cdl 0.96.0 で説明が図の題として画面に出るようになり、見本帳の 8 枚がその形で出ていた。
|
|
114
|
-
*
|
|
115
|
-
* 道具は図の中身を知らないので、名前を作れない。 落とした結果が説明として成り立たないなら
|
|
116
|
-
* **元の字をそのまま返す** = 呼出側が「自動修正で消えない」 と判定し、書き手に名前を求める。
|
|
117
|
-
* 置き換えの語 (かつての「図の説明」) を返すと、道具が書いた仮の題が画面に出る。
|
|
118
|
-
*/
|
|
119
|
-
function applyTopicAutoFix(topic: string): string {
|
|
120
|
-
let out = topic;
|
|
121
|
-
out = out.replace(/\s*\([^)]*(preset|render|SVG|polygon|polyline|arc|rect|path)[^)]*\)/gi, "");
|
|
122
|
-
out = out.replace(先頭の型の名前, "");
|
|
123
|
-
out = out.replace(/\b(preset|render)\b/gi, "");
|
|
124
|
-
// 落とした跡に残る区切りを畳む
|
|
125
|
-
out = out.replace(/^[\s\-—::・]+/, "").replace(/\s+/g, " ").trim();
|
|
126
|
-
// 括弧の中しか残らなかった形 (`(詳細)`) は説明として成り立たない。
|
|
127
|
-
// 字数だけで見ると 4 字あるので通ってしまう
|
|
128
|
-
const 括弧の外 = out.replace(/[((][^))]*[))]/g, "").trim();
|
|
129
|
-
if (out.length < 3 || 括弧の外.length < 3) return topic;
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* 図の説明に実装の書き方が入っていないかを見る。
|
|
135
|
-
*
|
|
136
|
-
* **自動修正で消えるかは、 直した後の説明にもう 1 度当てて決める** (#1940)。 自動修正が消すのは
|
|
137
|
-
* 先頭の型の名前と、 括弧の中の実装の言葉だけで、 括弧の外の `SVG polyline を使う` は字が変わらない。
|
|
138
|
-
* 当てずに「直せる」 と返すと、 道具が直せる数に入れ、 直したはずの指摘が次の検査でまた出る。
|
|
139
|
-
*
|
|
140
|
-
* 修正案は、 直せる時は自動修正が書く値そのもの (`LintIssue.suggestion` の約束)、 直せない時は
|
|
141
|
-
* 直し方の文にする。 文の例は書き換え先の表から作り、 字で書かない。
|
|
142
|
-
*/
|
|
143
|
-
function ruleTopicRedundancy(d: CdlDiagram): LintIssue[] {
|
|
144
|
-
const 当たる = REDUNDANT_TOPIC_PATTERNS.filter(({ pattern }) => pattern.test(d.topic));
|
|
145
|
-
if (当たる.length === 0) return [];
|
|
146
|
-
const 直した後 = applyTopicAutoFix(d.topic);
|
|
147
|
-
const 直せる =
|
|
148
|
-
直した後 !== d.topic &&
|
|
149
|
-
!REDUNDANT_TOPIC_PATTERNS.some(({ pattern }) => pattern.test(直した後));
|
|
150
|
-
// 例は見本帳の実物から採る (#2687)。 型の表から作ると図種の定義文になり、
|
|
151
|
-
// 案内どおりに書いた説明がそのまま図の題として画面に出る
|
|
152
|
-
const 手で直す = "その図が何を示しているかを名前で書く (「経路別の流入」 のように)";
|
|
153
|
-
return 当たる.map(({ hint }) => ({
|
|
154
|
-
rule: "topic-redundant-implementation-detail",
|
|
155
|
-
severity: "warn",
|
|
156
|
-
target: d.id,
|
|
157
|
-
message: `図の説明 \`${d.topic}\` に実装の書き方が入っている。 ${hint}`,
|
|
158
|
-
suggestion: 直せる ? 直した後 : 手で直す,
|
|
159
|
-
autoFixable: 直せる,
|
|
160
|
-
}));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function ruleEmptyChartData(d: CdlDiagram): LintIssue[] {
|
|
164
|
-
const out: LintIssue[] = [];
|
|
165
|
-
for (const n of d.nodes) {
|
|
166
|
-
if (n.kind === "chart-line" || n.kind === "chart-pie" || n.kind === "chart-bar") {
|
|
167
|
-
const data = n.chartData ?? [];
|
|
168
|
-
if (data.length === 0) {
|
|
169
|
-
out.push({
|
|
170
|
-
rule: "chart-empty-datum",
|
|
171
|
-
severity: "warn",
|
|
172
|
-
target: n.id,
|
|
173
|
-
message: `図表 \`${n.id}\` に値 (\`datum\`) が 1 件も無く、 図表が描かれない`,
|
|
174
|
-
suggestion: `\`.datum({ id, label, value })\` で値を 1 件以上足す`,
|
|
175
|
-
autoFixable: false,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (data.length === 1) {
|
|
179
|
-
out.push({
|
|
180
|
-
rule: "chart-single-datum",
|
|
181
|
-
severity: "info",
|
|
182
|
-
target: n.id,
|
|
183
|
-
message: `図表 \`${n.id}\` の値 (\`datum\`) が 1 件だけで、 比べることも移り変わりを追うこともできない`,
|
|
184
|
-
suggestion: `値を 2 件以上にする (折れ線グラフは 3 件以上で傾きが読める)`,
|
|
185
|
-
autoFixable: false,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return out;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function ruleGanttUnknownDependsOn(d: CdlDiagram): LintIssue[] {
|
|
194
|
-
const out: LintIssue[] = [];
|
|
195
|
-
for (const n of d.nodes) {
|
|
196
|
-
if (n.kind === "gantt-timeline") {
|
|
197
|
-
const tasks = n.ganttData ?? [];
|
|
198
|
-
const ids = new Set(tasks.map((t) => t.id));
|
|
199
|
-
for (const t of tasks) {
|
|
200
|
-
const dependencies = Array.isArray(t.dependsOn) ? t.dependsOn : t.dependsOn ? [t.dependsOn] : [];
|
|
201
|
-
for (const dependency of dependencies) {
|
|
202
|
-
if (!ids.has(dependency)) {
|
|
203
|
-
out.push({
|
|
204
|
-
rule: "gantt-unknown-depends-on",
|
|
205
|
-
severity: "warn",
|
|
206
|
-
target: t.id,
|
|
207
|
-
message: `作業 \`${t.id}\` の \`dependsOn\` が、 無い作業 \`${dependency}\` を指している`,
|
|
208
|
-
suggestion: `\`dependsOn\` を既にある作業の \`id\` に直すか、 \`dependsOn\` を外す`,
|
|
209
|
-
autoFixable: false,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* 枝の親が実在するかを見る。
|
|
221
|
-
*
|
|
222
|
-
* **記法からも届く** (`#1177`)。 一時期は組立て API (`mindMap()` builder) で組んだ図にしか
|
|
223
|
-
* 当たらなかった = 記法の `type: mind` が `card` を 3 列に並べる別実装で、 `mind-map` 種別を
|
|
224
|
-
* 作っていなかったため (`#1170` で `type: radial` が消えて唯一の作り手が無くなった)。
|
|
225
|
-
* `#1177` で `compileMind` を `mind-map` 種別に寄せたので、 前提が揃うようになった。
|
|
226
|
-
*
|
|
227
|
-
* ただし **記法から違反が出ることは無い**。 記法の `actors` は「1 つ目が中心、 残りが枝」 の
|
|
228
|
-
* 並びで親を書く場所が無く、 全ての枝が中心の直下 (必ず実在する) になるため。 違反が出るのは
|
|
229
|
-
* 組立て API で親を書き間違えた図になる。
|
|
230
|
-
*/
|
|
231
|
-
function ruleMindMapParentReference(d: CdlDiagram): LintIssue[] {
|
|
232
|
-
const out: LintIssue[] = [];
|
|
233
|
-
for (const n of d.nodes) {
|
|
234
|
-
if (n.kind === "mind-map" && n.mindData) {
|
|
235
|
-
const known = new Set<string>([n.mindData.rootId]);
|
|
236
|
-
for (const b of n.mindData.branches) known.add(b.id);
|
|
237
|
-
for (const b of n.mindData.branches) {
|
|
238
|
-
if (!known.has(b.parent)) {
|
|
239
|
-
out.push({
|
|
240
|
-
rule: "mindmap-unknown-parent",
|
|
241
|
-
severity: "warn",
|
|
242
|
-
target: b.id,
|
|
243
|
-
message: `枝 \`${b.id}\` の \`parent\` が、 無い枝 \`${b.parent}\` を指している`,
|
|
244
|
-
suggestion: `\`parent\` を中心 (\`${n.mindData.rootId}\`) か、 既にある枝の \`id\` に直す`,
|
|
245
|
-
autoFixable: false,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return out;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function ruleTreeParentReference(d: CdlDiagram): LintIssue[] {
|
|
255
|
-
const out: LintIssue[] = [];
|
|
256
|
-
for (const n of d.nodes) {
|
|
257
|
-
if (n.kind === "tree-hierarchy" && n.treeData) {
|
|
258
|
-
const ids = new Set(n.treeData.map((t) => t.id));
|
|
259
|
-
for (const t of n.treeData) {
|
|
260
|
-
if (t.parent && !ids.has(t.parent)) {
|
|
261
|
-
out.push({
|
|
262
|
-
rule: "tree-unknown-parent",
|
|
263
|
-
severity: "warn",
|
|
264
|
-
target: t.id,
|
|
265
|
-
message: `階層図の項目 \`${t.id}\` の \`parent\` が、 無い項目 \`${t.parent}\` を指している`,
|
|
266
|
-
suggestion: `\`parent\` を既にある項目の \`id\` に直すか、 \`parent\` を外して一番上の項目にする`,
|
|
267
|
-
autoFixable: false,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return out;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function ruleQuadrantMissingItems(d: CdlDiagram): LintIssue[] {
|
|
277
|
-
const out: LintIssue[] = [];
|
|
278
|
-
for (const n of d.nodes) {
|
|
279
|
-
if (n.kind === "quadrant-matrix" && n.quadrantData) {
|
|
280
|
-
const items = n.quadrantData.items;
|
|
281
|
-
if (items.length === 0) {
|
|
282
|
-
out.push({
|
|
283
|
-
rule: "quadrant-empty",
|
|
284
|
-
severity: "warn",
|
|
285
|
-
target: n.id,
|
|
286
|
-
message: `マトリクス図 \`${n.id}\` に項目 (\`item\`) が 1 件も無く、 軸だけが描かれる`,
|
|
287
|
-
suggestion: `\`.item({ id, title, quadrant })\` で項目を 1 件以上足す (\`quadrant\` は \`topLeft\` などの 4 区画から選ぶ)`,
|
|
288
|
-
autoFixable: false,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
const bySlot = new Set(items.map((it) => it.quadrant));
|
|
292
|
-
if (items.length >= 4 && bySlot.size === 1) {
|
|
293
|
-
out.push({
|
|
294
|
-
rule: "quadrant-single-quadrant",
|
|
295
|
-
severity: "info",
|
|
296
|
-
target: n.id,
|
|
297
|
-
message: `マトリクス図 \`${n.id}\` の項目がすべて 1 つの区画に集まり、 4 つに分けた意味が薄い`,
|
|
298
|
-
suggestion: `項目を 2 つ以上の区画に分ける。 強みと弱みを並べる分析 (\`SWOT\`) や優先度を決める図は、 4 つの区画に散らして使う`,
|
|
299
|
-
autoFixable: false,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
return out;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* 段階の人数が減っていくことを見る。
|
|
309
|
-
*
|
|
310
|
-
* 人数の欄は `{名前}` を書ける (状態から取る形、 cdl の `render/payload-binding.ts` が解く)。
|
|
311
|
-
* その場合ここでは値が決まらないので、**数どうしの組だけを比べる** (#1194)。
|
|
312
|
-
*
|
|
313
|
-
* 素通しで比べると文字列の大小比較になり、`{trial}` が `{signup}` より大きいという理由で
|
|
314
|
-
* 発火する。 実際にカタログの funnel を状態から取る形にした時、その偽発火が出た。
|
|
315
|
-
*/
|
|
316
|
-
function ruleFunnelMonotonicCount(d: CdlDiagram): LintIssue[] {
|
|
317
|
-
const out: LintIssue[] = [];
|
|
318
|
-
for (const n of d.nodes) {
|
|
319
|
-
if (n.kind === "funnel-stages" && n.funnelData) {
|
|
320
|
-
const stages = n.funnelData;
|
|
321
|
-
for (let i = 1; i < stages.length; i++) {
|
|
322
|
-
const 今 = stages[i]!.count;
|
|
323
|
-
const 前 = stages[i - 1]!.count;
|
|
324
|
-
if (typeof 今 !== "number" || typeof 前 !== "number") continue;
|
|
325
|
-
if (今 > 前) {
|
|
326
|
-
out.push({
|
|
327
|
-
rule: "funnel-increasing-count",
|
|
328
|
-
severity: "warn",
|
|
329
|
-
target: stages[i]!.id,
|
|
330
|
-
message: `段階 \`${stages[i]!.id}\` の数 (${今}) が前の段階 (${前}) より多い。 ファネル図は段階が進むほど数が減る`,
|
|
331
|
-
suggestion: `段階の順番を確かめる。 数が増える流れを示すなら、 折れ線グラフ (\`chart-line\`) などの別の図にする`,
|
|
332
|
-
autoFixable: false,
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return out;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// dev-only import bridging (unused var lint prevention)
|
|
342
|
-
export type { CdlDiagram, CdlNode };
|
package/src/relative-pos.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 位置を他の要素からの相対で書くための解決。
|
|
3
|
-
*
|
|
4
|
-
* `位置: Web の右 200` のように、 座標の代わりに「誰の」「どちら側に」「どれだけ離して」 を書く。
|
|
5
|
-
* 書く人も LLM も座標を知らないので、 数値を当てさせない形を用意する。
|
|
6
|
-
*
|
|
7
|
-
* 解決結果は絶対座標 (`posX` / `posY`) で、 座標を直接書いた時と同じ経路を通る。
|
|
8
|
-
* 書き方が増えるだけで、 効き方は変わらない。
|
|
9
|
-
*
|
|
10
|
-
* 座標を決める場所が組み立て側 (`compile.ts`) と画面側 (playground の `overlay-dsl.ts`) の
|
|
11
|
-
* 2 つあるため、 解決の規則は本 file に 1 つだけ置いて両方から呼ぶ。 片方だけ直すと画面が
|
|
12
|
-
* 直らない事故を、 規則を共有することで構造的に防ぐ。
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 基準からどちら側に置くか。
|
|
17
|
-
*
|
|
18
|
-
* 語の一覧は下の `RELATIVE_DIRECTIONS` が持つ。 型を並べ直さず、一覧から導く。
|
|
19
|
-
*/
|
|
20
|
-
export type RelativeDirection = (typeof RELATIVE_DIRECTIONS)[number];
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* 向きとして受ける語の一覧。
|
|
24
|
-
*
|
|
25
|
-
* 読み取りの入口が 2 つある (本文の `位置: Web の右 200` と JSON の `posRel.dir`) ため、
|
|
26
|
-
* 一覧を 1 箇所に置いて両方から引く。 2 度書くと、向きを足した時に片方だけが取り残される。
|
|
27
|
-
*/
|
|
28
|
-
export const RELATIVE_DIRECTIONS = ["right", "left", "above", "below"] as const;
|
|
29
|
-
|
|
30
|
-
/** 相対で書かれた位置の指定。 */
|
|
31
|
-
export type RelativePos = {
|
|
32
|
-
/** 基準にする相手の名前。 `actors:` に書かれた名前をそのまま持つ。 */
|
|
33
|
-
anchor: string;
|
|
34
|
-
dir: RelativeDirection;
|
|
35
|
-
/** 相手との間隔。 書かなかった時は `RELATIVE_GAP_DEFAULT`。 */
|
|
36
|
-
gap?: number;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** 位置を決めるのに必要な、 基準の中心と大きさ。 */
|
|
40
|
-
export type AnchorBox = { cx: number; cy: number; w: number; h: number };
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 間隔を書かなかった時の既定値。
|
|
44
|
-
*
|
|
45
|
-
* 8 図種 × 4 向きで間隔を変えながら、 cdl が出す近すぎ系の指摘の数を数えて決めた。
|
|
46
|
-
* 100 で 17 件、 130 で 10 件、 140 で 4 件と減り、 160 で 2 件に落ちて以降は変わらない。
|
|
47
|
-
* 残る 2 件は間隔と無関係な指摘なので、 減らなくなる 160 を既定にする。
|
|
48
|
-
*
|
|
49
|
-
* 自動配置が空ける隙間も実測では縦 100 / 横 256-406 で、 160 はその間に収まる。
|
|
50
|
-
* 隣に置いたと読める近さと、 詰まって見えない広さの両方を満たす。
|
|
51
|
-
*/
|
|
52
|
-
export const RELATIVE_GAP_DEFAULT = 160;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 書かれた向きの語。 日本語と英語の両方を受け付ける。
|
|
56
|
-
*
|
|
57
|
-
* 項目名が日本語でも英語でもよい (`種類` / `kind`) のと揃える。
|
|
58
|
-
*/
|
|
59
|
-
const DIRECTION_WORDS: Readonly<Record<string, RelativeDirection>> = {
|
|
60
|
-
右: "right",
|
|
61
|
-
左: "left",
|
|
62
|
-
上: "above",
|
|
63
|
-
下: "below",
|
|
64
|
-
right: "right",
|
|
65
|
-
left: "left",
|
|
66
|
-
above: "above",
|
|
67
|
-
below: "below",
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 間隔として受け付ける数の形。
|
|
72
|
-
*
|
|
73
|
-
* 負の数は含めない (向きが裏返るため)。 小数と指数表記は受け付ける。 整数だけに絞ると、
|
|
74
|
-
* 有効な数を書いたのに「書き方が読めません」 と返す (実測 = `1e2` / `.5` が弾かれた)。
|
|
75
|
-
*/
|
|
76
|
-
const GAP_NUM = String.raw`(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?`;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 日本語の形。 `Web の右` / `Web の右 200` / `Web の右200`。
|
|
80
|
-
*
|
|
81
|
-
* 相手の名前は控えめ (`.+?`) に取る。 名前自体が `の右` で終わる場合 (`Aの右 の左`) でも、
|
|
82
|
-
* 後戻りして末尾の向きを先に確定するため取り違えない。
|
|
83
|
-
*/
|
|
84
|
-
const RE_JA = new RegExp(String.raw`^(.+?)\s*の\s*(右|左|上|下)(?:\s*(${GAP_NUM}))?$`);
|
|
85
|
-
|
|
86
|
-
/** 英語の形。 `Web right` / `Web right 200`。 向きの前後は空白で区切る。 */
|
|
87
|
-
const RE_EN = new RegExp(String.raw`^(.+?)\s+(right|left|above|below)(?:\s+(${GAP_NUM}))?$`, "i");
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* `位置:` に書かれた値を相対指定として読む。 相対の形でなければ null。
|
|
91
|
-
*
|
|
92
|
-
* 座標の形 (`300,200`) は呼ぶ側が先に判定する。 ここは相対だけを見る。
|
|
93
|
-
*/
|
|
94
|
-
export function parseRelativePos(raw: string): RelativePos | null {
|
|
95
|
-
const s = raw.trim();
|
|
96
|
-
if (s === "") return null;
|
|
97
|
-
const m = s.match(RE_JA) ?? s.match(RE_EN);
|
|
98
|
-
if (!m) return null;
|
|
99
|
-
const anchor = m[1]!.trim();
|
|
100
|
-
if (anchor === "") return null;
|
|
101
|
-
const dir = DIRECTION_WORDS[m[2]!.toLowerCase()];
|
|
102
|
-
if (dir === undefined) return null;
|
|
103
|
-
const gapRaw = m[3];
|
|
104
|
-
if (gapRaw === undefined) return { anchor, dir };
|
|
105
|
-
const gap = Number(gapRaw);
|
|
106
|
-
// 数として読めない間隔は書かなかった扱いにする。 既定の間隔で置く方が、
|
|
107
|
-
// 図から消えるより書いた人が気付きやすい。
|
|
108
|
-
return Number.isFinite(gap) ? { anchor, dir, gap } : { anchor, dir };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 相対指定を絶対座標に直す。
|
|
113
|
-
*
|
|
114
|
-
* `posX` / `posY` は箱の中心。 間隔は箱の縁と縁の間の距離として扱う。 中心間の距離にすると、
|
|
115
|
-
* 大きさの違う箱を並べた時に見た目の隙間が揃わない。
|
|
116
|
-
*/
|
|
117
|
-
export function resolveRelativePos(
|
|
118
|
-
rel: RelativePos,
|
|
119
|
-
anchor: AnchorBox,
|
|
120
|
-
target: { w: number; h: number },
|
|
121
|
-
): { posX: number; posY: number } {
|
|
122
|
-
// 負の間隔は向きを裏返す。 `Web の右 -1000` が Web の左に置かれ、 確かめる側も同じ値で
|
|
123
|
-
// 期待を作るので矛盾に気付けない (実測 = 右と書いて左に出た)。 間隔は 0 以上として扱う。
|
|
124
|
-
// 記法から来る値は parser が弾くが、 本関数は公開しているので入口で閉じる
|
|
125
|
-
const raw = rel.gap ?? RELATIVE_GAP_DEFAULT;
|
|
126
|
-
const gap = Number.isFinite(raw) && raw >= 0 ? raw : RELATIVE_GAP_DEFAULT;
|
|
127
|
-
const dx = anchor.w / 2 + gap + target.w / 2;
|
|
128
|
-
const dy = anchor.h / 2 + gap + target.h / 2;
|
|
129
|
-
switch (rel.dir) {
|
|
130
|
-
case "right":
|
|
131
|
-
return { posX: anchor.cx + dx, posY: anchor.cy };
|
|
132
|
-
case "left":
|
|
133
|
-
return { posX: anchor.cx - dx, posY: anchor.cy };
|
|
134
|
-
case "below":
|
|
135
|
-
return { posX: anchor.cx, posY: anchor.cy + dy };
|
|
136
|
-
case "above":
|
|
137
|
-
return { posX: anchor.cx, posY: anchor.cy - dy };
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 相対指定を解く順番を決める。
|
|
143
|
-
*
|
|
144
|
-
* 基準にした相手がまた相対で書かれていることがある (`B は A の右`、 `C は B の右`)。
|
|
145
|
-
* 先に相手が決まっていないと座標を出せないので、 依存の浅い順に並べ替える。
|
|
146
|
-
*
|
|
147
|
-
* 輪になっている分 (`A は B の右`、 `B は A の右`) は解けないため、 順番からは外して
|
|
148
|
-
* 名前だけを返す。 呼ぶ側が誤りとして扱う。
|
|
149
|
-
*/
|
|
150
|
-
export function orderByDependency(
|
|
151
|
-
items: ReadonlyArray<{ name: string; rel?: RelativePos }>,
|
|
152
|
-
): { order: string[]; cyclic: string[] } {
|
|
153
|
-
const relOf = new Map<string, RelativePos>();
|
|
154
|
-
const known = new Set<string>();
|
|
155
|
-
for (const it of items) {
|
|
156
|
-
known.add(it.name);
|
|
157
|
-
if (it.rel) relOf.set(it.name, it.rel);
|
|
158
|
-
}
|
|
159
|
-
const order: string[] = [];
|
|
160
|
-
const done = new Set<string>();
|
|
161
|
-
const cyclic = new Set<string>();
|
|
162
|
-
|
|
163
|
-
// 再帰にすると基準の連鎖の長さだけ stack を積む。 連鎖の長さは書く人が決めるので
|
|
164
|
-
// 上限を置けない。 明示的な配列で辿る。
|
|
165
|
-
for (const it of items) {
|
|
166
|
-
if (done.has(it.name) || cyclic.has(it.name)) continue;
|
|
167
|
-
// path は [自分, 基準, 基準の基準, ...] の順に伸びる
|
|
168
|
-
const path: string[] = [];
|
|
169
|
-
const onPath = new Set<string>();
|
|
170
|
-
let cur: string | undefined = it.name;
|
|
171
|
-
while (cur !== undefined) {
|
|
172
|
-
if (done.has(cur) || cyclic.has(cur)) break;
|
|
173
|
-
if (onPath.has(cur)) {
|
|
174
|
-
// 輪を見つけた。 輪に含まれる分だけを外す。 輪に入る手前の分は解けるので残す
|
|
175
|
-
for (const n of path.slice(path.indexOf(cur))) cyclic.add(n);
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
path.push(cur);
|
|
179
|
-
onPath.add(cur);
|
|
180
|
-
const rel = relOf.get(cur);
|
|
181
|
-
// 相対で書かれていない、 または相手が居ない = ここで辿り終わり
|
|
182
|
-
if (rel === undefined || !known.has(rel.anchor)) break;
|
|
183
|
-
cur = rel.anchor;
|
|
184
|
-
}
|
|
185
|
-
// 奥 (基準側) から順に並べる。 基準が先に決まっていないと座標を出せない
|
|
186
|
-
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
187
|
-
const n = path[i]!;
|
|
188
|
-
if (cyclic.has(n) || done.has(n)) continue;
|
|
189
|
-
done.add(n);
|
|
190
|
-
order.push(n);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return { order, cyclic: [...cyclic] };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** 解けない書き方の種類。 誤りの文は入口ごとに作るので、ここでは種類だけを返す */
|
|
197
|
-
export type RelativeProblemKind = "self" | "missing-anchor" | "cyclic";
|
|
198
|
-
|
|
199
|
-
/** 解けない書き方 1 件。 `anchor` は相手が居ない時だけ持つ */
|
|
200
|
-
export type RelativeProblem = {
|
|
201
|
-
name: string;
|
|
202
|
-
kind: RelativeProblemKind;
|
|
203
|
-
anchor?: string;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 解けない相対の指定を挙げる (#2039)。
|
|
208
|
-
*
|
|
209
|
-
* 解けない書き方は 3 通りある。 相手が居ない / 自分を基準にした / 基準が輪になっている。
|
|
210
|
-
* どれも「書いたのに図が変わらない」 形で表に出るため、図を出す前に知らせる。
|
|
211
|
-
*
|
|
212
|
-
* **判定は本 file に 1 つだけ置く**。 読み取りの入口が 2 つある (本文と JSON) ため、
|
|
213
|
-
* 片方だけに判定を持つと、同じ書き方が入口によって通ったり通らなかったりする。
|
|
214
|
-
* 実際 JSON 側には判定が無く、相対で置く欄そのものが無かった。
|
|
215
|
-
*
|
|
216
|
-
* 誤りの文は返さない。 本文側は行番号を、JSON 側は欄の場所を添えるため、文の組み立ては
|
|
217
|
-
* 入口に任せる。 ここが返すのは「誰の」「どの種類の」 解けなさか だけ。
|
|
218
|
-
*
|
|
219
|
-
* 呼ぶ側は、挙がった名前から相対の指定を外す。 残したままだと、誤りを無視して読み込んだ
|
|
220
|
-
* 経路で解決できない指定が組み立てまで届く。
|
|
221
|
-
*/
|
|
222
|
-
export function findRelativeProblems(
|
|
223
|
-
items: ReadonlyArray<{ name: string; rel?: RelativePos }>,
|
|
224
|
-
): RelativeProblem[] {
|
|
225
|
-
const named = new Set(items.map((i) => i.name));
|
|
226
|
-
const broken = new Set<string>();
|
|
227
|
-
const out: RelativeProblem[] = [];
|
|
228
|
-
|
|
229
|
-
for (const it of items) {
|
|
230
|
-
const rel = it.rel;
|
|
231
|
-
if (!rel) continue;
|
|
232
|
-
if (rel.anchor === it.name) {
|
|
233
|
-
out.push({ name: it.name, kind: "self" });
|
|
234
|
-
broken.add(it.name);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (!named.has(rel.anchor)) {
|
|
238
|
-
out.push({ name: it.name, kind: "missing-anchor", anchor: rel.anchor });
|
|
239
|
-
broken.add(it.name);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 輪は、既に解けないと判った分を外してから探す。 外さないと「相手が居ない」 1 件が
|
|
244
|
-
// 「輪になっている」 としても挙がり、同じ 1 つの誤りが 2 通りの直し方で出る
|
|
245
|
-
const { cyclic } = orderByDependency(
|
|
246
|
-
items.map((i) => ({ name: i.name, rel: broken.has(i.name) ? undefined : i.rel })),
|
|
247
|
-
);
|
|
248
|
-
for (const name of cyclic) out.push({ name, kind: "cyclic" });
|
|
249
|
-
|
|
250
|
-
return out;
|
|
251
|
-
}
|