@cardenelabs/cdl 0.59.0 → 0.60.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.
@@ -2463,6 +2463,13 @@ type CdlFormula = {
2463
2463
  id: string;
2464
2464
  /** safe subset expression 文字列。 例 `"gasBase * (1 + delta/8)"` */
2465
2465
  expression: string;
2466
+ /**
2467
+ * 操作部の式の行に描く名札 (#853)。 無ければ id を描く。
2468
+ *
2469
+ * 名札があると、式の中身の名前のうち名札を持つ入力欄と式の id も名札に置き換えて描く。
2470
+ * 置き換えは表示だけで、計算は `expression` を読む。
2471
+ */
2472
+ label?: string;
2466
2473
  };
2467
2474
  /**
2468
2475
  * event handler の bind target (CAR #246)。 node / edge / lane の id 指定 or diagram 全体 (root)。
@@ -2463,6 +2463,13 @@ type CdlFormula = {
2463
2463
  id: string;
2464
2464
  /** safe subset expression 文字列。 例 `"gasBase * (1 + delta/8)"` */
2465
2465
  expression: string;
2466
+ /**
2467
+ * 操作部の式の行に描く名札 (#853)。 無ければ id を描く。
2468
+ *
2469
+ * 名札があると、式の中身の名前のうち名札を持つ入力欄と式の id も名札に置き換えて描く。
2470
+ * 置き換えは表示だけで、計算は `expression` を読む。
2471
+ */
2472
+ label?: string;
2466
2473
  };
2467
2474
  /**
2468
2475
  * event handler の bind target (CAR #246)。 node / edge / lane の id 指定 or diagram 全体 (root)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.59.0",
3
+ "version": "0.60.0",
4
4
  "description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
package/src/builder.ts CHANGED
@@ -1596,8 +1596,10 @@ export type DiagramBuilder = {
1596
1596
  * consumer が createFormulaComputeds() で computed 化して signal と reactive bind する。
1597
1597
  * expression syntax error は本 method 内で throw (fail-fast)。
1598
1598
  * 例 `.formula("gasFee", "gasBase * (1 + delta/8)")`
1599
+ *
1600
+ * `opts.label` は操作部の式の行に描く名札 (#853)。 例 `.formula("total", "baseFee + tip", { label: "合計" })`
1599
1601
  */
1600
- formula: (id: string, expression: string) => DiagramBuilder;
1602
+ formula: (id: string, expression: string, opts?: { label?: string }) => DiagramBuilder;
1601
1603
  /**
1602
1604
  * derive chain = 「連鎖形 formula」 を N 個宣言的に生成 (CAR: derive chain)。
1603
1605
  * 各 iteration で `{i}` 置換と、 直前 iteration の id (`prev`) を参照可能。
@@ -1607,11 +1609,14 @@ export type DiagramBuilder = {
1607
1609
  *
1608
1610
  * baseId は「gas1 / gas2 / gas3」 のように `gas${i+1}` として使われる、
1609
1611
  * expr fn は index (0..count-1) と prev id (前 iteration の完全 id) を受け取る。
1612
+ *
1613
+ * `opts.label` は index を受け取り、その式の名札を返す (#853)。 例 `{ label: (i) => \`${i + 1} 回目\` }`
1610
1614
  */
1611
1615
  deriveChain: (
1612
1616
  baseId: string,
1613
1617
  count: number,
1614
1618
  exprFn: (i: number, prev: string | null) => string,
1619
+ opts?: { label?: (i: number) => string },
1615
1620
  ) => DiagramBuilder;
1616
1621
  /**
1617
1622
  * animation primitive (CAR #245)。 `.animation.scroll(id, opts)` で scroll-driven trigger を追加。
@@ -2213,7 +2218,7 @@ export function diagram(
2213
2218
  return api;
2214
2219
  },
2215
2220
  },
2216
- formula(formulaId, expression) {
2221
+ formula(formulaId, expression, opts) {
2217
2222
  assertUniqueFormulaId(formulaId);
2218
2223
  // 入口で parse を試み、 syntax error は formula id 付きで throw (fail-fast)
2219
2224
  try {
@@ -2222,10 +2227,11 @@ export function diagram(
2222
2227
  const msg = e instanceof Error ? e.message : String(e);
2223
2228
  throw new Error(`formula '${formulaId}' の expression syntax error: ${msg}`);
2224
2229
  }
2225
- formulas.push({ id: formulaId, expression });
2230
+ // 名札の無い式は `label` を持たせない (空の文字列も持たせない = 名札の無い図の形を変えない)
2231
+ formulas.push({ id: formulaId, expression, ...(opts?.label ? { label: opts.label } : {}) });
2226
2232
  return api;
2227
2233
  },
2228
- deriveChain(baseId, count, exprFn) {
2234
+ deriveChain(baseId, count, exprFn, opts) {
2229
2235
  if (!Number.isInteger(count) || count < 1 || count > 100) {
2230
2236
  throw new Error(`deriveChain: count (${count}) は 1..100 の整数である必要があります`);
2231
2237
  }
@@ -2242,7 +2248,9 @@ export function diagram(
2242
2248
  const msg = e instanceof Error ? e.message : String(e);
2243
2249
  throw new Error(`deriveChain '${id}' の expression syntax error: ${msg}`);
2244
2250
  }
2245
- formulas.push({ id, expression: expr });
2251
+ const label = opts?.label?.(i);
2252
+ // 名札の無い式は `label` を持たせない (空の文字列も持たせない = 名札の無い図の形を変えない)
2253
+ formulas.push({ id, expression: expr, ...(label ? { label } : {}) });
2246
2254
  prev = id;
2247
2255
  }
2248
2256
  return api;
@@ -21,7 +21,8 @@ import { isValidTemplateName } from "../template-name";
21
21
 
22
22
  type Token =
23
23
  | { kind: "num"; value: number; pos: number }
24
- | { kind: "id"; name: string; pos: number }
24
+ /** `end` は字句の直後の位置。 `{name}` の形は閉じ括弧の直後 (表示用の置き換えが字句ごと差し替える) */
25
+ | { kind: "id"; name: string; pos: number; end: number }
25
26
  | { kind: "math"; fn: MathFnName; pos: number }
26
27
  | { kind: "op"; op: BinaryOp; pos: number }
27
28
  | { kind: "lparen"; pos: number }
@@ -92,7 +93,7 @@ function tokenize(src: string): Token[] {
92
93
  `invalid name "{${name}}" at position ${start} (空白 / { } / . / [ ] は名前に使えない)`,
93
94
  );
94
95
  }
95
- tokens.push({ kind: "id", name, pos: start });
96
+ tokens.push({ kind: "id", name, pos: start, end: close + 1 });
96
97
  i = close + 1;
97
98
  continue;
98
99
  }
@@ -156,7 +157,7 @@ function tokenize(src: string): Token[] {
156
157
  `property access "${name}.${src[i + 1] ?? ""}..." at position ${start} is not allowed (only Math.* namespace is accessible)`,
157
158
  );
158
159
  }
159
- tokens.push({ kind: "id", name, pos: start });
160
+ tokens.push({ kind: "id", name, pos: start, end: i });
160
161
  continue;
161
162
  }
162
163
  // operators
@@ -401,6 +402,34 @@ export function parseFormula(expression: string): FormulaAst {
401
402
  return new Parser(tokens).parse();
402
403
  }
403
404
 
405
+ /**
406
+ * 式の中身の名前を、`名札` に載る名前だけ名札に置き換えた表示用の字を返す (#853)。 **計算には使わない**。
407
+ *
408
+ * 置き換えは字句の位置で行う。 文字列の検索で探すと、`gas1` の置き換えが `gas10` の先頭に当たり、
409
+ * 値の `min` の置き換えが `Math.min` の中に当たる。 関数の名前 (`Math.min` / 直後が `(` の `min`) と数は
410
+ * 名前の字句にならないので置き換わらない。 名札が空の文字列の名前は置き換えない。
411
+ *
412
+ * 字句に切れない式 (構文の誤り) は、元の字をそのまま返す。
413
+ */
414
+ export function 式を名札で読む(expression: string, 名札: ReadonlyMap<string, string>): string {
415
+ let tokens: Token[];
416
+ try {
417
+ tokens = tokenize(expression);
418
+ } catch {
419
+ return expression;
420
+ }
421
+ let 読み = "";
422
+ let 前 = 0;
423
+ for (const t of tokens) {
424
+ if (t.kind !== "id") continue;
425
+ const 札 = 名札.get(t.name);
426
+ if (札 === undefined || 札 === "") continue;
427
+ 読み += expression.slice(前, t.pos) + 札;
428
+ 前 = t.end;
429
+ }
430
+ return 読み + expression.slice(前);
431
+ }
432
+
404
433
  /**
405
434
  * AST 内で参照される identifier (signal 名) を全て収集する。 evaluator が signal 経由で
406
435
  * runtime value を lookup する時の識別子リスト作成に使う。
@@ -133,6 +133,36 @@ export function displayWidth(text: string): number {
133
133
  return w;
134
134
  }
135
135
 
136
+ /**
137
+ * 字を先頭から半角の桁数 (`displayWidth`) で `桁` に収まるだけ取り、取った字と残りに割る (#851)。
138
+ *
139
+ * 2 桁の字が境目をまたぐ時は、その字を残りの側に回す (取った側が `桁` を越えない)。
140
+ */
141
+ export function 桁で割る(text: string, 桁: number): [string, string] {
142
+ let w = 0;
143
+ let 取った = "";
144
+ for (const ch of text) {
145
+ const code = ch.codePointAt(0) ?? 0;
146
+ const 字の桁 = code >= 0x20 && code <= 0x7e ? 1 : 2;
147
+ if (w + 字の桁 > 桁) break;
148
+ w += 字の桁;
149
+ 取った += ch;
150
+ }
151
+ return [取った, text.slice(取った.length)];
152
+ }
153
+
154
+ /**
155
+ * 字が半角の桁数で `上限` を越える時、「上限 - 1」 桁に収まるだけの字に `…` を付けて返す (#851)。
156
+ *
157
+ * 読み出しは字数で省いていた。 上限の字数は英字の幅で決めてあり、和文は 1 字が英字のおよそ 2 倍の幅なので
158
+ * 上限の字数に届く前に枠からはみ出していた。 上限を半角の桁数と読み替えれば、ASCII だけの字は変更前の
159
+ * `length > 上限 ? slice(0, 上限 - 1) + "…"` と同じ所で省き、和文は半分の字数で省く。
160
+ */
161
+ export function 桁で省く(text: string, 上限: number): string {
162
+ if (displayWidth(text) <= 上限) return text;
163
+ return `${桁で割る(text, 上限 - 1)[0]}…`;
164
+ }
165
+
136
166
  /**
137
167
  * rows[] から最低限必要な node width を算出。
138
168
  * 左 padding + 左列 max 幅 + col gap + 右列 max 幅 + 右 padding。
@@ -5,6 +5,7 @@ import { assertNever } from "../assert-never";
5
5
  import type { CdlDiagram, CdlInput, CdlReadout } from "../types";
6
6
  import { createInputSignals, inputDefaultValue } from "../input-signals";
7
7
  import { createFormulaComputeds } from "../formula";
8
+ import { 式を名札で読む } from "../formula/parser";
8
9
  import {
9
10
  createScrollProgressSignals,
10
11
  type ScrollProgressHandle,
@@ -16,6 +17,7 @@ import {
16
17
  import type { Signal, Computed } from "../reactive";
17
18
  import { effect } from "../reactive";
18
19
  import { textWidth } from "../kinds/text-width";
20
+ import { 桁で割る, 桁で省く } from "../kinds/row-align";
19
21
 
20
22
  /**
21
23
  * interactive primitive (input widget / formula / scroll trigger / event handler) を SVG diagram に
@@ -642,6 +644,10 @@ export function useInteractiveIntegration(
642
644
  readouts.length === 0
643
645
  )
644
646
  return null;
647
+ // 式の中身の名前を読む名札 (#853)。 名札を持つ入力欄と式だけを載せる。
648
+ // 巻き取りは載せない = 式は入力欄と前の式しか参照できず (`createFormulaComputeds` が他の名前を拒む)、巻き取りの名前は中身に出ない
649
+ const 名札 = new Map<string, string>();
650
+ for (const x of [...inputs, ...formulasList]) if (x.label) 名札.set(x.id, x.label);
645
651
  return (
646
652
  // 操作部の中の読み出しと入力欄へ、図の言語を届ける (#849)。 型の外の値は英語に落ちる
647
653
  <操作部の言語.Provider value={resolveLang(diagram.lang)}>
@@ -659,7 +665,9 @@ export function useInteractiveIntegration(
659
665
  <FormulaReadout
660
666
  key={f.id}
661
667
  id={f.id}
662
- expression={f.expression}
668
+ name={f.label || f.id}
669
+ // 名札の無い式は中身も書いたまま描く。 入力欄の名札は多くの図にあるので、置き換えると名札を付けていない式の描き方まで変わる
670
+ expression={f.label ? 式を名札で読む(f.expression, 名札) : f.expression}
663
671
  value={stateOverrides[f.id]}
664
672
  />
665
673
  ))}
@@ -668,7 +676,7 @@ export function useInteractiveIntegration(
668
676
  {scrolls.length > 0 && (
669
677
  <div className="cdl-ip-group">
670
678
  {scrolls.map((s) => (
671
- <ScrollReadout key={s.id} id={s.id} progress={stateOverrides[s.id]} />
679
+ <ScrollReadout key={s.id} id={s.id} name={s.label || s.id} progress={stateOverrides[s.id]} />
672
680
  ))}
673
681
  </div>
674
682
  )}
@@ -965,30 +973,36 @@ function NumberInputWidget({
965
973
  );
966
974
  }
967
975
 
976
+ /** 式の行。 `name` は名札か id (#853)、目印 `data-cdl-formula` は名札の有無によらず id */
968
977
  function FormulaReadout({
969
978
  id,
979
+ name,
970
980
  expression,
971
981
  value,
972
982
  }: {
973
983
  id: string;
984
+ name: string;
974
985
  expression: string;
975
986
  value: unknown;
976
987
  }): JSX.Element {
977
988
  return (
978
989
  <div className="cdl-ip-item cdl-ip-formula" data-cdl-formula={id}>
979
- <span className="cdl-ip-label">{id}</span>
990
+ <span className="cdl-ip-label">{name}</span>
980
991
  <code className="cdl-ip-formula-expr">= {expression}</code>
981
992
  <span className="cdl-ip-formula-value">{String(value ?? "")}</span>
982
993
  </div>
983
994
  );
984
995
  }
985
996
 
986
- function ScrollReadout({ id, progress }: { id: string; progress: unknown }): JSX.Element {
997
+ /**
998
+ * 巻き取りの進みの行。 `name` は名札か id (#853)。 型と組み立ては `label` を持っていたが、この行は id を描いていた
999
+ */
1000
+ function ScrollReadout({ id, name, progress }: { id: string; name: string; progress: unknown }): JSX.Element {
987
1001
  const p = typeof progress === "number" ? progress : 0;
988
1002
  const pct = Math.round(p * 100);
989
1003
  return (
990
1004
  <div className="cdl-ip-item cdl-ip-scroll" data-cdl-scroll={id}>
991
- <span className="cdl-ip-label">{id}</span>
1005
+ <span className="cdl-ip-label">{name}</span>
992
1006
  <div className="cdl-ip-scroll-track">
993
1007
  <div className="cdl-ip-scroll-fill" style={{ width: `${pct}%` }} />
994
1008
  </div>
@@ -7510,7 +7524,7 @@ function KanbanBoardReadout({
7510
7524
  style={{ userSelect: "none" }}
7511
7525
  >
7512
7526
  <title>{task.title}</title>
7513
- {task.title.length > 18 ? `${task.title.slice(0, 17)}…` : task.title}
7527
+ {桁で省く(task.title, 18)}
7514
7528
  </text>
7515
7529
  </g>
7516
7530
  );
@@ -7667,7 +7681,7 @@ function TimelineVerticalReadout({
7667
7681
  style={{ userSelect: "none" }}
7668
7682
  >
7669
7683
  <title>{ev.title}</title>
7670
- {ev.title.length > 38 ? `${ev.title.slice(0, 37)}…` : ev.title}
7684
+ {桁で省く(ev.title, 38)}
7671
7685
  </text>
7672
7686
  {ev.description ? (
7673
7687
  <text
@@ -7678,7 +7692,7 @@ function TimelineVerticalReadout({
7678
7692
  style={{ userSelect: "none" }}
7679
7693
  >
7680
7694
  <title>{ev.description}</title>
7681
- {ev.description.length > 44 ? `${ev.description.slice(0, 43)}…` : ev.description}
7695
+ {桁で省く(ev.description, 44)}
7682
7696
  </text>
7683
7697
  ) : null}
7684
7698
  </g>
@@ -8036,7 +8050,7 @@ function StepProgressReadout({
8036
8050
  style={{ userSelect: "none" }}
8037
8051
  >
8038
8052
  <title>{s}</title>
8039
- {s.length > 10 ? `${s.slice(0, 9)}…` : s}
8053
+ {桁で省く(s, 10)}
8040
8054
  </text>
8041
8055
  </g>
8042
8056
  );
@@ -8102,7 +8116,7 @@ function UserPresenceReadout({
8102
8116
  fill="#0f172a"
8103
8117
  style={{ userSelect: "none" }}
8104
8118
  >
8105
- {u.name.length > 24 ? `${u.name.slice(0, 23)}…` : u.name}
8119
+ {桁で省く(u.name, 24)}
8106
8120
  </text>
8107
8121
  <text
8108
8122
  x={width - 4}
@@ -8272,7 +8286,7 @@ function OrgChartMiniReadout({
8272
8286
  style={{ userSelect: "none" }}
8273
8287
  >
8274
8288
  <title>{n.name}</title>
8275
- {n.name.length > 10 ? `${n.name.slice(0, 9)}…` : n.name}
8289
+ {桁で省く(n.name, 10)}
8276
8290
  </text>
8277
8291
  {lvl < 2 && byLevel[lvl + 1]!.length > 0 ? (
8278
8292
  <line
@@ -8887,7 +8901,7 @@ function FileDropzoneReadout({
8887
8901
  </text>
8888
8902
  <text x={54} y={34} fontSize={12} fontWeight={600} fill="#0f172a" style={{ userSelect: "none" }}>
8889
8903
  <title>{filename}</title>
8890
- {filename.length > 26 ? `${filename.slice(0, 25)}…` : filename}
8904
+ {桁で省く(filename, 26)}
8891
8905
  </text>
8892
8906
  <text x={54} y={50} fontSize={10} fill="#64748b" style={{ userSelect: "none" }}>
8893
8907
  <字 k="readout.dropzone.uploaded" />
@@ -8972,7 +8986,7 @@ function LogStreamReadout({
8972
8986
  <rect x={0} y={0} width={width} height={height} fill="#0f172a" rx={4} />
8973
8987
  {shown.map((r, i) => {
8974
8988
  const y = 4 + i * rowH;
8975
- const displayMsg = r.msg.length > 24 ? `${r.msg.slice(0, 23)}…` : r.msg;
8989
+ const displayMsg = 桁で省く(r.msg, 24);
8976
8990
  return (
8977
8991
  <g key={i} data-cdl-log-row={i}>
8978
8992
  <text x={6} y={y + 12} fontSize={9} fill="#94a3b8" fontFamily="monospace" style={{ userSelect: "none" }}>
@@ -9071,7 +9085,7 @@ function AlertBannerReadout({
9071
9085
  style={{ userSelect: "none" }}
9072
9086
  >
9073
9087
  <title>{message}</title>
9074
- {message.length > 32 ? `${message.slice(0, 31)}…` : message}
9088
+ {桁で省く(message, 32)}
9075
9089
  </text>
9076
9090
  </svg>
9077
9091
  </div>
@@ -9144,7 +9158,7 @@ function ServiceHealthReadout({
9144
9158
  style={{ userSelect: "none" }}
9145
9159
  >
9146
9160
  <title>{s.name}</title>
9147
- {s.name.length > 10 ? `${s.name.slice(0, 9)}…` : s.name}
9161
+ {桁で省く(s.name, 10)}
9148
9162
  </text>
9149
9163
  </g>
9150
9164
  );
@@ -9328,7 +9342,7 @@ function PricingTierReadout({
9328
9342
  </text>
9329
9343
  {features.slice(0, 3).map((f, i) => {
9330
9344
  const y = 96 + i * 18;
9331
- const displayF = f.length > 24 ? `${f.slice(0, 23)}…` : f;
9345
+ const displayF = 桁で省く(f, 24);
9332
9346
  return (
9333
9347
  <g key={i} data-cdl-tier-feature={i}>
9334
9348
  <text x={16} y={y} fontSize={11} fill={colorAccent} style={{ userSelect: "none" }}>
@@ -9487,6 +9501,9 @@ function ArticlePreviewReadout({
9487
9501
  const width = 300;
9488
9502
  const height = 116;
9489
9503
  const thumbW = 80;
9504
+ // 抜粋は 1 行 28 桁で 2 行に割る (#851)。 1 行目は収まるだけ置いて `…` を付けず、残りを 2 行目に続ける。
9505
+ // 以前は 1 行目を 27 字と `…` で切り、2 行目を 29 字目から描いていたので、28 字目がどちらの行にも出なかった
9506
+ const [抜粋の一行目, 抜粋の残り] = 桁で割る(excerpt, 28);
9490
9507
  return (
9491
9508
  <div className="cdl-ip-item cdl-ip-readout-article-preview" data-cdl-readout={id}>
9492
9509
  <span className="cdl-ip-label">{label}</span>
@@ -9518,14 +9535,14 @@ function ArticlePreviewReadout({
9518
9535
  style={{ userSelect: "none" }}
9519
9536
  >
9520
9537
  <title>{title}</title>
9521
- {title.length > 26 ? `${title.slice(0, 25)}…` : title}
9538
+ {桁で省く(title, 26)}
9522
9539
  </text>
9523
9540
  <text x={thumbW + 20} y={44} fontSize={10} fill="#334155" style={{ userSelect: "none" }}>
9524
9541
  <title>{excerpt}</title>
9525
- {excerpt.length > 28 ? `${excerpt.slice(0, 27)}…` : excerpt}
9542
+ {抜粋の一行目}
9526
9543
  </text>
9527
9544
  <text x={thumbW + 20} y={60} fontSize={10} fill="#334155" style={{ userSelect: "none" }}>
9528
- {excerpt.length > 56 ? `${excerpt.slice(28, 55)}…` : excerpt.slice(28, 56)}
9545
+ {桁で省く(抜粋の残り, 28)}
9529
9546
  </text>
9530
9547
  <text x={thumbW + 20} y={height - 14} fontSize={10} fontWeight={600} fill="#64748b" style={{ userSelect: "none" }}>
9531
9548
  <字 k="readout.article.by" v={{ author }} />
@@ -9588,7 +9605,7 @@ function TocNavReadout({
9588
9605
  {shown.map((it, i) => {
9589
9606
  const y = 4 + i * rowH;
9590
9607
  const indent = 12 + it.level * 16;
9591
- const displayT = it.title.length > 30 ? `${it.title.slice(0, 29)}…` : it.title;
9608
+ const displayT = 桁で省く(it.title, 30);
9592
9609
  return (
9593
9610
  <g key={i} data-cdl-toc-item={i}>
9594
9611
  {it.active ? (
package/src/types.ts CHANGED
@@ -2514,6 +2514,13 @@ export type CdlFormula = {
2514
2514
  id: string;
2515
2515
  /** safe subset expression 文字列。 例 `"gasBase * (1 + delta/8)"` */
2516
2516
  expression: string;
2517
+ /**
2518
+ * 操作部の式の行に描く名札 (#853)。 無ければ id を描く。
2519
+ *
2520
+ * 名札があると、式の中身の名前のうち名札を持つ入力欄と式の id も名札に置き換えて描く。
2521
+ * 置き換えは表示だけで、計算は `expression` を読む。
2522
+ */
2523
+ label?: string;
2517
2524
  };
2518
2525
 
2519
2526
  /**