@cardenelabs/cdl 0.43.0 → 0.45.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.43.0",
3
+ "version": "0.45.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/index.ts CHANGED
@@ -76,6 +76,7 @@ export type {
76
76
  SweepReport,
77
77
  ValidateOptions,
78
78
  ValidationProfile,
79
+ ReferenceContainer,
79
80
  } from "./visual-validate";
80
81
  // edge label の文字仕様。 下流が主題で色を上書きする時、 「その文字に何 :1 が要るか」 は
81
82
  // 大きさと太さで決まる。 下流が 22 / 19 / 700 を書き写すと、 engine 側を変えた時に下流の
@@ -412,10 +412,65 @@ const SKIPPED_AXES: Readonly<Record<ValidationProfile, ReadonlySet<VisualAxis>>>
412
412
  catalog: new Set<VisualAxis>(["seo-metadata-quality", "structured-data-extraction"]),
413
413
  };
414
414
 
415
+ /**
416
+ * 図を描く器 (px)。 軸 23 `responsive-viewport` が画面上の字の大きさを出す時に使う。
417
+ *
418
+ * **高さは省ける**。 幅にだけ合わせて縦は巻き取る器があるため = 0 や無限大で表すと
419
+ * 「上限が無い」 と「潰れている」 が同じ値になる (`rules/quality.md § 判定できなかったことを
420
+ * 値に潰さない` と同じ形)。
421
+ */
422
+ export interface ReferenceContainer {
423
+ /** 器の幅 (px)。 正の有限値。 */
424
+ readonly width: number;
425
+ /** 器の高さ (px)。 正の有限値。 **上限が無い器では省く**。 */
426
+ readonly height?: number;
427
+ }
428
+
415
429
  /** 検査の設定。 */
416
430
  export interface ValidateOptions {
417
431
  /** 検査の用途。 既定は `production` (全ての軸を見る)。 */
418
432
  profile?: ValidationProfile;
433
+ /**
434
+ * 軸 23 `responsive-viewport` が基準にする器。
435
+ *
436
+ * 既定は下流の見本帳の拡大表示を画面で測った値 (1150 × 630px)。
437
+ * 実際の器は呼び出し側が決めるものなので、別の見せ方を測る時はここで渡す。
438
+ */
439
+ container?: ReferenceContainer;
440
+ }
441
+
442
+ /** 既定の器。 下流の見本帳の拡大表示を画面で測った値から導く (`#793` / `#797`)。 */
443
+ const 既定の器: Required<Pick<ReferenceContainer, "width">> & { height: number } = {
444
+ /**
445
+ * 拡大表示の枠は `min(1200px, 94vw)` で、中身は 1px の縁と 24px の余白の内側に描かれる。
446
+ * 画面が 1280px 以上なら 1200 が効くので、そこから縁 2 枚と余白 2 枚を引く。
447
+ */
448
+ width: 1200 - 1 * 2 - 24 * 2,
449
+ /** 同じ拡大表示の高さの頭打ち (`90vh − 180px`) を、基準の画面の高さ 900px で評価した値。 */
450
+ height: 900 * 0.9 - 180,
451
+ };
452
+
453
+ /**
454
+ * 渡された器を検査して確定させる。 **入口で 1 度だけ呼ぶ** = 軸 23 の中で投げると
455
+ * 図ごとに投げることになり、どの呼び出しが誤っていたかが読みにくい。
456
+ *
457
+ * 壊れた寸法は既定に落とさず投げる。 落とすと、誤った値を渡した呼び出し側が
458
+ * 「効いている」 と読める。
459
+ */
460
+ function 器を確かめる(c: ReferenceContainer | undefined): {
461
+ 幅: number;
462
+ 高さ: number | undefined;
463
+ } {
464
+ if (c === undefined) return { 幅: 既定の器.width, 高さ: 既定の器.height };
465
+ if (!(c.width > 0) || !Number.isFinite(c.width)) {
466
+ throw new Error(`container.width が描画不能 (正の有限値である必要がある): ${c.width}`);
467
+ }
468
+ if (c.height !== undefined && (!(c.height > 0) || !Number.isFinite(c.height))) {
469
+ throw new Error(
470
+ `container.height が描画不能 (正の有限値で渡すか、上限が無いなら省く): ${c.height}`,
471
+ );
472
+ }
473
+ return { 幅: c.width, 高さ: c.height };
419
474
  }
420
475
 
421
476
  export interface VisualValidationReport {
@@ -1401,7 +1456,16 @@ export function visualValidateLaid(
1401
1456
  counts[axis]++;
1402
1457
  };
1403
1458
 
1404
- return visualValidateInner(laid, diag, violations, counts, push, profile, [...skipped]);
1459
+ return visualValidateInner(
1460
+ laid,
1461
+ diag,
1462
+ violations,
1463
+ counts,
1464
+ push,
1465
+ profile,
1466
+ [...skipped],
1467
+ 器を確かめる(opts?.container),
1468
+ );
1405
1469
  }
1406
1470
 
1407
1471
  /**
@@ -1456,7 +1520,16 @@ export function visualValidate(diag: CdlDiagram, opts?: ValidateOptions): Visual
1456
1520
  console.warn = origWarn;
1457
1521
  }
1458
1522
 
1459
- return visualValidateInner(laid, safeDiag, violations, counts, push, profile, [...skipped]);
1523
+ return visualValidateInner(
1524
+ laid,
1525
+ safeDiag,
1526
+ violations,
1527
+ counts,
1528
+ push,
1529
+ profile,
1530
+ [...skipped],
1531
+ 器を確かめる(opts?.container),
1532
+ );
1460
1533
  }
1461
1534
 
1462
1535
  /**
@@ -1782,6 +1855,8 @@ function visualValidateInner(
1782
1855
  push: (axis: VisualAxis, detail: string, severity?: "error" | "warn") => void,
1783
1856
  profile: ValidationProfile,
1784
1857
  skippedAxes: VisualAxis[],
1858
+ /** 軸 23 が基準にする器。 入口で確かめ済 (`器を確かめる`)。 高さ `undefined` は上限なし。 */
1859
+ 器: { 幅: number; 高さ: number | undefined },
1785
1860
  ): VisualValidationReport {
1786
1861
  // 層 1 = 入口の検査 (top-level の配列 13 個と `viewBox`)。 壊れている箇所を報告してから、
1787
1862
  // 検査が反復できる形に差し替える。
@@ -1801,7 +1876,7 @@ function visualValidateInner(
1801
1876
  // 途中までに見つけた違反は `violations` に積まれているので保持される。 例外を投げた軸より
1802
1877
  // 後ろの軸は走らない = 「検査できなかった」 ことを報告に出す。
1803
1878
  try {
1804
- return runAxes(laid, diag, violations, counts, push, profile, skippedAxes);
1879
+ return runAxes(laid, diag, violations, counts, push, profile, skippedAxes, 器);
1805
1880
  } catch (err) {
1806
1881
  // 入口が既に破綻を見つけていたなら入力側、 何も見つけていなければ検査側の欠陥。
1807
1882
  // 直す先が違うので軸を分ける。 まとめると「入力が悪い」 と誤読して本 file の bug を
@@ -1837,6 +1912,8 @@ function runAxes(
1837
1912
  push: (axis: VisualAxis, detail: string, severity?: "error" | "warn") => void,
1838
1913
  profile: ValidationProfile,
1839
1914
  skippedAxes: VisualAxis[],
1915
+ /** 軸 23 が基準にする器。 入口で確かめ済 (`器を確かめる`)。 高さ `undefined` は上限なし。 */
1916
+ 器: { 幅: number; 高さ: number | undefined },
1840
1917
  ): VisualValidationReport {
1841
1918
  // ───────────────────────────────────────────────────────────
1842
1919
  // Axis 1: node-visibility (bbox 面積 / 最小寸法)
@@ -3062,22 +3139,35 @@ function runAxes(
3062
3139
  // 縦横比 1.4 なので 1 度も拾われず、 縦横比が最大の図 (比 8.0、 幅 1535 world) は 15.8px で
3063
3140
  // 十分読めた。 文字が小さい上位 20 枚のうち縦横比が拾ったのは 5 枚だけ (`#785`)。
3064
3141
  //
3065
- // 幅の **下限** は見ない。 viewBox width が小さいほど倍率が上がって読みやすくなるので、
3142
+ // 幅の **下限** は見ない。 寸法が小さいほど倍率が上がって読みやすくなるので、
3066
3143
  // 「幅が小さい = 識別不能」 は成り立たない。 以前 400 world 未満を warn にしていた頃に
3067
3144
  // 発火した 17 件は全て単体部品の図 (271-390 world) で、 幅 375px の携帯では 1.0-1.38 倍に
3068
3145
  // 拡大される最も読みやすい部類だった。
3069
3146
  //
3147
+ // `#785` は幅だけを見ていた。 図は `preserveAspectRatio="xMidYMid meet"` で器に収まるよう
3148
+ // 中で縮むため、 **横が余っていても縦が足りなければ縦が倍率を決める**。 下流の見本帳を
3149
+ // 画面で測ると 28 回のうち 27 回は縦が効いており、 幅から出す見込みは縦が長い図で
3150
+ // 2.4-2.5 倍に外れていた (実測 = 1255×1700 の図が見込み 19.3px に対し実測 8.2px、
3151
+ // 875×1256 の図が 27.7px に対し 11.0px)。 順位も逆を向く = 2190×2904 の図は見込みで
3152
+ // 14 枚中 9 位だが実測は最下位だった。 `#793` で倍率の下限に置き換えた。
3153
+ //
3070
3154
  // 描画不能な寸法 (0 / 負値 / 非有限) は別に拾う。 倍率は 0 幅で無限大になり、 負値で
3071
3155
  // 符号が反転して比較が素通りするため、 割る前に弾く。
3072
3156
  // ───────────────────────────────────────────────────────────
3073
3157
  //
3074
3158
  /**
3075
- * 判定に使う親要素の幅 (px)。 書き出しの窓 1600px から左右の余白を引いた実測の見当。
3159
+ * 判定に使う器 (px)。 **呼び出し側が `ValidateOptions.container` で渡す** (`#797`)。
3160
+ *
3161
+ * 実際の器は見せ方ごとに違う (拡大表示は 1150 × 630px、 一覧に並べた時の枠は
3162
+ * 幅 874px で高さの頭打ちが無い)。 engine はどちらで見られるかを知らないので、
3163
+ * 渡されなければ拡大表示の値を既定にする (`既定の器`)。
3076
3164
  *
3077
- * **実際の親幅は呼び出し側が決める**。 engine はそれを知らないので、 判定の基準として
3078
- * 1 つ選ぶ。 選んだ値そのものが閾値の意味を決めるため、 定数として出す。
3165
+ * **高さは省ける**。 図は `preserveAspectRatio="xMidYMid meet"` で器に収まるよう中で縮むため、
3166
+ * 高さの上限がある器では縦が倍率を決めうる (画面での実測 28 回のうち 27 回は縦が効いていた、
3167
+ * `#793`)。 縦に巻き取る器ではその制約が無いので、 高さを見ない。
3079
3168
  */
3080
- const RESPONSIVE_REF_CONTAINER_PX = 1100;
3169
+ const RESPONSIVE_REF_CONTAINER_W_PX = 器.幅;
3170
+ const RESPONSIVE_REF_CONTAINER_H_PX = 器.高さ;
3081
3171
  /**
3082
3172
  * 画面上で読めると見なす文字の下限 (px)。
3083
3173
  *
@@ -3093,9 +3183,6 @@ function runAxes(
3093
3183
  * ほぼ全ての図が境を割る。 最も多く出る文字で見る。
3094
3184
  */
3095
3185
  const RESPONSIVE_BASE_FONT_WORLD = 22;
3096
- /** 上の 3 つから導く viewBox width の上限。 数を直接書かない。 */
3097
- const RESPONSIVE_MAX_VIEWBOX_W =
3098
- (RESPONSIVE_BASE_FONT_WORLD * RESPONSIVE_REF_CONTAINER_PX) / RESPONSIVE_MIN_ON_SCREEN_FONT_PX;
3099
3186
  const vbResp = laid.viewBox;
3100
3187
  if (
3101
3188
  !(vbResp.w > 0) ||
@@ -3108,15 +3195,37 @@ function runAxes(
3108
3195
  `viewBox 寸法 ${vbResp.w}×${vbResp.h} が描画不能 (正の有限値である必要がある)`,
3109
3196
  "warn",
3110
3197
  );
3111
- } else if (vbResp.w > RESPONSIVE_MAX_VIEWBOX_W) {
3112
- const 画面上の字 = (RESPONSIVE_BASE_FONT_WORLD * RESPONSIVE_REF_CONTAINER_PX) / vbResp.w;
3113
- push(
3114
- "responsive-viewport",
3115
- `viewBox width ${vbResp.w} が上限 ${RESPONSIVE_MAX_VIEWBOX_W.toFixed(0)} 超え ` +
3116
- `(親幅 ${RESPONSIVE_REF_CONTAINER_PX}px で箱の題が ${画面上の字.toFixed(1)}px、 ` +
3117
- `下限 ${RESPONSIVE_MIN_ON_SCREEN_FONT_PX}px)`,
3118
- "warn",
3119
- );
3198
+ } else {
3199
+ // 器に収めた時に効く倍率。 `xMidYMid meet` は縦横の小さい方に合わせる。
3200
+ const 横の倍率 = RESPONSIVE_REF_CONTAINER_W_PX / vbResp.w;
3201
+ // **高さの上限が無い器では縦を見ない**。 0 や巨大な数で表すと「上限なし」 と
3202
+ // 「潰れている」 が同じ値になるため、 `undefined` のまま `Infinity` に落とす。
3203
+ const 縦の倍率 =
3204
+ RESPONSIVE_REF_CONTAINER_H_PX === undefined
3205
+ ? Number.POSITIVE_INFINITY
3206
+ : RESPONSIVE_REF_CONTAINER_H_PX / vbResp.h;
3207
+ const 倍率 = Math.min(横の倍率, 縦の倍率);
3208
+ const 画面上の字 = RESPONSIVE_BASE_FONT_WORLD * 倍率;
3209
+ if (画面上の字 < RESPONSIVE_MIN_ON_SCREEN_FONT_PX) {
3210
+ // **効いた辺を出す**。 出さないと、 幅を削っても直らない図で書き手が幅を削り続ける
3211
+ // (実測 = 1255×1700 の図は幅を半分にしても縦が効いたまま)。
3212
+ const 効く辺 = 横の倍率 <= 縦の倍率 ? "横" : "縦";
3213
+ const 収まる寸法 =
3214
+ 効く辺 === "横" || RESPONSIVE_REF_CONTAINER_H_PX === undefined
3215
+ ? `幅 ${((RESPONSIVE_REF_CONTAINER_W_PX * RESPONSIVE_BASE_FONT_WORLD) / RESPONSIVE_MIN_ON_SCREEN_FONT_PX).toFixed(0)} 以下`
3216
+ : `高さ ${((RESPONSIVE_REF_CONTAINER_H_PX * RESPONSIVE_BASE_FONT_WORLD) / RESPONSIVE_MIN_ON_SCREEN_FONT_PX).toFixed(0)} 以下`;
3217
+ const 器の書き方 =
3218
+ RESPONSIVE_REF_CONTAINER_H_PX === undefined
3219
+ ? `器 幅 ${RESPONSIVE_REF_CONTAINER_W_PX}px (高さの上限なし)`
3220
+ : `器 ${RESPONSIVE_REF_CONTAINER_W_PX}×${RESPONSIVE_REF_CONTAINER_H_PX}px`;
3221
+ push(
3222
+ "responsive-viewport",
3223
+ `viewBox ${vbResp.w}×${vbResp.h} は${器の書き方} で ` +
3224
+ `${倍率.toFixed(3)} 倍に縮み、 箱の題が ${画面上の字.toFixed(1)}px になる ` +
3225
+ `(下限 ${RESPONSIVE_MIN_ON_SCREEN_FONT_PX}px、 倍率を決めたのは ${効く辺}、 ${収まる寸法}にすると届く)`,
3226
+ "warn",
3227
+ );
3228
+ }
3120
3229
  }
3121
3230
 
3122
3231
  // ───────────────────────────────────────────────────────────
@@ -4267,6 +4376,9 @@ export interface SweepReport {
4267
4376
  }
4268
4377
 
4269
4378
  export function visualValidateAll(diagrams: CdlDiagram[], opts?: ValidateOptions): SweepReport {
4379
+ // **図を 1 枚も持たない呼び出しでも器を確かめる**。 各図の `visualValidate` に任せると、
4380
+ // 図が 0 枚の掃き出しでは壊れた器が黙って通る (`#797`)。
4381
+ 器を確かめる(opts?.container);
4270
4382
  // Axis 52 用 = 各 diagram の validate 実行時間 (ms) を計測。 performance budget 超過検知。
4271
4383
  const durations: number[] = [];
4272
4384
  const reports = diagrams.map((d) => {