@cardenelabs/dragon 0.7.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/index.cjs +4916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1451 -0
- package/dist/index.d.ts +1451 -0
- package/dist/index.js +4870 -0
- package/dist/index.js.map +1 -0
- package/examples/quick-start.md +142 -0
- package/package.json +79 -0
- package/src/canvas-bounds.ts +52 -0
- package/src/color.ts +172 -0
- package/src/compile.ts +3739 -0
- package/src/focus.ts +46 -0
- package/src/index.ts +226 -0
- package/src/input-size.ts +154 -0
- package/src/json-parser.ts +434 -0
- package/src/keywords.ts +114 -0
- package/src/notation-lint.ts +304 -0
- package/src/parser.ts +354 -0
- package/src/relative-pos.ts +182 -0
- package/src/schema.ts +24 -0
- package/src/schemas/diagram.json +133 -0
- package/src/types.ts +294 -0
- package/src/v05/index.ts +9 -0
- package/src/v05/parser.ts +1678 -0
- package/src/write-position.ts +270 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 登場人物 1 人の位置を DSL 本文に書き込む。
|
|
3
|
+
*
|
|
4
|
+
* editor の画面で見えている座標を、 そのまま記法に落とすために使う。 書く人は図を見ながら
|
|
5
|
+
* 数値を決められないので、 今の位置を出発点として渡す経路が要る。
|
|
6
|
+
*
|
|
7
|
+
* 記法を知っているのは本 package なので、 本文の書き換えもここに置く。 画面側に置くと
|
|
8
|
+
* 記法が増えた時に画面側だけが取り残される。
|
|
9
|
+
*
|
|
10
|
+
* 書き方は 4 通りあり、 元の形を保ったまま位置だけを差し替える。 形を勝手に揃えると、
|
|
11
|
+
* 書いた人の見た目が変わって差分が読めなくなる。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 位置の書き込み結果。 対象が見つからなければ null。 */
|
|
15
|
+
export function writeActorPosition(
|
|
16
|
+
src: string,
|
|
17
|
+
actorName: string,
|
|
18
|
+
posX: number,
|
|
19
|
+
posY: number,
|
|
20
|
+
): string | null {
|
|
21
|
+
if (!Number.isFinite(posX) || !Number.isFinite(posY)) return null;
|
|
22
|
+
const x = Math.round(posX);
|
|
23
|
+
const y = Math.round(posY);
|
|
24
|
+
|
|
25
|
+
// 改行コードは行ごとに元のまま残す。 buffer 全体で一括判定すると、 混在した本文で
|
|
26
|
+
// 無関係な行の改行まで書き換わって差分が変更箇所の外に広がる
|
|
27
|
+
const seg = src.split(/(\r\n|\n)/);
|
|
28
|
+
const lineAt = (i: number): string => seg[i * 2] ?? "";
|
|
29
|
+
const lineCount = Math.ceil(seg.length / 2);
|
|
30
|
+
|
|
31
|
+
// 探すのは `actors:` の中だけ。 全文を走ると、 別の項目に同じ名前で並ぶ行
|
|
32
|
+
// (`notes:` の下の `- API: service` 等) を先に掴んで書き換える (実測)
|
|
33
|
+
const span = actorsSpan(lineAt, lineCount);
|
|
34
|
+
if (!span) return null;
|
|
35
|
+
|
|
36
|
+
for (let i = span.from; i < span.to; i += 1) {
|
|
37
|
+
const head = matchActorHead(lineAt(i));
|
|
38
|
+
if (!head || head.name !== actorName) continue;
|
|
39
|
+
|
|
40
|
+
// 1. `- API: { kind: service }` = 中括弧の中に座標を入れる
|
|
41
|
+
if (head.rest.startsWith("{") && head.rest.endsWith("}")) {
|
|
42
|
+
seg[i * 2] = `${head.prefix}{ ${upsertInlineFields(head.rest.slice(1, -1), x, y)} }`;
|
|
43
|
+
return seg.join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. `- API:` だけの行 = 続く字下げ行に `位置:` を入れる
|
|
47
|
+
if (head.hasColon && head.rest === "") {
|
|
48
|
+
return writeVerticalForm(seg, i, span.to, head.indent, x, y);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. `- API: service` = 値の並びに `@x,y` を入れる
|
|
52
|
+
if (head.hasColon) {
|
|
53
|
+
seg[i * 2] = `${head.prefix}${upsertAtToken(head.rest, x, y)}`;
|
|
54
|
+
return seg.join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4. `- API` = 名前だけ。 値を書ける形にしてから座標を足す
|
|
58
|
+
seg[i * 2] = `${head.indentText}- ${head.rawName}: @${x},${y}`;
|
|
59
|
+
return seg.join("");
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `actors:` block の行範囲。 見つからなければ null。
|
|
66
|
+
*
|
|
67
|
+
* 開始は行頭の `actors:` に限る。 終わりは次の行頭項目 (字下げなしの `key:`) の直前。
|
|
68
|
+
*
|
|
69
|
+
* `actors:` が複数ある本文では最後のものを返す。 記法側は同じ項目が 2 度現れると後の方で
|
|
70
|
+
* 上書きするため、 最初のものを見ると parser が採らない箱を書き換えることになる
|
|
71
|
+
* (実測 = 後の block に居る箱の書き戻しが `null` になった)。
|
|
72
|
+
*/
|
|
73
|
+
function actorsSpan(
|
|
74
|
+
lineAt: (i: number) => string,
|
|
75
|
+
lineCount: number,
|
|
76
|
+
): { from: number; to: number } | null {
|
|
77
|
+
let from = -1;
|
|
78
|
+
let to = lineCount;
|
|
79
|
+
for (let i = 0; i < lineCount; i += 1) {
|
|
80
|
+
const line = lineAt(i);
|
|
81
|
+
if (/^actors[ \t]*:[ \t]*$/.test(line)) {
|
|
82
|
+
from = i + 1;
|
|
83
|
+
to = lineCount;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// 字下げのない `key:` は次の項目の始まり。 直前の block はそこで閉じる
|
|
87
|
+
if (from >= 0 && to === lineCount && /^[^\s#][^:]*:/.test(line)) to = i;
|
|
88
|
+
}
|
|
89
|
+
return from < 0 ? null : { from, to };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type ActorHead = {
|
|
93
|
+
/** 行頭から `:` までをそのまま保つ。 元の空白の入れ方を壊さないために持つ */
|
|
94
|
+
prefix: string;
|
|
95
|
+
indentText: string;
|
|
96
|
+
indent: number;
|
|
97
|
+
rawName: string;
|
|
98
|
+
name: string;
|
|
99
|
+
hasColon: boolean;
|
|
100
|
+
rest: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* ` - API: service` の形を、 前置き / 名前 / 残りに分ける。
|
|
105
|
+
*
|
|
106
|
+
* `:` を任意にした 1 本の正規表現では書けない。 名前の部分を控えめに取ると `A` で切って
|
|
107
|
+
* 残りを `PI: service` と読んでしまう (実測)。 `:` のある形を先に当てる。
|
|
108
|
+
*/
|
|
109
|
+
function matchActorHead(line: string): ActorHead | null {
|
|
110
|
+
const NAME = String.raw`"(?:[^"\\]|\\.)*"|[^:\s][^:]*?`;
|
|
111
|
+
const withColon = line.match(new RegExp(String.raw`^(\s*)-[ \t]+(${NAME})[ \t]*:[ \t]*(.*)$`));
|
|
112
|
+
if (withColon) {
|
|
113
|
+
const [, indentText = "", rawName = "", rest = ""] = withColon;
|
|
114
|
+
return {
|
|
115
|
+
// 元の空白の入れ方を壊さないよう、 行頭から値の直前までをそのまま持つ
|
|
116
|
+
prefix: line.slice(0, line.length - rest.length),
|
|
117
|
+
indentText,
|
|
118
|
+
indent: indentText.length,
|
|
119
|
+
rawName,
|
|
120
|
+
name: unquote(rawName),
|
|
121
|
+
hasColon: true,
|
|
122
|
+
rest: rest.trimEnd(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const nameOnly = line.match(new RegExp(String.raw`^(\s*)-[ \t]+("(?:[^"\\]|\\.)*"|\S+)[ \t]*$`));
|
|
126
|
+
if (!nameOnly) return null;
|
|
127
|
+
const [, indentText = "", rawName = ""] = nameOnly;
|
|
128
|
+
return {
|
|
129
|
+
prefix: line,
|
|
130
|
+
indentText,
|
|
131
|
+
indent: indentText.length,
|
|
132
|
+
rawName,
|
|
133
|
+
name: unquote(rawName),
|
|
134
|
+
hasColon: false,
|
|
135
|
+
rest: "",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function unquote(raw: string): string {
|
|
140
|
+
if (!raw.startsWith('"') || !raw.endsWith('"') || raw.length < 2) return raw;
|
|
141
|
+
return raw.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 空白区切りの値の並びに `@x,y` を差し込む。 既に書かれていれば差し替える。
|
|
146
|
+
*
|
|
147
|
+
* 引用符の中は触らない。 補足 (`"@1,2 の話"`) を座標と取り違えないため。
|
|
148
|
+
*/
|
|
149
|
+
function upsertAtToken(rest: string, x: number, y: number): string {
|
|
150
|
+
const kept = splitOutsideQuotes(rest).filter((t) => !/^@-?\d+(\.\d+)?\s*[,、]\s*-?\d+(\.\d+)?$/.test(t));
|
|
151
|
+
kept.push(`@${x},${y}`);
|
|
152
|
+
return kept.join(" ");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 引用符と角括弧の外の空白で区切る。 */
|
|
156
|
+
function splitOutsideQuotes(s: string): string[] {
|
|
157
|
+
const out: string[] = [];
|
|
158
|
+
let buf = "";
|
|
159
|
+
let quote: '"' | "'" | null = null;
|
|
160
|
+
let depth = 0;
|
|
161
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
162
|
+
const c = s[i]!;
|
|
163
|
+
if (quote) {
|
|
164
|
+
buf += c;
|
|
165
|
+
if (quote === '"' && c === "\\") {
|
|
166
|
+
buf += s[i + 1] ?? "";
|
|
167
|
+
i += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (c === quote) quote = null;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (c === '"' || c === "'") {
|
|
174
|
+
quote = c;
|
|
175
|
+
buf += c;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (c === "[") depth += 1;
|
|
179
|
+
if (c === "]") depth -= 1;
|
|
180
|
+
if (/\s/.test(c) && depth === 0) {
|
|
181
|
+
if (buf) out.push(buf);
|
|
182
|
+
buf = "";
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
buf += c;
|
|
186
|
+
}
|
|
187
|
+
if (buf) out.push(buf);
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 中括弧の中の `posX` / `posY` を差し替える。 他の項目は順番ごと残す。 */
|
|
192
|
+
function upsertInlineFields(inner: string, x: number, y: number): string {
|
|
193
|
+
const kept = splitTopLevel(inner).filter((f) => {
|
|
194
|
+
const key = f.slice(0, f.indexOf(":")).trim();
|
|
195
|
+
return key !== "posX" && key !== "posY";
|
|
196
|
+
});
|
|
197
|
+
kept.push(`posX: ${x}`, `posY: ${y}`);
|
|
198
|
+
return kept.join(", ");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 中括弧と角括弧の深さ 0 の `,` で区切る。 入れ子の中の項目を top-level と取り違えない。 */
|
|
202
|
+
function splitTopLevel(inner: string): string[] {
|
|
203
|
+
const out: string[] = [];
|
|
204
|
+
let depth = 0;
|
|
205
|
+
let start = 0;
|
|
206
|
+
let quote: '"' | "'" | null = null;
|
|
207
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
208
|
+
const c = inner[i]!;
|
|
209
|
+
if (quote) {
|
|
210
|
+
if (quote === '"' && c === "\\") {
|
|
211
|
+
i += 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (c === quote) quote = null;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (c === '"' || c === "'") quote = c;
|
|
218
|
+
else if (c === "{" || c === "[") depth += 1;
|
|
219
|
+
else if (c === "}" || c === "]") depth -= 1;
|
|
220
|
+
else if (c === "," && depth === 0) {
|
|
221
|
+
out.push(inner.slice(start, i));
|
|
222
|
+
start = i + 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
out.push(inner.slice(start));
|
|
226
|
+
return out.map((f) => f.trim()).filter((f) => f.length > 0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 縦に並べた形に `位置:` を入れる。
|
|
231
|
+
*
|
|
232
|
+
* 既にあれば値だけ差し替える。 無ければ最後の項目の次に足す。 字下げは既にある項目に
|
|
233
|
+
* 合わせ、 項目が 1 つも無ければ頭の字下げに 4 を足す。
|
|
234
|
+
*
|
|
235
|
+
* 差し替える対象は登場人物の直下の項目だけ。 深さを見ないと、 入れ子の中の `位置:`
|
|
236
|
+
* (`nodes: → header: → 位置:`) を掴んで書き換える (実測)。
|
|
237
|
+
*/
|
|
238
|
+
function writeVerticalForm(
|
|
239
|
+
seg: string[],
|
|
240
|
+
headIdx: number,
|
|
241
|
+
lineCount: number,
|
|
242
|
+
headIndent: number,
|
|
243
|
+
x: number,
|
|
244
|
+
y: number,
|
|
245
|
+
): string {
|
|
246
|
+
let lastChild = -1;
|
|
247
|
+
let childIndent = -1;
|
|
248
|
+
for (let j = headIdx + 1; j < lineCount; j += 1) {
|
|
249
|
+
const line = seg[j * 2] ?? "";
|
|
250
|
+
if (line.trim() === "") continue;
|
|
251
|
+
const indent = line.length - line.trimStart().length;
|
|
252
|
+
if (indent <= headIndent) break;
|
|
253
|
+
if (childIndent < 0) childIndent = indent;
|
|
254
|
+
lastChild = j;
|
|
255
|
+
// 位置 / pos の行が既にあれば、 その行の値だけ差し替える。 直下の深さのものだけを見る
|
|
256
|
+
const key = indent === childIndent ? line.trim().match(/^(位置|pos)\s*:/) : null;
|
|
257
|
+
if (key) {
|
|
258
|
+
seg[j * 2] = `${" ".repeat(indent)}${key[1]}: ${x},${y}`;
|
|
259
|
+
return seg.join("");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const indent = childIndent >= 0 ? childIndent : headIndent + 4;
|
|
263
|
+
const newLine = `${" ".repeat(indent)}位置: ${x},${y}`;
|
|
264
|
+
const insertAt = (lastChild >= 0 ? lastChild : headIdx) + 1;
|
|
265
|
+
// 挿入位置の直前で実際に使われている改行に合わせる。 末尾に足す時は最後の区切りを見る
|
|
266
|
+
const sep = seg[insertAt * 2 - 1] ?? seg[seg.length - 2] ?? "\n";
|
|
267
|
+
if (insertAt * 2 >= seg.length) seg.push(sep, newLine);
|
|
268
|
+
else seg.splice(insertAt * 2, 0, newLine, sep);
|
|
269
|
+
return seg.join("");
|
|
270
|
+
}
|