@cardenelabs/dragon 0.15.0 → 0.17.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/src/v05/parser.ts CHANGED
@@ -41,8 +41,9 @@
41
41
  * 出力は v0.4 と同じ DslDocument。 既存 compile.ts で CdlDiagram に変換できる。
42
42
  */
43
43
 
44
- import type { NodeKind, Tone, EdgeStyle } from "@cardenelabs/cdl";
45
- import { TONES, NODE_KINDS, parseFormula } from "@cardenelabs/cdl";
44
+ import type { NodeKind, Tone, EdgeStyle, EdgeHead, EdgeHeadFill, ClassRelationType, SequenceMessageKind } from "@cardenelabs/cdl";
45
+ import { TONES, NODE_KINDS, EDGE_HEADS, EDGE_HEAD_FILLS, EDGE_STYLES, EDGE_REVEALS, CLASS_RELATION_LOOK, SEQUENCE_MESSAGE_LOOK, parseFormula } from "@cardenelabs/cdl";
46
+ import type { EdgeReveal } from "@cardenelabs/cdl";
46
47
  import { TONE_ALIAS, NODE_KIND_ALIAS } from "../keywords";
47
48
  import { parseRelativePos, orderByDependency } from "../relative-pos";
48
49
  import {
@@ -74,6 +75,7 @@ import type {
74
75
  DslError,
75
76
  PresetType,
76
77
  DslLane,
78
+ DslBand,
77
79
  DslGroup,
78
80
  DslViewport,
79
81
  } from "../types";
@@ -83,6 +85,26 @@ export type V05ParseResult = { ok: true; doc: DslDocument } | { ok: false; error
83
85
  /** 矢印を出す辺。 Text DSL / JSON validator / 公開 schema の 3 経路で同じ 4 値を使う。 */
84
86
  export const EDGE_SIDE_VALUES = ["top", "right", "bottom", "left"] as const;
85
87
 
88
+ /**
89
+ * 矢印の先の形として書ける語 (#1462)。
90
+ *
91
+ * **描画側から導く**。 手で並べると、描画側が形を増やした時に書けないままになる。
92
+ */
93
+ export const EDGE_HEAD_VALUES: readonly string[] = EDGE_HEADS;
94
+
95
+ /** 端の印の塗り方 (#1466)。 描画側の一覧から導く = 描画側が増やせば書ける */
96
+ export const EDGE_HEAD_FILL_VALUES: readonly string[] = EDGE_HEAD_FILLS;
97
+
98
+ /**
99
+ * クラス図の関係の種類 (#1466)。 描画側の表 (`CLASS_RELATION_LOOK`) の key から導く。
100
+ *
101
+ * 手で並べると、描画側が種類を足した時にここだけ取り残されて書けないままになる。
102
+ */
103
+ export const CLASS_RELATION_VALUES: readonly string[] = Object.keys(CLASS_RELATION_LOOK);
104
+
105
+ /** 順序図の言づての種類 (#1466)。 描画側の表から導く */
106
+ export const SEQ_MESSAGE_VALUES: readonly string[] = Object.keys(SEQUENCE_MESSAGE_LOOK);
107
+
86
108
  /**
87
109
  * 記法が受ける top-level の項目 (#1190)。
88
110
  *
@@ -115,6 +137,10 @@ export const TOP_LEVEL_KEYS = [
115
137
  "events",
116
138
  // 巻き上げに応じて進む値 (#1393)
117
139
  "scrolls",
140
+ // 動いている間の帯 (#1466)。 順序図だけが読む
141
+ "bands",
142
+ // 矢印をいつ出すか (#1470)
143
+ "reveal",
118
144
  ] as const;
119
145
 
120
146
  /**
@@ -249,6 +275,11 @@ export const PRESET_TYPES: ReadonlySet<PresetType> = new Set([
249
275
  "pie",
250
276
  "bar",
251
277
  "line",
278
+ "gauge",
279
+ "radial",
280
+ "stat",
281
+ "waffle",
282
+ "stacked",
252
283
  "funnel",
253
284
  "tree",
254
285
  "journey",
@@ -330,11 +361,14 @@ export const NODE_KIND_VALID: ReadonlySet<string> = new Set<string>([
330
361
  const TONE_VALID: ReadonlySet<string> = new Set<string>(TONES);
331
362
 
332
363
  /**
333
- * 受理する線種。 `EdgeStyle` は型だけで実体を持たないため、 実行時の一覧はここが唯一の出どころ。
364
+ * 受理する線種。 **描画側の一覧から導く** (#1466)。
365
+ *
366
+ * 以前は手で並べており、描画側が `dashed` を足した時にここだけ取り残されて「線種が
367
+ * 読めません」 で落ちた (実測)。 色名 (`TONE_VALID`) と同じく描画側を出どころにする。
334
368
  *
335
- * JSON 経路も同じ集合を読む (#1304)。 別に持つと、 線種が増えた時に片方だけ取り残される。
369
+ * JSON 経路も同じ集合を読む (#1304)。 別に持つと、線種が増えた時に片方だけ取り残される。
336
370
  */
337
- export const STYLE_VALID: ReadonlySet<string> = new Set<string>(["solid", "dotted-flow"]);
371
+ export const STYLE_VALID: ReadonlySet<string> = new Set<string>(EDGE_STYLES);
338
372
 
339
373
  /**
340
374
  * 色の名前として書ける語の一覧 (#1304)。 知らせの `hint` に出す。
@@ -391,6 +425,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
391
425
  let type: PresetType | null = null;
392
426
  let eyebrow: string | null = null;
393
427
  let eyebrowLine = 0;
428
+ let reveal: EdgeReveal | null = null;
394
429
  let axes: DslAxes | undefined = undefined;
395
430
  let axesLine = 0;
396
431
  let actors: DslActor[] = [];
@@ -399,6 +434,8 @@ export function parseTextDslV05(src: string): V05ParseResult {
399
434
  const values: DslValue[] = [];
400
435
  let viewport: DslViewport | undefined = undefined;
401
436
  let lanesMap: Record<string, DslLane> | undefined = undefined;
437
+ // 動いている間の帯 (#1466)。 順序図だけが読む
438
+ let bands: DslBand[] | undefined = undefined;
402
439
  let readoutsList: DslReadout[] | undefined = undefined;
403
440
  let inputsList: DslInput[] | undefined = undefined;
404
441
  let formulasList: DslFormula[] | undefined = undefined;
@@ -432,6 +469,29 @@ export function parseTextDslV05(src: string): V05ParseResult {
432
469
  i += 1;
433
470
  continue;
434
471
  }
472
+ if (head.key === "reveal") {
473
+ /*
474
+ * 矢印をいつ出すか (#1470)。
475
+ *
476
+ * 既定 (`phase`) は「段が名指しする矢印は、その段が来るまで描かない」。 段を進めるごとに
477
+ * 関係が増える図で、全ての矢印が 1 段目から見えていたことへの対処 (`cdl#582`)。
478
+ * `all` と書くと段に関わらず最初から全部描く。
479
+ */
480
+ const v = (head.value ?? "").trim();
481
+ if (v.length > 0) {
482
+ if ((EDGE_REVEALS as readonly string[]).includes(v)) {
483
+ reveal = v as EdgeReveal;
484
+ } else {
485
+ errors.push({
486
+ line: line.no,
487
+ message: `reveal が読めません (書いた値: ${v})`,
488
+ hint: `使える語 = ${EDGE_REVEALS.join(" / ")}`,
489
+ });
490
+ }
491
+ }
492
+ i += 1;
493
+ continue;
494
+ }
435
495
  if (head.key === "eyebrow") {
436
496
  // 空で書いた形 (`eyebrow:`) は「書かなかった」 と同じにする。 空文字を残すと
437
497
  // 描画側が中身のない帯を出す
@@ -640,6 +700,31 @@ export function parseTextDslV05(src: string): V05ParseResult {
640
700
  i = next;
641
701
  continue;
642
702
  }
703
+ if (head.key === "bands") {
704
+ /*
705
+ * 動いている間の帯 (#1466)。 `- DB: 1..2` の形で、段の番号の区間を書く。
706
+ *
707
+ * 面ごとに 2 行以上書ける = 途中で手が空く面はそこで切れる。 書かない図は
708
+ * 組み立て器が「最初に関わった段から最後まで」 の 1 本にする。
709
+ */
710
+ const { items, next } = collectIndentedList(lines, i + 1, line.indent);
711
+ bands = [];
712
+ for (const it of items) {
713
+ // 一覧の読み手が先頭の `- ` を落とす形と落とさない形の両方を受ける
714
+ const m = it.trimmed.match(/^(?:-\s*)?(.+?)\s*:\s*(\d+)\s*\.\.\s*(\d+)\s*$/);
715
+ if (!m) {
716
+ errors.push({
717
+ line: it.no,
718
+ message: `帯の書き方が読めません: "${it.trimmed}"`,
719
+ hint: "use `- DB: 1..2` (面の名前と、段の番号の区間)",
720
+ });
721
+ continue;
722
+ }
723
+ bands.push({ actor: m[1]!, from: Number(m[2]), to: Number(m[3]) });
724
+ }
725
+ i = next;
726
+ continue;
727
+ }
643
728
  if (head.key === "lanes") {
644
729
  // lanes:\n l1: { x: 0, width: 320, label: "..." }\n l2: { ... }
645
730
  const { items, next } = collectIndentedList(lines, i + 1, line.indent);
@@ -880,6 +965,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
880
965
  title: title!,
881
966
  type: type!,
882
967
  ...(eyebrow !== null ? { eyebrow, eyebrowPos: { line: eyebrowLine } } : {}),
968
+ ...(reveal !== null ? { reveal } : {}),
883
969
  ...(axes !== undefined ? { axes, axesPos: { line: axesLine } } : {}),
884
970
  actors,
885
971
  flow,
@@ -887,6 +973,7 @@ export function parseTextDslV05(src: string): V05ParseResult {
887
973
  ...(values.length > 0 ? { values } : {}),
888
974
  viewport,
889
975
  lanes: lanesMap,
976
+ ...(bands && bands.length > 0 ? { bands } : {}),
890
977
  readouts: readoutsList,
891
978
  inputs: inputsList,
892
979
  formulas: formulasList,
@@ -2376,6 +2463,11 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
2376
2463
  case "値":
2377
2464
  out.value = stripQuotes(raw);
2378
2465
  break;
2466
+ // 前の時点の値 (#1450)。 内訳を帯で示す図と値 1 つを大きく示す図が読む
2467
+ case "previous":
2468
+ case "前の値":
2469
+ out.previous = stripQuotes(raw);
2470
+ break;
2379
2471
  case "shape":
2380
2472
  case "図形":
2381
2473
  out.shape = 図形として読む(raw, ln.no, errors);
@@ -2413,6 +2505,22 @@ function applyContinuationLines(actor: DslActor, rest: Line[], errors: DslError[
2413
2505
  .map((x) => stripQuotes(x.trim()))
2414
2506
  .filter(Boolean);
2415
2507
  break;
2508
+ /*
2509
+ * 行頭の印 (#1466)。 `rows` と同じ並びで、空文字はその行に印を付けない。
2510
+ *
2511
+ * **語の意味は図の種類が決める**。 ER は `pk` / `fk` / `opt`、状態遷移は
2512
+ * `entry` / `exit` / `do` / `internal`。 印の 2 軸 (形 × 塗り) は共通だが、その軸が
2513
+ * 何を指すかは種類ごとに違う = 1 つの語彙に畳むと、どの図でも意味が合わない語が残る。
2514
+ *
2515
+ * 空の要素を捨てない (`filter(Boolean)` を掛けない) = 並びが `rows` とずれる。
2516
+ */
2517
+ case "marks":
2518
+ case "印":
2519
+ out.marks = raw
2520
+ .replace(/^\[|\]$/g, "")
2521
+ .split(/,(?![^[]*\])/)
2522
+ .map((x) => stripQuotes(x.trim()));
2523
+ break;
2416
2524
  case "位置":
2417
2525
  case "pos": {
2418
2526
  // `位置: 300,200` の形。 posX と posY は両方揃わないと効かないので、 1 つの項目に
@@ -2572,8 +2680,14 @@ export const ACTOR_ITEM_KEYS: ReadonlySet<string> = new Set([
2572
2680
  "補足",
2573
2681
  "value",
2574
2682
  "値",
2683
+ // 前の時点の値 (#1450)。 内訳の変化を帯で示す図と、値 1 つを大きく示す図が読む
2684
+ "previous",
2685
+ "前の値",
2575
2686
  "rows",
2576
2687
  "行",
2688
+ // 行頭の印 (#1466)。 `rows` と同じ並びで、形 × 塗り の 2 軸を語で書く
2689
+ "marks",
2690
+ "印",
2577
2691
  // 箱の中に描く図形 (#1374)
2578
2692
  "shape",
2579
2693
  "図形",
@@ -2736,7 +2850,9 @@ const ACTOR_RESERVED_FIELDS: ReadonlySet<string> = new Set([
2736
2850
  "subtitle",
2737
2851
  "eyebrow",
2738
2852
  "value",
2853
+ "previous",
2739
2854
  "rows",
2855
+ "marks",
2740
2856
  "lane",
2741
2857
  "stack",
2742
2858
  "initial",
@@ -2888,7 +3004,9 @@ export const INLINE_ACTOR_ALIASES: Record<string, string> = {
2888
3004
  種類: "kind",
2889
3005
  補足: "subtitle",
2890
3006
  値: "value",
3007
+ 前の値: "previous",
2891
3008
  行: "rows",
3009
+ 印: "marks",
2892
3010
  };
2893
3011
 
2894
3012
  /** 中括弧に書かれた日本語の項目名を、同じ意味の英語名に寄せる (#1301) */
@@ -2910,7 +3028,9 @@ export const INLINE_ACTOR_KEYS: ReadonlySet<string> = new Set([
2910
3028
  "subtitle",
2911
3029
  "eyebrow",
2912
3030
  "value",
3031
+ "previous",
2913
3032
  "rows",
3033
+ "marks",
2914
3034
  "lane",
2915
3035
  "stack",
2916
3036
  "initial",
@@ -2973,6 +3093,33 @@ const FLOW_INLINE_READERS = {
2973
3093
  v !== undefined && (EDGE_SIDE_VALUES as readonly string[]).includes(v)
2974
3094
  ? (v as "top" | "right" | "bottom" | "left")
2975
3095
  : undefined,
3096
+ // 矢印の先の形 (#1462)。 書かなければ描画側の既定 (塗った三角) になる
3097
+ head: (v: string | undefined) =>
3098
+ v !== undefined && EDGE_HEAD_VALUES.includes(v) ? (v as EdgeHead) : undefined,
3099
+ // 出どころ側の端の形 (#1466)。 ER は端ごとに違う個数を示すので両端に要る
3100
+ tailHead: (v: string | undefined) =>
3101
+ v !== undefined && EDGE_HEAD_VALUES.includes(v) ? (v as EdgeHead) : undefined,
3102
+ // 端の印の塗り (#1466)。 白抜きの菱が「持つ」、塗った菱が「抱える」
3103
+ headFill: (v: string | undefined) =>
3104
+ v !== undefined && EDGE_HEAD_FILL_VALUES.includes(v) ? (v as EdgeHeadFill) : undefined,
3105
+ tailHeadFill: (v: string | undefined) =>
3106
+ v !== undefined && EDGE_HEAD_FILL_VALUES.includes(v) ? (v as EdgeHeadFill) : undefined,
3107
+ /*
3108
+ * クラス図の関係の種類 (#1466)。 書くと **線と端の形と塗りと付く側** がまとめて決まる
3109
+ * (`CLASS_RELATION_LOOK`)。
3110
+ *
3111
+ * 4 つを個別に書かせない = 組合せは 6 通りしか無く、1 つでも書き違えると読み手に別の意味で
3112
+ * 伝わる (菱を逆に置くと持ち主が入れ替わる)。 種類で書けばその 6 通りから外れない。
3113
+ */
3114
+ relation: (v: string | undefined) =>
3115
+ v !== undefined && CLASS_RELATION_VALUES.includes(v) ? (v as ClassRelationType) : undefined,
3116
+ /*
3117
+ * 順序図の言づての種類 (#1466)。 書くと線と矢の形がまとめて決まる。
3118
+ *
3119
+ * 呼ぶ (実線 + 塗った矢) / 返す (破線 + 開いた矢) / 投げる (実線 + 開いた矢) の 3 つ。
3120
+ */
3121
+ kind: (v: string | undefined) =>
3122
+ v !== undefined && SEQ_MESSAGE_VALUES.includes(v) ? (v as SequenceMessageKind) : undefined,
2976
3123
  /*
2977
3124
  * 矢印を値に追随させる 3 欄 (#1396)。 箱の `wBind` (#1392) と同じく文字列だけを取る。
2978
3125
  *
@@ -3080,6 +3227,7 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
3080
3227
  owner: isPart ? undefined : opts.owner,
3081
3228
  end: isPart ? undefined : opts.end,
3082
3229
  value: opts.value,
3230
+ previous: opts.previous,
3083
3231
  rows: opts.rows
3084
3232
  ? opts.rows
3085
3233
  .replace(/^\[|\]$/g, "")
@@ -3087,6 +3235,13 @@ function parseActor(line: Line, errors: DslError[]): DslActor | null {
3087
3235
  .map((x) => stripQuotes(x.trim()))
3088
3236
  .filter(Boolean)
3089
3237
  : undefined,
3238
+ // 行頭の印 (#1466)。 `rows` と同じ並びなので **空の要素を捨てない**
3239
+ marks: opts.marks
3240
+ ? opts.marks
3241
+ .replace(/^\[|\]$/g, "")
3242
+ .split(/,(?![^[]*\])/)
3243
+ .map((x) => stripQuotes(x.trim()))
3244
+ : undefined,
3090
3245
  lane: opts.lane,
3091
3246
  // 箱の中に描く図形 (#1374)。 パーツでは状態の上書きとして意味を持つため横取りしない
3092
3247
  shape:
@@ -3219,6 +3374,26 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3219
3374
  hint: `使える値 = ${EDGE_SIDE_VALUES.join(", ")}`,
3220
3375
  });
3221
3376
  }
3377
+ // 読めない語を黙って捨てない (#1462)。 捨てると「書いたのに端の形が変わらない」 が
3378
+ // 手掛かりなしで起きる
3379
+ // 読めない語を黙って捨てない (#1462 / #1466)。 捨てると「書いたのに見た目が変わらない」 が
3380
+ // 手掛かりなしで起きる。 端の 3 欄と関係の種類を同じ形で見る
3381
+ for (const [欄, 使える] of [
3382
+ ["head", EDGE_HEAD_VALUES],
3383
+ ["tailHead", EDGE_HEAD_VALUES],
3384
+ ["headFill", EDGE_HEAD_FILL_VALUES],
3385
+ ["tailHeadFill", EDGE_HEAD_FILL_VALUES],
3386
+ ["relation", CLASS_RELATION_VALUES],
3387
+ ["kind", SEQ_MESSAGE_VALUES],
3388
+ ] as const) {
3389
+ if (opts[欄] !== undefined && 中括弧[欄] === undefined) {
3390
+ errors.push({
3391
+ line: line.no,
3392
+ message: `矢印の ${欄} が読めません: "${opts[欄]}"`,
3393
+ hint: `使える値 = ${使える.join(", ")}`,
3394
+ });
3395
+ }
3396
+ }
3222
3397
  数と真偽 = 表で読む(FLOW_INLINE_VALUE_KINDS, opts, "矢印の ", line.no, errors);
3223
3398
  rest = 塊.前.trim();
3224
3399
  }
@@ -3226,6 +3401,12 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3226
3401
  const guard = 中括弧.guard as string | undefined;
3227
3402
  const cardinality = 中括弧.cardinality as string | undefined;
3228
3403
  const side = 中括弧.side as "top" | "right" | "bottom" | "left" | undefined;
3404
+ const head = 中括弧.head as EdgeHead | undefined;
3405
+ const tailHead = 中括弧.tailHead as EdgeHead | undefined;
3406
+ const headFill = 中括弧.headFill as EdgeHeadFill | undefined;
3407
+ const tailHeadFill = 中括弧.tailHeadFill as EdgeHeadFill | undefined;
3408
+ const relation = 中括弧.relation as ClassRelationType | undefined;
3409
+ const msgKind = 中括弧.kind as SequenceMessageKind | undefined;
3229
3410
  // 値に追随する 3 欄 (#1396)。 空は捨てずに知らせる = 描画側は空文字を既定値へ落とさず
3230
3411
  // そのまま置換に使うため、書き忘れが「線が消えた」 形で出る
3231
3412
  const widthBind = 追随する大きさとして読む(
@@ -3306,6 +3487,12 @@ function parseFlowStep(line: Line, no: number, errors: DslError[]): DslStep | nu
3306
3487
  guard,
3307
3488
  cardinality,
3308
3489
  side,
3490
+ head,
3491
+ tailHead,
3492
+ headFill,
3493
+ tailHeadFill,
3494
+ relation,
3495
+ msgKind,
3309
3496
  widthBind,
3310
3497
  strokeBind,
3311
3498
  dashOffsetBind,
@@ -3512,7 +3699,10 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
3512
3699
  continue;
3513
3700
  }
3514
3701
  if (key === "draw") {
3515
- const = stripQuotes(value).trim();
3702
+ // `draw: line 0.4` = 語の後ろに「描き終えるまでに段の何割を使うか」 を書ける (#1441)
3703
+ // 割合を別の項目にすると「割合だけ書いて `draw` が無い段」 が書けてしまい、
3704
+ // 何も起きない指定になる。 同じ行なら書けない
3705
+ const [語 = "", 割合の文字, ...余り] = stripQuotes(value).trim().split(/\s+/);
3516
3706
  // **読めない語を黙って捨てない** (#1304 / #1306 と同じ扱い)。 受ける語は 1 つだけで、
3517
3707
  // 書き間違いはその段が何も描かない形になって手掛かりが残らない
3518
3708
  if (!DRAW_WORDS.has(語)) {
@@ -3521,9 +3711,29 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
3521
3711
  message: `draw に書けない語です: "${語}"`,
3522
3712
  hint: `使える語 = ${[...DRAW_WORDS].join(", ")}`,
3523
3713
  });
3714
+ } else if (余り.length > 0) {
3715
+ errors.push({
3716
+ line: ln.no,
3717
+ message: `draw に書ける項目は語と割合の 2 つまでです: "${stripQuotes(value).trim()}"`,
3718
+ hint: "use `draw: line 0.4`",
3719
+ });
3524
3720
  } else {
3525
3721
  phase.draw = 語;
3526
3722
  phase.drawPos = { line: ln.no };
3723
+ if (割合の文字 !== undefined) {
3724
+ const 割合 = Number(割合の文字);
3725
+ // 範囲外を黙って捨てない。 描画側は 1 に落として図を出すため、
3726
+ // 知らせが無いと「書いたのに速さが変わらない」 が手掛かりなしで起きる
3727
+ if (!Number.isFinite(割合) || 割合 <= 0 || 割合 > 1) {
3728
+ errors.push({
3729
+ line: ln.no,
3730
+ message: `draw の割合が範囲外です: "${割合の文字}"`,
3731
+ hint: "0 より大きく 1 以下で書きます (例 `draw: line 0.4`)",
3732
+ });
3733
+ } else {
3734
+ phase.drawRatio = 割合;
3735
+ }
3736
+ }
3527
3737
  }
3528
3738
  i += 1;
3529
3739
  continue;