@cardenelabs/dragon 0.10.0 → 0.12.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/dragon",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Dragon — Mermaid 感覚で animated SVG を生成する Text DSL。 cdl engine を内部利用。",
5
5
  "license": "MIT",
6
6
  "author": "cardene777",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "pnpm run typecheck && pnpm run test && pnpm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@cardenelabs/cdl": "^0.12.0"
59
+ "@cardenelabs/cdl": "^0.14.0"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "react": "^19.0.0",
package/src/compile.ts CHANGED
@@ -4645,7 +4645,9 @@ function compileTree(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
4645
4645
  const data: NonNullable<CdlDiagram["nodes"][number]["treeData"]> = doc.actors.map((a) => {
4646
4646
  const id = slugify(a.name);
4647
4647
  const p3 = 親.get(id);
4648
- return { id, title: a.name, ...(p3 !== undefined ? { parent: p3 } : {}) };
4648
+ // 木も放射と同じく名前と補足を分けて渡す (#1332) 分けないと補足が捨てられ、
4649
+ // 書いた文字が図に出ない
4650
+ return { id, ...放射に出す文字(a), ...(p3 !== undefined ? { parent: p3 } : {}) };
4649
4651
  });
4650
4652
  b.node(`${slugify(doc.title) || "tree"}-chart`, {
4651
4653
  lane: "chart", stack: 0, kind: "tree-hierarchy", title: doc.title, ...図の小見出し(doc), w: W, h: H, treeData: data,
@@ -4997,14 +4999,13 @@ type 放射で描ける欄 =
4997
4999
  /** 中心の名前 / 枝の名前になる */
4998
5000
  | "name"
4999
5001
  /**
5000
- * 名前の後ろに続けて出す 2 欄。
5002
+ * 名前とは分けて補足に出す 2 欄。
5001
5003
  *
5002
5004
  * `- 描く量を減らす: "{draw} ms"` の形は副題として解析されるため、 値の欄だけを見ると
5003
5005
  * 記法で最も普通な書き方が届かない (#1230 で実測)。 明示的に書いた値の欄も同じ場所へ出す。
5004
5006
  *
5005
- * 描画側が描くのは枝と中心の `title` だけで (`mind-map.tsx` は型にある `subtitle`
5006
- * 1 度も描かない)、 状態を読み替えるのも `title` と `rootTitle` に限る
5007
- * (`resolveMindData`)。 届ける先が他に無いため名前と同じ欄へ載せる。
5007
+ * 描画側は枝の `subtitle` と中心の `rootSubtitle` を名前とは別の行に描く。 2 欄を
5008
+ * `放射に出す文字` 1 つの補足にまとめ、 名前とは分けて渡す。
5008
5009
  */
5009
5010
  | "subtitle"
5010
5011
  | "value"
@@ -5115,18 +5116,17 @@ function 放射で描けない欄を書いたか(a: DslActor, 欄: 放射で描
5115
5116
  }
5116
5117
 
5117
5118
  /**
5118
- * 放射の枝と中心に出す文字を組む (#1230)。
5119
+ * 放射と木の箱に出す名前と補足 (#1332)。
5119
5120
  *
5120
- * 値の欄を書いた登場人物は名前の後ろに空白 1 つで続ける。 書いていない図は名前だけになり、
5121
- * 変更前と同じ文字列になる。
5121
+ * **連結しない**。 描画側は名前と補足を別々に受け取れば箱の中で 2 行に積む。 1 つの文字列に
5122
+ * すると 1 行に全部入り、箱幅を超えて末尾が切られる (実測 = 箱 120px に対し文字 163px)。
5122
5123
  *
5123
- * **名前と同じ欄に載せるしかない**。 描画側が描くのは枝と中心の `title` だけで
5124
- * (`mind-map.tsx` は型にある `subtitle` を 1 度も描かない)、 状態を読み替えるのも
5125
- * `title` と `rootTitle` に限る (`resolveMindData`)。 値を別の欄へ渡しても絵に出ない。
5124
+ * `subtitle` `value` の両方が書かれた場合は空白で繋いで 1 つの補足にする。 描画側の
5125
+ * 補足は 1 行なので、2 つを別々の行にはできない。
5126
5126
  */
5127
- function 放射に出す文字(a: DslActor): string {
5127
+ function 放射に出す文字(a: DslActor): { title: string; subtitle?: string } {
5128
5128
  const 続き = [a.subtitle, a.value].map((x) => x?.trim()).filter((x): x is string => !!x);
5129
- return 続き.length > 0 ? `${a.name} ${続き.join(" ")}` : a.name;
5129
+ return { title: a.name, ...(続き.length > 0 ? { subtitle: 続き.join(" ") } : {}) };
5130
5130
  }
5131
5131
 
5132
5132
  function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): CdlDiagram {
@@ -5246,7 +5246,7 @@ function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
5246
5246
  // 枝は色を持てる (`MindBranchNode.tone`)
5247
5247
  branches.push({
5248
5248
  id,
5249
- title: 放射に出す文字(a),
5249
+ ...放射に出す文字(a),
5250
5250
  parent: 親.get(id) ?? rootId,
5251
5251
  ...(a.tone ? { tone: a.tone } : {}),
5252
5252
  });
@@ -5269,7 +5269,15 @@ function compileMind(doc: DslDocument, onNotice?: (n: CompileNotice) => void): C
5269
5269
  ...図の小見出し(doc),
5270
5270
  w: W,
5271
5271
  h: H,
5272
- mindData: { rootId, rootTitle: 放射に出す文字(root), branches },
5272
+ mindData: (() => {
5273
+ const 中心 = 放射に出す文字(root);
5274
+ return {
5275
+ rootId,
5276
+ rootTitle: 中心.title,
5277
+ ...(中心.subtitle === undefined ? {} : { rootSubtitle: 中心.subtitle }),
5278
+ branches,
5279
+ };
5280
+ })(),
5273
5281
  });
5274
5282
  return b.build();
5275
5283
  }
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ export { compileToCdl } from "./compile";
13
13
  export type { CompileNotice } from "./compile";
14
14
  export { parseTextDslV05 } from "./v05";
15
15
  // 記法一覧が「実際に受け付ける値」 を実装から引くための公開。 手書きすると説明と実装がずれる。
16
- export { DRAW_TARGETS, DRAW_WORDS, PRESET_TYPES, TOP_LEVEL_KEYS } from "./v05/parser";
16
+ export { DRAW_TARGETS, DRAW_WORDS, PHASE_ITEM_WORDS, PRESET_TYPES, TOP_LEVEL_KEYS } from "./v05/parser";
17
17
  export { TONE_ALIAS, NODE_KIND_ALIAS } from "./keywords";
18
18
  export { lintDiagram, autoFix } from "./notation-lint";
19
19
  export type { LintIssue, LintReport, LintSeverity } from "./notation-lint";
package/src/v05/parser.ts CHANGED
@@ -1069,24 +1069,63 @@ function writtenScaleFields(inner: string): Map<string, string> {
1069
1069
  *
1070
1070
  * `parseInlineMapping` と、倍率の「書かれた名前」 を拾う経路 (#1026) で共用する。
1071
1071
  * 割り方を 2 つ持つと、片方だけが拾える項目という食い違いが生まれる。
1072
+ *
1073
+ * **引用符の中のカンマでは割らない** (#1367)。 見ていなかった間、`{ subtitle: "a, b" }` の
1074
+ * 補足が `"a` に切れて図に出ていた (実測で見本 1 件が踏んでいた)。 配列
1075
+ * (`{ initial: '["入力", "確認"]' }`) が壊れていなかったのは角括弧の深さで守られていたためで、
1076
+ * 引用符だけで守られる値は守られていなかった。
1072
1077
  */
1073
1078
  function splitInlineFields(inner: string): string[] {
1074
- let depth = 0;
1075
- let buf = "";
1076
- const parts: string[] = [];
1077
- for (let i = 0; i < inner.length; i += 1) {
1078
- const c = inner[i]!;
1079
- if (c === "[" || c === "{") depth += 1;
1080
- else if (c === "]" || c === "}") depth -= 1;
1081
- if (c === "," && depth === 0) {
1082
- parts.push(buf);
1083
- buf = "";
1084
- continue;
1079
+ const 引用符を見て割る = (見る: boolean): { parts: string[]; 閉じた: boolean } => {
1080
+ let depth = 0;
1081
+ let 引用符: '"' | "'" | null = null;
1082
+ let 直前: string | null = null;
1083
+ let buf = "";
1084
+ const parts: string[] = [];
1085
+ for (let i = 0; i < inner.length; i += 1) {
1086
+ const c = inner[i]!;
1087
+ if (見る) {
1088
+ if (引用符 !== null) {
1089
+ // 二重引用の中では、逃がした引用符を終端と取り違えない
1090
+ if (引用符 === '"' && c === "\\" && i + 1 < inner.length) {
1091
+ buf += c;
1092
+ i += 1;
1093
+ buf += inner[i]!;
1094
+ continue;
1095
+ }
1096
+ // **開いた記号と同じものだけが閉じる**。 `"Guns N' Roses"` の `'` は文字として残す
1097
+ if (c === 引用符) 引用符 = null;
1098
+ buf += c;
1099
+ continue;
1100
+ }
1101
+ // 値の途中の apostrophe まで開始記号にすると、別 field まで引用内として飲み込む
1102
+ if ((c === '"' || c === "'") && (直前 === null || ":,{[".includes(直前))) {
1103
+ 引用符 = c;
1104
+ 直前 = c;
1105
+ buf += c;
1106
+ continue;
1107
+ }
1108
+ }
1109
+ if (c === "[" || c === "{") depth += 1;
1110
+ else if (c === "]" || c === "}") depth -= 1;
1111
+ if (c === "," && depth === 0) {
1112
+ parts.push(buf);
1113
+ buf = "";
1114
+ 直前 = c;
1115
+ continue;
1116
+ }
1117
+ buf += c;
1118
+ if (c !== " " && c !== "\t") 直前 = c;
1085
1119
  }
1086
- buf += c;
1087
- }
1088
- if (buf.trim()) parts.push(buf);
1089
- return parts;
1120
+ if (buf.trim()) parts.push(buf);
1121
+ return { parts, 閉じた: 引用符 === null };
1122
+ };
1123
+
1124
+ const 見た = 引用符を見て割る(true);
1125
+ // **閉じない引用符が残ったらこれまでどおりに割る** (#1367)。 壊れた入力で挙動が変わると、
1126
+ // 今まで通っていた書き方が黙って別の結果になる。 実測では見本 9832 件の中括弧のうち
1127
+ // 閉じない形は 0 件で、この経路は保険
1128
+ return 見た.閉じた ? 見た.parts : 引用符を見て割る(false).parts;
1090
1129
  }
1091
1130
 
1092
1131
  function collectIndentedList(
@@ -2174,6 +2213,18 @@ function ensureAnimate(a: DslAnimate | undefined, lineNo: number): DslAnimate {
2174
2213
  return { states: [], phases: [], pos: { line: lineNo } };
2175
2214
  }
2176
2215
 
2216
+ /**
2217
+ * 段に書ける項目の英語名。 `parsePhase` が受け付ける名前の入口でも使う。
2218
+ *
2219
+ * 誤りの案内で使うほか、**見本が値の名前にこの語を使っていないか** を見るのにも使う
2220
+ * (#1330)。 段の項目と値は階層が違うので衝突しないが、同じ語が 2 つの意味で並ぶと
2221
+ * 見本を読む人が階層から意味を判断することになる。
2222
+ */
2223
+ export const PHASE_ITEM_WORDS = ["focus", "badge", "body", "description", "tween", "set", "draw"] as const;
2224
+
2225
+ const 段の項目の英語 = PHASE_ITEM_WORDS;
2226
+ const 段の項目の集合: ReadonlySet<string> = new Set(PHASE_ITEM_WORDS);
2227
+
2177
2228
  function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
2178
2229
  // block[0]!: `step: "request" 1.5s`
2179
2230
  const head = block[0]!;
@@ -2220,6 +2271,17 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
2220
2271
  }
2221
2272
  const key = (propMatch[1] ?? "").toLowerCase();
2222
2273
  const value = (propMatch[2] ?? "").trim();
2274
+ // 一覧を parse の入口にも使う。 分岐だけを増やして一覧を更新し忘れると、見本の名前との
2275
+ // 重なり検査が新しい項目を見落とすため、一覧にない語は各分岐へ到達させない。
2276
+ if (!段の項目の集合.has(key)) {
2277
+ errors.push({
2278
+ line: ln.no,
2279
+ message: `段の項目名が読めません: "${propMatch[1] ?? ""}"`,
2280
+ hint: 段の項目のヒント(propMatch[1] ?? ""),
2281
+ });
2282
+ i += 1;
2283
+ continue;
2284
+ }
2223
2285
  if (key === "focus") {
2224
2286
  phase.highlight = parseFocusList(value);
2225
2287
  i += 1;
@@ -2304,8 +2366,8 @@ function parsePhase(block: Line[], errors: DslError[]): DslPhase | null {
2304
2366
  i = j;
2305
2367
  continue;
2306
2368
  }
2307
- // ここに来るのは上のどれにも当たらなかった名前 (#1301)。 黙って捨てると
2308
- // 「書いたのに段が変わらない」 が手掛かりなしで起きる
2369
+ // ここに来るのは一覧へ語を足したのに処理分岐を足していない時だけ。 黙って捨てると
2370
+ // 「一覧にはあるのに段が変わらない」 が手掛かりなしで起きる。
2309
2371
  errors.push({
2310
2372
  line: ln.no,
2311
2373
  message: `段の項目名が読めません: "${propMatch[1] ?? ""}"`,
@@ -2329,9 +2391,6 @@ function 段の項目のヒント(書いた名前: string): string {
2329
2391
  : `使える項目 = ${段の項目の英語.join(", ")}`;
2330
2392
  }
2331
2393
 
2332
- /** 段に書ける項目の英語名。 `parsePhase` の分岐から導く一覧 */
2333
- const 段の項目の英語 = ["focus", "badge", "body", "description", "tween", "set", "draw"] as const;
2334
-
2335
2394
  /**
2336
2395
  * `draw:` に書ける語と、その語が効く図種 (#1312 / #1314)。
2337
2396
  *