@cardenelabs/dragon 0.35.0 → 0.36.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/README.md +41 -3
- package/dist/cli.cjs +597 -375
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +597 -375
- package/dist/cli.js.map +1 -1
- package/dist/export.cjs +104 -37
- package/dist/export.cjs.map +1 -1
- package/dist/export.d.cts +1 -1
- package/dist/export.d.ts +1 -1
- package/dist/export.js +104 -37
- package/dist/export.js.map +1 -1
- package/dist/index.cjs +833 -526
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -15
- package/dist/index.d.ts +21 -15
- package/dist/index.js +833 -526
- package/dist/index.js.map +1 -1
- package/dist/player.global.js +2 -2
- package/dist/{schema-BBWfniwH.d.cts → schema-9VLameDG.d.cts} +15 -3
- package/dist/{schema-BBWfniwH.d.ts → schema-9VLameDG.d.ts} +15 -3
- package/package.json +20 -8
- package/src/cli.ts +13 -14
- package/src/figure-text.ts +1 -1
- package/src/index.ts +4 -4
- package/src/look/values.ts +16 -4
- package/src/messages.ts +158 -80
- package/src/parser.ts +402 -245
- package/src/player.ts +80 -9
- package/src/printer.ts +90 -60
- package/src/render/parts.ts +28 -8
- package/src/render/svg.ts +128 -33
- package/src/schema.ts +1 -1
- package/src/tokenize.ts +11 -8
- package/src/view.ts +5 -2
- package/src/words.ts +39 -65
package/src/player.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* 図そのものは読み上げから隠して同じ中身を 2 度読ませない。
|
|
7
7
|
* 見せ方は枠の幅で選び (`view.ts`)、寄って追う時は窓と全体図で描き直す。
|
|
8
8
|
* 釦と段の一覧の読み上げの名前と全体図の札は、渡した言葉 (`options.lang`、書かなければ日本語) で出す (#3001)。
|
|
9
|
+
* 段が替わるたびに `options.onStep` で今の段を知らせ、頁の側が段の札や進み具合を出せるようにする (#3031)。
|
|
10
|
+
* 段の本文の帯は本文の最も長い段の高さに保ち、段を送っても図の大きさと位置を変えない (#3049)。
|
|
11
|
+
* 帯の中身の幅が `LOOK.player.narrow` より狭い時は、段を送る釦を指で押せる大きさにして段の番号と上の段に並べ、
|
|
12
|
+
* 題と本文をその下の段の全幅に置く (#3057、#3069)。
|
|
9
13
|
*/
|
|
10
14
|
import { FIGURE_TEXT } from "./figure-text";
|
|
11
15
|
import { FONT_VARS, LOOK, paletteVars, resolvePalette, type TextStyle, TYPE } from "./look/values";
|
|
@@ -13,6 +17,16 @@ import { drawSvg, type RenderOptions, renderDiagram } from "./render/svg";
|
|
|
13
17
|
import type { Span } from "./timeline";
|
|
14
18
|
import { chooseView } from "./view";
|
|
15
19
|
|
|
20
|
+
export interface PlayerOptions extends RenderOptions {
|
|
21
|
+
/** 段が替わるたびに今の段 (0 から) を受け取る。置いた時にも最初の段で 1 度呼ぶ。頁の側で段の札や進み具合を出す時に使う (#3031) */
|
|
22
|
+
onStep?: (step: number) => void;
|
|
23
|
+
/**
|
|
24
|
+
* 図を、図の枠の中で見える高さに収める (#3047)。高さに収めると字が読めない図は寄って追う図にし、寄って追う窓と全体図をその高さに収める。
|
|
25
|
+
* 枠の高さが決まっている置き場 (カタログの拡大の重ね窓) で使う。枠の高さが図に合わせて伸びる置き場で使うと、描くたびに低くなる
|
|
26
|
+
*/
|
|
27
|
+
fitHeight?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
export interface Player {
|
|
17
31
|
/** 今の段 (0 から) */
|
|
18
32
|
readonly step: number;
|
|
@@ -34,15 +48,37 @@ function playerCss(): string {
|
|
|
34
48
|
return [
|
|
35
49
|
`.dg-player{display:flex;flex-direction:column;gap:${px(P.gap)};color:var(--ink)}`,
|
|
36
50
|
`.dg-player .dg-frame{border-radius:${px(P.radius)};overflow:hidden}`,
|
|
37
|
-
`.dg-
|
|
51
|
+
// 並べ方は帯の中身の幅で選ぶ。帯の中の 1 段 (`.dg-row`) が並べ替わり、帯の左右の余白は置いた頁が帯に付ける
|
|
52
|
+
`.dg-bar{container:dg-bar/inline-size}`,
|
|
53
|
+
`.dg-row{display:flex;align-items:flex-start;gap:${px(P.gap)}}`,
|
|
38
54
|
`.dg-btns{display:flex;gap:${px(P.gap / 2)};flex:none}`,
|
|
39
55
|
`.dg-btn{width:${px(P.button)};height:${px(P.button)};border-radius:${px(P.radius)};border:${px(LOOK.stroke.hair)} solid var(--hair);background:var(--surf);color:var(--ink);${font(TYPE.button)};cursor:pointer}`,
|
|
40
56
|
`.dg-btn:focus-visible{outline:${px(P.focus)} solid var(--c-blue);outline-offset:${px(P.focus)}}`,
|
|
41
57
|
`.dg-say{display:flex;gap:${px(P.gap)};min-width:0}`,
|
|
42
58
|
`.dg-no{${font(TYPE.stepNo)};color:var(--mute);padding-top:${px(P.focus)}}`,
|
|
59
|
+
`.dg-txt{display:grid;grid-template-columns:minmax(0,1fr);min-width:0}`,
|
|
60
|
+
`.dg-txt>*{grid-area:1/1}`,
|
|
61
|
+
`.dg-room{visibility:hidden}`,
|
|
43
62
|
`.dg-tt{${font(TYPE.stepTitle)};margin:0}`,
|
|
44
63
|
`.dg-tb{${font(TYPE.stepBody)};color:var(--ink2);margin:0;white-space:pre-line}`,
|
|
45
64
|
`.dg-sr{position:absolute;width:${hidden};height:${hidden};margin:-${hidden};padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}`,
|
|
65
|
+
// 狭い帯 (携帯の幅) では、段を送る釦を指で押せる大きさにし、その横に番号を置き、
|
|
66
|
+
// 題を 2 段目、本文を 3 段目に置いてどちらも帯の全幅を使う (#3057、#3069)。
|
|
67
|
+
// 釦を大きくすると横に題を置く幅が足りず題が細かく折れるため、題は釦の段に置かない。
|
|
68
|
+
// 題と本文を包む箱を外して写しも同じ升に重ねるので、2 段目は最も高い題、3 段目は最も長い本文の高さになる。
|
|
69
|
+
// 題と本文の間は広い帯と同じく空けず、釦の段の下にだけ間を取って、帯が高くなる分を抑える
|
|
70
|
+
`@container dg-bar (width < ${px(P.narrow)}){` +
|
|
71
|
+
`.dg-row{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:start;gap:0 ${px(P.gap)}}` +
|
|
72
|
+
`.dg-say,.dg-txt,.dg-txt>*{display:contents}` +
|
|
73
|
+
`.dg-btns{grid-area:1/1;margin-bottom:${px(P.gap / 2)}}` +
|
|
74
|
+
`.dg-btn{width:${px(P.touch)};height:${px(P.touch)}}` +
|
|
75
|
+
`.dg-no{grid-area:1/2;display:flex;align-items:center;min-height:${px(P.touch)};padding-top:0}` +
|
|
76
|
+
`.dg-tt{grid-area:2/1/3/3}` +
|
|
77
|
+
`.dg-tb{grid-area:3/1/4/3}` +
|
|
78
|
+
`}`,
|
|
79
|
+
// 指で触る時は、広い帯でも段を送る釦を指で押せる大きさにする (#3076)。並びは横 1 列のまま変えない。
|
|
80
|
+
// 広い帯は本文が 2 行あれば帯の高さが変わらず、本文の無い帯だけが釦の分だけ高くなる
|
|
81
|
+
`@media (pointer: coarse){.dg-btn{width:${px(P.touch)};height:${px(P.touch)}}}`,
|
|
46
82
|
].join("");
|
|
47
83
|
}
|
|
48
84
|
|
|
@@ -61,7 +97,7 @@ const pad = (n: number): string => String(n).padStart(2, "0");
|
|
|
61
97
|
/** 段の番号の札 (`01/12`)。`step` は 0 から数える。 */
|
|
62
98
|
export const stepNo = (step: number, count: number): string => `${pad(step + 1)}/${pad(count)}`;
|
|
63
99
|
|
|
64
|
-
export function mountDiagram(host: HTMLElement, diagram: unknown, options:
|
|
100
|
+
export function mountDiagram(host: HTMLElement, diagram: unknown, options: PlayerOptions = {}): Player {
|
|
65
101
|
const doc = host.ownerDocument;
|
|
66
102
|
const win = doc.defaultView;
|
|
67
103
|
// 描く前に読む。描いた後に読むと、動きを嫌う人にも一瞬だけ流れてしまう
|
|
@@ -108,17 +144,41 @@ export function mountDiagram(host: HTMLElement, diagram: unknown, options: Rende
|
|
|
108
144
|
no.className = "dg-no";
|
|
109
145
|
no.dataset.dragon = "step-no";
|
|
110
146
|
const text = doc.createElement("div");
|
|
147
|
+
text.className = "dg-txt";
|
|
111
148
|
const title = doc.createElement("p");
|
|
112
149
|
title.className = "dg-tt";
|
|
113
150
|
title.dataset.dragon = "step-title";
|
|
114
151
|
const body = doc.createElement("p");
|
|
115
152
|
body.className = "dg-tb";
|
|
116
153
|
body.dataset.dragon = "step-body";
|
|
117
|
-
|
|
154
|
+
const live = doc.createElement("div");
|
|
155
|
+
live.append(title, body);
|
|
156
|
+
text.append(live);
|
|
157
|
+
// 全部の段の題と本文の写しを今の段と同じ升に重ね、見た目からは隠して場所だけを取る。
|
|
158
|
+
// 帯の高さが本文の最も長い段で決まり、段を送っても図の枠が伸び縮みしない (#3049)
|
|
159
|
+
for (const step of steps) {
|
|
160
|
+
const room = doc.createElement("div");
|
|
161
|
+
room.className = "dg-room";
|
|
162
|
+
room.dataset.dragon = "caption-room";
|
|
163
|
+
const roomTitle = doc.createElement("p");
|
|
164
|
+
roomTitle.className = "dg-tt";
|
|
165
|
+
roomTitle.textContent = step.title;
|
|
166
|
+
const roomBody = doc.createElement("p");
|
|
167
|
+
roomBody.className = "dg-tb";
|
|
168
|
+
roomBody.textContent = step.body;
|
|
169
|
+
room.append(roomTitle, roomBody);
|
|
170
|
+
text.append(room);
|
|
171
|
+
}
|
|
118
172
|
say.append(no, text);
|
|
173
|
+
// 狭い帯では語りの箱を外して番号と題と本文を釦と同じ升の並びに置く (#3057)。
|
|
174
|
+
// 箱を外した要素の読み上げからの隠し方に頼らず、見える語りの 1 つずつも読み上げから隠す
|
|
175
|
+
for (const shown of [no, title, body]) shown.setAttribute("aria-hidden", "true");
|
|
176
|
+
const row = doc.createElement("div");
|
|
177
|
+
row.className = "dg-row";
|
|
178
|
+
row.append(buttons, say);
|
|
119
179
|
const bar = doc.createElement("div");
|
|
120
180
|
bar.className = "dg-bar";
|
|
121
|
-
bar.append(
|
|
181
|
+
bar.append(row);
|
|
122
182
|
|
|
123
183
|
// 読み上げの道具が段の順に読む一覧。見た目からは外すが、読み上げからは隠さない
|
|
124
184
|
const list = doc.createElement("ol");
|
|
@@ -142,8 +202,16 @@ export function mountDiagram(host: HTMLElement, diagram: unknown, options: Rende
|
|
|
142
202
|
host.append(root);
|
|
143
203
|
|
|
144
204
|
const frameWidth = (): number => frame.clientWidth || host.clientWidth || rendered.layout.width;
|
|
145
|
-
|
|
205
|
+
/** 高さに収めると頼まれた時だけ、図の枠の中で見える高さ (上下の余白を除く)。測れない時は `undefined` で、幅だけで決める */
|
|
206
|
+
const frameHeight = (): number | undefined => {
|
|
207
|
+
if (!options.fitHeight || !win) return undefined;
|
|
208
|
+
const style = win.getComputedStyle(frame);
|
|
209
|
+
const inner = frame.clientHeight - (Number.parseFloat(style.paddingTop) || 0) - (Number.parseFloat(style.paddingBottom) || 0);
|
|
210
|
+
return inner > 0 ? inner : undefined;
|
|
211
|
+
};
|
|
212
|
+
let mode = chooseView(rendered, frameWidth(), frameHeight()).mode;
|
|
146
213
|
let width = frameWidth();
|
|
214
|
+
let height = frameHeight();
|
|
147
215
|
let svg: SVGSVGElement | null = null;
|
|
148
216
|
let step = 0;
|
|
149
217
|
let playing = !reduce;
|
|
@@ -172,7 +240,7 @@ export function mountDiagram(host: HTMLElement, diagram: unknown, options: Rende
|
|
|
172
240
|
svg = next;
|
|
173
241
|
born = Date.now();
|
|
174
242
|
};
|
|
175
|
-
const markup = (): string => (mode === "follow" ? drawSvg(rendered.diagram, rendered.layout, timeline, palette, rendered.theme, { width, lang }) : rendered.svg);
|
|
243
|
+
const markup = (): string => (mode === "follow" ? drawSvg(rendered.diagram, rendered.layout, timeline, palette, rendered.theme, { width, ...(height === undefined ? {} : { height }), lang }) : rendered.svg);
|
|
176
244
|
|
|
177
245
|
const now = (): number => (playing ? (at + (Date.now() - born) / 1000) % timeline.total : at);
|
|
178
246
|
const stepAt = (t: number): number => {
|
|
@@ -189,6 +257,7 @@ export function mountDiagram(host: HTMLElement, diagram: unknown, options: Rende
|
|
|
189
257
|
body.textContent = current?.body ?? "";
|
|
190
258
|
toggleButton.setAttribute("aria-label", playing ? words.pause : words.play);
|
|
191
259
|
toggleButton.textContent = playing ? BUTTON_TEXT.pause : BUTTON_TEXT.play;
|
|
260
|
+
options.onStep?.(step);
|
|
192
261
|
};
|
|
193
262
|
const restart = (t: number, play: boolean): void => {
|
|
194
263
|
at = t;
|
|
@@ -227,16 +296,18 @@ export function mountDiagram(host: HTMLElement, diagram: unknown, options: Rende
|
|
|
227
296
|
toggleButton.addEventListener("click", onToggle);
|
|
228
297
|
frame.addEventListener("click", onFigure);
|
|
229
298
|
|
|
230
|
-
//
|
|
299
|
+
// 枠の大きさが変わったら見せ方を選び直す。寄って追う窓は幅 (高さに収める時は高さも) で決まるので、変われば描き直す
|
|
231
300
|
const Observer = (win as (Window & { ResizeObserver?: typeof ResizeObserver }) | null)?.ResizeObserver;
|
|
232
301
|
const observer = Observer
|
|
233
302
|
? new Observer(() => {
|
|
234
303
|
const w = frameWidth();
|
|
235
|
-
const
|
|
236
|
-
|
|
304
|
+
const h = frameHeight();
|
|
305
|
+
const chosen = chooseView(rendered, w, h).mode;
|
|
306
|
+
if (chosen === mode && (mode === "fixed" || (w === width && h === height))) return;
|
|
237
307
|
const t = now();
|
|
238
308
|
mode = chosen;
|
|
239
309
|
width = w;
|
|
310
|
+
height = h;
|
|
240
311
|
at = t;
|
|
241
312
|
place(markup());
|
|
242
313
|
})
|
package/src/printer.ts
CHANGED
|
@@ -1,76 +1,106 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 図の原本の JSON
|
|
2
|
+
* 図の原本の JSON を、人が書く dragon記法へ書き戻す (#3137)。
|
|
3
3
|
*
|
|
4
4
|
* 並べ方は決まった形に整える。JSON の欄の並び順は見ず、欄の名前で読むので、
|
|
5
5
|
* 同じ内容の JSON からは毎回同じ文字列になる。書き手の行の並べ方とコメントは保たない。
|
|
6
6
|
* 段の中で起きることは JSON の並び順 (起きる順) のまま書く。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* `"` で囲むのは要る時だけにする。`[` を含む名前と、`,` か `]` を含む属性の値と、`"` で始まる字を囲む。
|
|
9
|
+
* 箱は JSON の並びのまま書き、同じ区画の箱が続く所を 1 つの `group … end` にする。
|
|
10
|
+
* 区画の箱が飛び飛びの時は同じ `group` を開き直す。区画の並びが箱の現れる順と違う時と、
|
|
11
|
+
* 箱の無い区画がある時は、先に全ての区画を中の無い `group … end` で宣言してから開き直す。
|
|
11
12
|
*/
|
|
12
|
-
import type { Box,
|
|
13
|
-
import { type NotationLang, type WORDS, wordOf } from "./words";
|
|
13
|
+
import type { Box, Diagram, DiagramEvent, Region } from "./schema";
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const quote = (text: string): string => `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
16
|
+
/** 行の終わりか `[` の前に置く名前と文 */
|
|
17
|
+
const name = (text: string): string => (text.includes("[") || text.startsWith('"') ? quote(text) : text);
|
|
18
|
+
/** `:` の前に置く字 (帯の題と、`set` の欄) */
|
|
19
|
+
const key = (text: string): string => (text.includes(":") || text.includes("[") || text.startsWith('"') ? quote(text) : text);
|
|
20
|
+
/** `strip` の行に `,` で並べる欄 */
|
|
21
|
+
const field = (text: string): string => (text.includes(",") || text.startsWith('"') ? quote(text) : text);
|
|
22
|
+
/** 属性の値 */
|
|
23
|
+
const value = (text: string): string => (text.includes(",") || text.includes("]") || text.startsWith('"') ? quote(text) : text);
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** 名前の後ろに `text` を囲んで付ける。`text` が無ければ何も付けない */
|
|
23
|
-
function quoted(name: string, text: string | undefined, lang: NotationLang): string {
|
|
24
|
-
if (text === undefined) return "";
|
|
25
|
-
if (lang === "en" && !name.includes('"') && !text.includes('"')) return ` "${text}"`;
|
|
26
|
-
return `「${text}」`;
|
|
27
|
-
}
|
|
25
|
+
/** 属性の並び。値の無い鍵は書かず、1 つも無ければ何も付けない */
|
|
26
|
+
const attrs = (pairs: [string, string | undefined][]): string => {
|
|
27
|
+
const written = pairs.filter((pair): pair is [string, string] => pair[1] !== undefined);
|
|
28
|
+
return written.length === 0 ? "" : ` [${written.map(([k, v]) => `${k}: ${value(v)}`).join(", ")}]`;
|
|
29
|
+
};
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const attrs = (pairs: [AttrKey, string | undefined][]): string =>
|
|
32
|
-
pairs
|
|
33
|
-
.filter((pair): pair is [AttrKey, string] => pair[1] !== undefined)
|
|
34
|
-
.map(([key, value]) => ` [${wordOf("attr", key, lang)} ${value}]`)
|
|
35
|
-
.join("");
|
|
31
|
+
/** 名札と違う名前だけを `as` の後ろに書く */
|
|
32
|
+
const shown = (id: string, text: string): string => (text === id ? "" : ` as ${name(text)}`);
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
const declareRegion = (r: Region): string =>
|
|
35
|
+
`group ${r.id}${shown(r.id, r.name)}${attrs([
|
|
36
|
+
["note", r.note],
|
|
37
|
+
["color", r.color],
|
|
38
|
+
["icon", r.icon],
|
|
39
|
+
])}`;
|
|
40
|
+
const declareBox = (b: Box): string =>
|
|
41
|
+
`box ${b.id}${shown(b.id, b.name)}${attrs([
|
|
42
|
+
["note", b.sub],
|
|
43
|
+
["color", b.color],
|
|
44
|
+
["icon", b.icon],
|
|
45
|
+
])}`;
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
function event(e: DiagramEvent): string {
|
|
48
|
+
switch (e.type) {
|
|
49
|
+
case "edge": {
|
|
50
|
+
const carry: [string, string][] = e.carry.kind === "tag" ? [["tag", e.carry.text]] : e.carry.kind === "envelope" ? [] : [["carry", e.carry.kind]];
|
|
51
|
+
return `${e.from} -> ${e.to}${e.label === undefined ? "" : `: ${name(e.label)}`}${attrs([...carry, ["side", e.labelSide]])}`;
|
|
52
|
+
}
|
|
53
|
+
case "action":
|
|
54
|
+
return `act ${e.box}: ${e.label}`;
|
|
55
|
+
case "swap":
|
|
56
|
+
return `note ${e.box}: ${e.text}`;
|
|
57
|
+
case "strip":
|
|
58
|
+
return `set ${key(e.field)}${e.value === undefined ? "" : `: ${name(e.value)}`}${attrs([["tag", e.tag]])}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
/** 区画と箱の宣言の塊。塊どうしは空行で区切る */
|
|
63
|
+
function declarations(diagram: Diagram): string[] {
|
|
64
|
+
const regions = diagram.regions ?? [];
|
|
65
|
+
const runs: { region?: string; boxes: Box[] }[] = [];
|
|
66
|
+
for (const b of diagram.boxes) {
|
|
67
|
+
const last = runs.at(-1);
|
|
68
|
+
if (last && last.region === b.region) last.boxes.push(b);
|
|
69
|
+
else runs.push({ ...(b.region !== undefined ? { region: b.region } : {}), boxes: [b] });
|
|
70
|
+
}
|
|
71
|
+
const opened = [...new Set(runs.flatMap((run) => (run.region === undefined ? [] : [run.region])))];
|
|
72
|
+
const inOrder = opened.length === regions.length && opened.every((id, i) => regions[i]?.id === id);
|
|
73
|
+
const declared = new Set<string>();
|
|
74
|
+
const blocks: string[] = [];
|
|
75
|
+
if (!inOrder) {
|
|
76
|
+
for (const r of regions) {
|
|
77
|
+
blocks.push(`${declareRegion(r)}\nend`);
|
|
78
|
+
declared.add(r.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const run of runs) {
|
|
82
|
+
const lines = run.boxes.map(declareBox);
|
|
83
|
+
if (run.region === undefined) {
|
|
84
|
+
blocks.push(lines.join("\n"));
|
|
85
|
+
continue;
|
|
58
86
|
}
|
|
59
|
-
|
|
87
|
+
const region = regions.find((r) => r.id === run.region);
|
|
88
|
+
const open = declared.has(run.region) || !region ? `group ${run.region}` : declareRegion(region);
|
|
89
|
+
declared.add(run.region);
|
|
90
|
+
blocks.push([open, ...lines.map((line) => ` ${line}`), "end"].join("\n"));
|
|
91
|
+
}
|
|
92
|
+
return blocks;
|
|
93
|
+
}
|
|
60
94
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
...step.events.map(event),
|
|
73
|
-
].join("\n"),
|
|
74
|
-
);
|
|
75
|
-
return [`${wordOf("kind", diagram.kind, lang)} ${diagram.title}`, head.join("\n"), ...steps].join("\n\n") + "\n";
|
|
95
|
+
export function printNotation(diagram: Diagram): string {
|
|
96
|
+
const blocks = [`${diagram.kind} ${name(diagram.title)}${attrs([["view", diagram.view]])}`, ...declarations(diagram)];
|
|
97
|
+
if (diagram.strip) blocks.push(`strip ${key(diagram.strip.title)}: ${diagram.strip.fields.map(field).join(", ")}`);
|
|
98
|
+
for (const step of diagram.steps) {
|
|
99
|
+
const body = step.body.split("\n");
|
|
100
|
+
const say = body.length === 1 ? [` say ${step.body}`] : [" say", ...body.map((line) => (line === "" ? "" : ` ${line}`))];
|
|
101
|
+
blocks.push(
|
|
102
|
+
[`step ${step.title}`, ...say, ...(step.code === undefined ? [] : [` code ${step.code}`]), ...step.events.map((e) => ` ${event(e)}`)].join("\n"),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return `${blocks.join("\n\n")}\n`;
|
|
76
106
|
}
|
package/src/render/parts.ts
CHANGED
|
@@ -76,31 +76,51 @@ export function band(region: SceneRegion): string {
|
|
|
76
76
|
/** 箱の下の小さな字の移り変わり。`[時刻, 字, class]` の並びで、最初は元の字。 */
|
|
77
77
|
export type SubChange = [number, string, string];
|
|
78
78
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
const { m, sh } = ctx;
|
|
82
|
-
const { radius, icon, iconRadius, iconScale, iconY, nameY, subY } = LOOK.box;
|
|
79
|
+
/** 箱の下の小さな字。基準線を `y` に置き、起きたことに合わせて書き換わる。 */
|
|
80
|
+
function subLines(box: SceneBox, m: Motion, changes: SubChange[], y: number): string {
|
|
83
81
|
const seq: SubChange[] = [[0, box.sub ?? "", "dg-ss"], ...changes];
|
|
84
|
-
|
|
82
|
+
return seq
|
|
85
83
|
.map(([t, text, cls], k) => {
|
|
86
84
|
const next = seq[k + 1]?.[0];
|
|
87
85
|
const anim = k === 0 ? (next === undefined ? "" : m.until(next - MOTION.untilFade)) : next === undefined ? m.rise(t) : m.rise(t, next - MOTION.untilFade);
|
|
88
86
|
if (text === "") return "";
|
|
89
|
-
return `<g${anim ? ` class="dg-a" style="${anim}"` : ""}><text class="${cls}" x="${r1(box.cx)}" y="${r1(
|
|
87
|
+
return `<g${anim ? ` class="dg-a" style="${anim}"` : ""}><text class="${cls}" x="${r1(box.cx)}" y="${r1(y)}" text-anchor="middle">${esc(text)}</text></g>`;
|
|
90
88
|
})
|
|
91
89
|
.join("");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 箱の地と縁。上端 `y` から高さ `h`。動く段 (`act` の間) は縁に色を付け、外側に淡い光を足す。 */
|
|
93
|
+
function cardFrame(box: SceneBox, ctx: Ctx, act: string | undefined, y: number, h: number): string {
|
|
94
|
+
const rect = (cls: string, extra = ""): string => `<rect class="${cls}" x="${r1(box.x)}" y="${r1(y)}" width="${r1(box.w)}" height="${h}" rx="${LOOK.box.radius}"${extra}/>`;
|
|
95
|
+
return `${act ? `<g class="dg-a" style="${act}">${rect("dg-halo")}</g>` : ""}${rect("dg-st", ` ${ctx.sh}`)}${act ? `<g class="dg-a" style="${act}">${rect("dg-on")}</g>` : ""}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 箱。動く段は縁に色を付け、外側に淡い光を足す。下の小さな字は起きたことに合わせて書き換わる。 */
|
|
99
|
+
export function card(box: SceneBox, ctx: Ctx, on: number[], changes: SubChange[]): string {
|
|
100
|
+
const { icon, iconRadius, iconScale, iconY, nameY, subY } = LOOK.box;
|
|
101
|
+
const subs = subLines(box, ctx.m, changes, box.cy + subY);
|
|
92
102
|
const act = on.length > 0 ? activeKf(ctx, on) : undefined;
|
|
93
|
-
const rect = (cls: string, extra = ""): string => `<rect class="${cls}" x="${r1(box.x)}" y="${r1(box.y)}" width="${r1(box.w)}" height="${box.h}" rx="${radius}"${extra}/>`;
|
|
94
103
|
const mark =
|
|
95
104
|
box.icon && ICONS[box.icon]
|
|
96
105
|
? `<rect class="dg-ic" x="${r1(box.cx - icon / 2)}" y="${r1(box.cy + iconY - icon / 2)}" width="${icon}" height="${icon}" rx="${iconRadius}"/>${glyph(box.icon, box.cx, box.cy + iconY, iconScale)}`
|
|
97
106
|
: "";
|
|
98
107
|
return (
|
|
99
|
-
`<g style="${cs(box.color)}">${
|
|
108
|
+
`<g style="${cs(box.color)}">${cardFrame(box, ctx, act, box.y, box.h)}` +
|
|
100
109
|
`${mark}<text class="dg-sn" x="${r1(box.cx)}" y="${r1(box.cy + nameY)}" text-anchor="middle">${esc(box.name)}</text>${subs}</g>`
|
|
101
110
|
);
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
/**
|
|
114
|
+
* 寄って追う時系列図の窓の上端に貼る頭の箱 (#3120)。図の頭の箱と同じ横の位置と縁で、アイコンを外して名前と下の字の 2 段にする。
|
|
115
|
+
* 携帯の幅では窓が低く、図の頭の箱のままでは窓の高さの 3 割を頭の列が取るため。
|
|
116
|
+
*/
|
|
117
|
+
export function headCard(box: SceneBox, ctx: Ctx, on: number[], changes: SubChange[]): string {
|
|
118
|
+
const { pad, h, nameY, subY } = LOOK.follow.head;
|
|
119
|
+
const subs = subLines(box, ctx.m, changes, pad + subY);
|
|
120
|
+
const act = on.length > 0 ? activeKf(ctx, on) : undefined;
|
|
121
|
+
return `<g style="${cs(box.color)}">${cardFrame(box, ctx, act, pad, h)}<text class="dg-sn" x="${r1(box.cx)}" y="${r1(pad + nameY)}" text-anchor="middle">${esc(box.name)}</text>${subs}</g>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
104
124
|
export function lifeline(x: number, y1: number, y2: number): string {
|
|
105
125
|
return `<line class="dg-ll" x1="${r1(x)}" y1="${r1(y1)}" x2="${r1(x)}" y2="${r1(y2)}"/>`;
|
|
106
126
|
}
|