@yagejs-addons/dialogue 0.1.0 → 0.2.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/dist/DialogueController-BMeNLi0v.d.cts +1204 -0
- package/dist/DialogueController-Cs5IUc-u.d.ts +1204 -0
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/chunk-CU47RPEB.js +410 -0
- package/dist/chunk-CU47RPEB.js.map +1 -0
- package/dist/chunk-GJQKZCOL.js +983 -0
- package/dist/chunk-GJQKZCOL.js.map +1 -0
- package/dist/index.cjs +3441 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +591 -0
- package/dist/index.d.ts +591 -0
- package/dist/index.js +2048 -0
- package/dist/index.js.map +1 -0
- package/dist/presenters.cjs +3149 -0
- package/dist/presenters.cjs.map +1 -0
- package/dist/presenters.d.cts +1817 -0
- package/dist/presenters.d.ts +1817 -0
- package/dist/presenters.js +2920 -0
- package/dist/presenters.js.map +1 -0
- package/dist/types-DSbBSlh7.d.cts +375 -0
- package/dist/types-DSbBSlh7.d.ts +375 -0
- package/dist/yaml.cjs +726 -0
- package/dist/yaml.cjs.map +1 -0
- package/dist/yaml.d.cts +23 -0
- package/dist/yaml.d.ts +23 -0
- package/dist/yaml.js +37 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-7QVYU63E.js";
|
|
4
|
+
|
|
5
|
+
// src/core/markup.ts
|
|
6
|
+
var EMPTY_PARSED = Object.freeze({
|
|
7
|
+
runs: Object.freeze([]),
|
|
8
|
+
tokens: Object.freeze([]),
|
|
9
|
+
length: 0
|
|
10
|
+
});
|
|
11
|
+
var NAMED_COLORS = {
|
|
12
|
+
black: 0,
|
|
13
|
+
white: 16777215,
|
|
14
|
+
red: 16734810,
|
|
15
|
+
green: 9232491,
|
|
16
|
+
blue: 7316441,
|
|
17
|
+
yellow: 16769126,
|
|
18
|
+
gold: 16765530,
|
|
19
|
+
orange: 16097640,
|
|
20
|
+
purple: 13214975,
|
|
21
|
+
pink: 16752331,
|
|
22
|
+
gray: 10133670,
|
|
23
|
+
grey: 10133670
|
|
24
|
+
};
|
|
25
|
+
function parseColor(raw) {
|
|
26
|
+
const v = raw.trim().toLowerCase();
|
|
27
|
+
if (v.startsWith("#")) {
|
|
28
|
+
const hex = v.slice(1);
|
|
29
|
+
if (/^[0-9a-f]{6}$/.test(hex)) return parseInt(hex, 16);
|
|
30
|
+
if (/^[0-9a-f]{3}$/.test(hex)) {
|
|
31
|
+
const r = hex[0];
|
|
32
|
+
const g = hex[1];
|
|
33
|
+
const b = hex[2];
|
|
34
|
+
return parseInt(`${r}${r}${g}${g}${b}${b}`, 16);
|
|
35
|
+
}
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
if (/^0x[0-9a-f]{6}$/.test(v)) return parseInt(v.slice(2), 16);
|
|
39
|
+
return NAMED_COLORS[v];
|
|
40
|
+
}
|
|
41
|
+
__name(parseColor, "parseColor");
|
|
42
|
+
function effectiveStyle(stack) {
|
|
43
|
+
let style = {};
|
|
44
|
+
for (const f of stack) style = mergeStyle(style, f.override);
|
|
45
|
+
return style;
|
|
46
|
+
}
|
|
47
|
+
__name(effectiveStyle, "effectiveStyle");
|
|
48
|
+
function mergeStyle(parent, child) {
|
|
49
|
+
const merged = {
|
|
50
|
+
...parent,
|
|
51
|
+
...stripUndefined(child)
|
|
52
|
+
};
|
|
53
|
+
if (child.speed !== void 0) {
|
|
54
|
+
merged.speed = (parent.speed ?? 1) * child.speed;
|
|
55
|
+
}
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
__name(mergeStyle, "mergeStyle");
|
|
59
|
+
function stripUndefined(s) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const k of Object.keys(s)) {
|
|
62
|
+
if (s[k] !== void 0) out[k] = s[k];
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
__name(stripUndefined, "stripUndefined");
|
|
67
|
+
var TAG_RE = /\[(\/?)([a-zA-Z]+)(?:=([^\s\]/]*))?((?:\s+[A-Za-z_][\w-]*=[^\s\]/]*)*)(\/)?\]/g;
|
|
68
|
+
var GRAPHEME_SEGMENTER = new Intl.Segmenter();
|
|
69
|
+
function splitGraphemes(text) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const s of GRAPHEME_SEGMENTER.segment(text)) out.push(s.segment);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
__name(splitGraphemes, "splitGraphemes");
|
|
75
|
+
function parseMarkup(input) {
|
|
76
|
+
const runs = [];
|
|
77
|
+
const tokens = [];
|
|
78
|
+
const stack = [];
|
|
79
|
+
let charCount = 0;
|
|
80
|
+
let buffer = "";
|
|
81
|
+
const flush = /* @__PURE__ */ __name(() => {
|
|
82
|
+
if (buffer.length === 0) return;
|
|
83
|
+
const graphemeCount = splitGraphemes(buffer).length;
|
|
84
|
+
runs.push({ text: buffer, style: effectiveStyle(stack), graphemeCount });
|
|
85
|
+
charCount += graphemeCount;
|
|
86
|
+
buffer = "";
|
|
87
|
+
}, "flush");
|
|
88
|
+
let lastIndex = 0;
|
|
89
|
+
TAG_RE.lastIndex = 0;
|
|
90
|
+
let m;
|
|
91
|
+
while ((m = TAG_RE.exec(input)) !== null) {
|
|
92
|
+
let backslashes = 0;
|
|
93
|
+
for (let i = m.index - 1; i >= lastIndex && input[i] === "\\"; i--) backslashes++;
|
|
94
|
+
if (backslashes % 2 === 1) {
|
|
95
|
+
buffer += unescape(input.slice(lastIndex, m.index - 1)) + m[0];
|
|
96
|
+
lastIndex = TAG_RE.lastIndex;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const literal = input.slice(lastIndex, m.index);
|
|
100
|
+
buffer += unescape(literal);
|
|
101
|
+
lastIndex = TAG_RE.lastIndex;
|
|
102
|
+
const closing = m[1] === "/";
|
|
103
|
+
const name = m[2].toLowerCase();
|
|
104
|
+
const arg = m[3];
|
|
105
|
+
const propsStr = m[4];
|
|
106
|
+
const selfClosing = m[5] === "/";
|
|
107
|
+
if (propsStr && !selfClosing) {
|
|
108
|
+
buffer += m[0];
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (closing) {
|
|
112
|
+
flush();
|
|
113
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
114
|
+
if (stack[i].name === name) {
|
|
115
|
+
stack.splice(i, 1);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (selfClosing) {
|
|
122
|
+
flush();
|
|
123
|
+
if (name === "pause") {
|
|
124
|
+
const ms = Number(arg ?? "0");
|
|
125
|
+
if (Number.isFinite(ms) && ms > 0) {
|
|
126
|
+
tokens.push({ kind: "pause", atChar: charCount, ms });
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
tokens.push({
|
|
130
|
+
kind: "marker",
|
|
131
|
+
atChar: charCount,
|
|
132
|
+
name,
|
|
133
|
+
props: markerProps(name, arg, propsStr)
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const override = styleForTag(name, arg);
|
|
139
|
+
if (override) {
|
|
140
|
+
flush();
|
|
141
|
+
stack.push({ name, override });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
buffer += unescape(input.slice(lastIndex));
|
|
145
|
+
flush();
|
|
146
|
+
return { runs: mergeAdjacent(runs), tokens, length: charCount };
|
|
147
|
+
}
|
|
148
|
+
__name(parseMarkup, "parseMarkup");
|
|
149
|
+
function markerProps(name, arg, propsStr) {
|
|
150
|
+
const props = {};
|
|
151
|
+
if (arg !== void 0) props[name] = arg;
|
|
152
|
+
if (propsStr) {
|
|
153
|
+
for (const tok of propsStr.trim().split(/\s+/)) {
|
|
154
|
+
const eq = tok.indexOf("=");
|
|
155
|
+
if (eq > 0) props[tok.slice(0, eq).toLowerCase()] = tok.slice(eq + 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return props;
|
|
159
|
+
}
|
|
160
|
+
__name(markerProps, "markerProps");
|
|
161
|
+
function styleForTag(name, arg) {
|
|
162
|
+
switch (name) {
|
|
163
|
+
case "b":
|
|
164
|
+
case "bold":
|
|
165
|
+
return { bold: true };
|
|
166
|
+
case "i":
|
|
167
|
+
case "italic":
|
|
168
|
+
return { italic: true };
|
|
169
|
+
case "color":
|
|
170
|
+
case "c": {
|
|
171
|
+
const color = arg ? parseColor(arg) : void 0;
|
|
172
|
+
return color !== void 0 ? { color } : null;
|
|
173
|
+
}
|
|
174
|
+
case "speed": {
|
|
175
|
+
const s = Number(arg);
|
|
176
|
+
return Number.isFinite(s) && s > 0 ? { speed: s } : null;
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
return { effect: name };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
__name(styleForTag, "styleForTag");
|
|
183
|
+
function unescape(s) {
|
|
184
|
+
return s.replace(/\\([[\]\\])/g, "$1");
|
|
185
|
+
}
|
|
186
|
+
__name(unescape, "unescape");
|
|
187
|
+
function mergeAdjacent(runs) {
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const run of runs) {
|
|
190
|
+
const prev = out[out.length - 1];
|
|
191
|
+
if (prev && sameStyle(prev.style, run.style)) {
|
|
192
|
+
out[out.length - 1] = {
|
|
193
|
+
text: prev.text + run.text,
|
|
194
|
+
style: prev.style,
|
|
195
|
+
graphemeCount: prev.graphemeCount + run.graphemeCount
|
|
196
|
+
};
|
|
197
|
+
} else {
|
|
198
|
+
out.push(run);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
__name(mergeAdjacent, "mergeAdjacent");
|
|
204
|
+
function sameStyle(a, b) {
|
|
205
|
+
return !!a.bold === !!b.bold && !!a.italic === !!b.italic && a.color === b.color && a.effect === b.effect && (a.speed ?? 1) === (b.speed ?? 1);
|
|
206
|
+
}
|
|
207
|
+
__name(sameStyle, "sameStyle");
|
|
208
|
+
function stripMarkup(input) {
|
|
209
|
+
return parseMarkup(input).runs.map((r) => r.text).join("");
|
|
210
|
+
}
|
|
211
|
+
__name(stripMarkup, "stripMarkup");
|
|
212
|
+
function firstUnknownTag(input) {
|
|
213
|
+
TAG_RE.lastIndex = 0;
|
|
214
|
+
let lastIndex = 0;
|
|
215
|
+
let m;
|
|
216
|
+
while ((m = TAG_RE.exec(input)) !== null) {
|
|
217
|
+
let backslashes = 0;
|
|
218
|
+
for (let i = m.index - 1; i >= lastIndex && input[i] === "\\"; i--) backslashes++;
|
|
219
|
+
lastIndex = TAG_RE.lastIndex;
|
|
220
|
+
if (backslashes % 2 === 1) continue;
|
|
221
|
+
if (m[1] === "/" || m[4] || m[5] === "/") continue;
|
|
222
|
+
const name = m[2].toLowerCase();
|
|
223
|
+
if (styleForTag(name, m[3]) === null) return name;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
__name(firstUnknownTag, "firstUnknownTag");
|
|
228
|
+
|
|
229
|
+
// src/core/LineReveal.ts
|
|
230
|
+
var LineReveal = class {
|
|
231
|
+
/** @param charsPerSec base reveal rate (graphemes/second), scaled by the
|
|
232
|
+
* hold, per-line, and per-run multipliers. */
|
|
233
|
+
constructor(charsPerSec) {
|
|
234
|
+
this.charsPerSec = charsPerSec;
|
|
235
|
+
}
|
|
236
|
+
charsPerSec;
|
|
237
|
+
static {
|
|
238
|
+
__name(this, "LineReveal");
|
|
239
|
+
}
|
|
240
|
+
parsed;
|
|
241
|
+
/** Reveal cursor, in graphemes (fractional while typing). */
|
|
242
|
+
cursor = 0;
|
|
243
|
+
pauseTimer = 0;
|
|
244
|
+
/** Next un-drained token in `parsed.tokens` (one ordered cursor over pauses +
|
|
245
|
+
* markers — source order is drain order). */
|
|
246
|
+
tokenIdx = 0;
|
|
247
|
+
/** Graphemes already ticked (so each grapheme ticks exactly once). */
|
|
248
|
+
tickCount = 0;
|
|
249
|
+
/** Hold-to-fast-forward rate (1 = normal). */
|
|
250
|
+
speedMul = 1;
|
|
251
|
+
/** Per-line `say.speed` multiplier (1 = base). */
|
|
252
|
+
lineSpeed = 1;
|
|
253
|
+
done = false;
|
|
254
|
+
completed = false;
|
|
255
|
+
/** Fired exactly once when the line finishes revealing. The consuming view
|
|
256
|
+
* wires this to the session-owned reveal listener (NOT a public mutable
|
|
257
|
+
* field a game could clobber). */
|
|
258
|
+
onComplete;
|
|
259
|
+
/** Per-grapheme ticks + inline markers, wired by the consuming view to the
|
|
260
|
+
* session-owned beat listener (like {@link onComplete}, never a public field). */
|
|
261
|
+
onBeat;
|
|
262
|
+
/**
|
|
263
|
+
* Register the completion listener — fires once per line, the moment the
|
|
264
|
+
* cursor reaches the end (or synchronously from {@link begin} for an empty
|
|
265
|
+
* line, or from {@link complete}). Pass `undefined` to clear.
|
|
266
|
+
*/
|
|
267
|
+
setCompletionListener(listener) {
|
|
268
|
+
this.onComplete = listener;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Register the reveal-beat listener — per-grapheme ticks and inline markers,
|
|
272
|
+
* in char order, the moment the cursor reaches each. Session-owned (set once,
|
|
273
|
+
* like {@link setCompletionListener}); pass `undefined` to clear.
|
|
274
|
+
*/
|
|
275
|
+
setBeatListener(listener) {
|
|
276
|
+
this.onBeat = listener;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Start revealing a new line. Resets the cursor, pauses, and the hold
|
|
280
|
+
* multiplier (a stale fast-forward must not leak into the next line — an
|
|
281
|
+
* active binding re-asserts it on its next poll). An **empty** line
|
|
282
|
+
* (`parsed.length === 0`) is complete immediately and fires the completion
|
|
283
|
+
* listener synchronously, matching the no-typewriter contract.
|
|
284
|
+
*/
|
|
285
|
+
begin(parsed, lineSpeed = 1) {
|
|
286
|
+
this.parsed = parsed;
|
|
287
|
+
this.lineSpeed = lineSpeed > 0 ? lineSpeed : 1;
|
|
288
|
+
this.cursor = 0;
|
|
289
|
+
this.pauseTimer = 0;
|
|
290
|
+
this.tokenIdx = 0;
|
|
291
|
+
this.tickCount = 0;
|
|
292
|
+
this.speedMul = 1;
|
|
293
|
+
this.done = parsed.length === 0;
|
|
294
|
+
this.completed = false;
|
|
295
|
+
this.drainTokens();
|
|
296
|
+
if (this.done) this.finish();
|
|
297
|
+
}
|
|
298
|
+
/** Hold-to-fast-forward multiplier (1 = normal, e.g. 4 while skip is held). */
|
|
299
|
+
setSpeedMultiplier(m) {
|
|
300
|
+
this.speedMul = Math.max(1, m);
|
|
301
|
+
}
|
|
302
|
+
/** Advance the reveal cursor by `dt` (ms). Honours armed pauses and per-run
|
|
303
|
+
* speed; fires completion once the cursor reaches the end. No-op after the
|
|
304
|
+
* line is done or before the first {@link begin}. */
|
|
305
|
+
update(dt) {
|
|
306
|
+
const parsed = this.parsed;
|
|
307
|
+
if (!parsed || this.done) return;
|
|
308
|
+
if (this.pauseTimer > 0) {
|
|
309
|
+
this.pauseTimer = Math.max(0, this.pauseTimer - dt);
|
|
310
|
+
} else {
|
|
311
|
+
this.drainTokens();
|
|
312
|
+
if (this.pauseTimer === 0) {
|
|
313
|
+
const rate = this.charsPerSec * this.speedMul * this.lineSpeed * this.runSpeedAt(this.cursor);
|
|
314
|
+
this.cursor = Math.min(parsed.length, this.cursor + rate * dt / 1e3);
|
|
315
|
+
this.drainTokens();
|
|
316
|
+
this.emitTicks();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (this.cursor >= parsed.length && this.pauseTimer === 0) this.finish();
|
|
320
|
+
}
|
|
321
|
+
/** Reveal everything now (skip-to-end on a click/tap). Drains any not-yet-fired
|
|
322
|
+
* markers in order so their consequences still happen (`viaSkip=true` lets a
|
|
323
|
+
* host suppress a loud one-shot) and blows straight through pending pauses (a
|
|
324
|
+
* skip ignores holds), but DISCARDS pending ticks — replaying dozens of
|
|
325
|
+
* typewriter blips at once would machine-gun. Fires completion. */
|
|
326
|
+
complete() {
|
|
327
|
+
const parsed = this.parsed;
|
|
328
|
+
if (!parsed) return;
|
|
329
|
+
this.cursor = parsed.length;
|
|
330
|
+
this.pauseTimer = 0;
|
|
331
|
+
this.drainTokens(true);
|
|
332
|
+
this.tickCount = parsed.length;
|
|
333
|
+
this.finish();
|
|
334
|
+
}
|
|
335
|
+
/** Revealed grapheme count (fractional while typing). The view floors this to
|
|
336
|
+
* map onto its glyph prefix table. */
|
|
337
|
+
get revealed() {
|
|
338
|
+
return this.cursor;
|
|
339
|
+
}
|
|
340
|
+
/** True once the line is fully revealed (also true for an empty line). */
|
|
341
|
+
isComplete() {
|
|
342
|
+
return this.done;
|
|
343
|
+
}
|
|
344
|
+
/** True while glyphs are still appearing. */
|
|
345
|
+
isRevealing() {
|
|
346
|
+
return !this.done;
|
|
347
|
+
}
|
|
348
|
+
finish() {
|
|
349
|
+
this.done = true;
|
|
350
|
+
if (this.completed) return;
|
|
351
|
+
this.completed = true;
|
|
352
|
+
this.onComplete?.();
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Drain tokens whose offset the cursor has reached, IN SOURCE ORDER. A `marker`
|
|
356
|
+
* emits a beat; a `pause` arms the hold, clamps the cursor to its offset, and
|
|
357
|
+
* STOPS the drain for this frame (a one-frame advance can overshoot the offset,
|
|
358
|
+
* so the clamp keeps glyphs past the beat from popping in early, and a later
|
|
359
|
+
* token waits until the hold resumes). `viaSkip` (from {@link complete}) tags
|
|
360
|
+
* drained markers and blows straight through pauses without holding. Monotonic
|
|
361
|
+
* `tokenIdx` → each token is handled exactly once.
|
|
362
|
+
*/
|
|
363
|
+
drainTokens(viaSkip = false) {
|
|
364
|
+
const tokens = this.parsed?.tokens;
|
|
365
|
+
if (!tokens) return;
|
|
366
|
+
while (this.tokenIdx < tokens.length && this.cursor >= tokens[this.tokenIdx].atChar) {
|
|
367
|
+
const tok = tokens[this.tokenIdx];
|
|
368
|
+
this.tokenIdx++;
|
|
369
|
+
if (tok.kind === "marker") {
|
|
370
|
+
this.onBeat?.({ kind: "marker", marker: tok, viaSkip });
|
|
371
|
+
} else if (!viaSkip && tok.ms > 0) {
|
|
372
|
+
this.pauseTimer = tok.ms;
|
|
373
|
+
this.cursor = Math.min(this.cursor, tok.atChar);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/** Emit a `tick` for each grapheme newly revealed since the last call (raw —
|
|
379
|
+
* no whitespace test; the host filters). Multiple in order on a large-dt
|
|
380
|
+
* frame; `tickCount` is monotonic so none repeat. */
|
|
381
|
+
emitTicks() {
|
|
382
|
+
const next = Math.floor(this.cursor);
|
|
383
|
+
for (let i = this.tickCount; i < next; i++) {
|
|
384
|
+
this.onBeat?.({ kind: "tick", index: i });
|
|
385
|
+
}
|
|
386
|
+
this.tickCount = next;
|
|
387
|
+
}
|
|
388
|
+
/** Reveal speed multiplier for whichever run the cursor currently sits in. */
|
|
389
|
+
runSpeedAt(reveal) {
|
|
390
|
+
const parsed = this.parsed;
|
|
391
|
+
if (!parsed) return 1;
|
|
392
|
+
const at = Math.floor(reveal);
|
|
393
|
+
let acc = 0;
|
|
394
|
+
for (const run of parsed.runs) {
|
|
395
|
+
if (at < acc + run.graphemeCount) return run.style.speed ?? 1;
|
|
396
|
+
acc += run.graphemeCount;
|
|
397
|
+
}
|
|
398
|
+
return 1;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export {
|
|
403
|
+
EMPTY_PARSED,
|
|
404
|
+
splitGraphemes,
|
|
405
|
+
parseMarkup,
|
|
406
|
+
stripMarkup,
|
|
407
|
+
firstUnknownTag,
|
|
408
|
+
LineReveal
|
|
409
|
+
};
|
|
410
|
+
//# sourceMappingURL=chunk-CU47RPEB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/markup.ts","../src/core/LineReveal.ts"],"sourcesContent":["/**\n * Inline-markup parser. Turns an authored string into styled {@link TextRun}s\n * plus {@link RevealToken}s, using a small BBCode-ish tag syntax that survives\n * translation (translators keep the tags, reorder the words):\n *\n * plain text\n * [b]bold[/b] [i]italic[/i]\n * [color=#ffcc00]hex[/color] [color=gold]named[/color]\n * [wave]animated[/wave] (effect span — OPEN vocabulary: any [name]..[/name];\n * the bundled view animates wave/shake/pulse/rainbow)\n * [speed=2]faster[/speed] [speed=0.5]slower[/speed]\n * [pause=400/] (self-closing reveal PAUSE — holds at its offset, in ms)\n * [sfx=ding/] (self-closing reveal MARKER — fires at its offset)\n * [expression=happy/] (self-named shortcut → props { expression: happy })\n * [shake amount=3/] (marker with explicit key=value props)\n * [shake=500 amount=3/] (shortcut + props compose → { shake: 500, amount: 3 })\n * \\[literal bracket]\n *\n * Tags nest; styles inherit down the stack (so [b][color=red]X[/color][/b]\n * is bold+red). A trailing `/` makes a tag **self-closing** — a zero-width\n * {@link RevealToken} (a `[pause=600/]` hold or a `[name k=v/]` marker) that the\n * reveal drains at its char offset, distinct from the styling tags (which never\n * end in `/`). Pause + markers share one ordered stream, so **source order is\n * drain order**: `[pause=600/][shake/]` holds then fires; `[shake/][pause=600/]`\n * fires then holds. A non-self-closing tag that isn't a built-in text attribute\n * opens an EFFECT span named after the tag (an open vocabulary the presenter\n * interprets); translators MUST keep a marker/pause's self-closing `/` so the\n * token survives a re-order.\n */\n\nimport type { ParsedText, RevealToken, RunStyle, TextRun } from \"./types.js\";\n\n/** The empty parse result (no runs / tokens, length 0). Shared so a presenter or\n * the session can present a contentless line (an empty choice prompt) without\n * re-constructing the shape — and without forgetting the required `tokens`\n * field. */\nexport const EMPTY_PARSED: ParsedText = Object.freeze({\n runs: Object.freeze([]) as readonly TextRun[],\n tokens: Object.freeze([]) as readonly RevealToken[],\n length: 0,\n});\n\nconst NAMED_COLORS: Record<string, number> = {\n black: 0x000000,\n white: 0xffffff,\n red: 0xff5a5a,\n green: 0x8ce06b,\n blue: 0x6fa3d9,\n yellow: 0xffe066,\n gold: 0xffd25a,\n orange: 0xf5a168,\n purple: 0xc9a4ff,\n pink: 0xff9ecb,\n gray: 0x9aa0a6,\n grey: 0x9aa0a6,\n};\n\nfunction parseColor(raw: string): number | undefined {\n const v = raw.trim().toLowerCase();\n if (v.startsWith(\"#\")) {\n const hex = v.slice(1);\n if (/^[0-9a-f]{6}$/.test(hex)) return parseInt(hex, 16);\n if (/^[0-9a-f]{3}$/.test(hex)) {\n // #rgb → #rrggbb\n const r = hex[0]!;\n const g = hex[1]!;\n const b = hex[2]!;\n return parseInt(`${r}${r}${g}${g}${b}${b}`, 16);\n }\n return undefined;\n }\n if (/^0x[0-9a-f]{6}$/.test(v)) return parseInt(v.slice(2), 16);\n return NAMED_COLORS[v];\n}\n\ninterface Frame {\n /** Originating tag name, so a closing tag pops the matching frame. */\n readonly name: string;\n /** This tag's own style delta (not pre-merged with parents). */\n readonly override: Partial<RunStyle>;\n}\n\n/**\n * Fold every open frame's delta outermost→innermost into one effective style.\n * Recomputing from deltas (rather than caching a pre-merged style per frame)\n * means closing any frame — even a crossed/mismatched one — yields the\n * correct inherited style for the text that follows.\n */\nfunction effectiveStyle(stack: readonly Frame[]): RunStyle {\n let style: RunStyle = {};\n for (const f of stack) style = mergeStyle(style, f.override);\n return style;\n}\n\n/** Merge a child override onto the inherited parent style. */\nfunction mergeStyle(parent: RunStyle, child: Partial<RunStyle>): RunStyle {\n const merged: { -readonly [K in keyof RunStyle]: RunStyle[K] } = {\n ...parent,\n ...stripUndefined(child),\n };\n // `speed` composes multiplicatively so nested [speed] tags stack.\n if (child.speed !== undefined) {\n merged.speed = (parent.speed ?? 1) * child.speed;\n }\n return merged;\n}\n\nfunction stripUndefined(s: Partial<RunStyle>): Partial<RunStyle> {\n const out: Partial<RunStyle> = {};\n for (const k of Object.keys(s) as (keyof RunStyle)[]) {\n if (s[k] !== undefined) (out as Record<string, unknown>)[k] = s[k];\n }\n return out;\n}\n\n// Groups: 1 closing `/`, 2 name, 3 `=value` (styled spans + a marker's self-named\n// shortcut — the shared `=`-separator), 4 space-separated `key=value` props, 5 a\n// trailing `/` marking a self-closing token (`[pause=N/]` or a marker). The value\n// (3) and prop values (4) both exclude whitespace and `/`, so the self-named\n// shortcut composes with explicit props (`[shake=500 amount=3/]` → group 3 `500`,\n// group 4 ` amount=3`) and the trailing slash stays unambiguous. Groups 4–5 are\n// additive: an existing styled/closing tag matches them empty.\nconst TAG_RE =\n /\\[(\\/?)([a-zA-Z]+)(?:=([^\\s\\]/]*))?((?:\\s+[A-Za-z_][\\w-]*=[^\\s\\]/]*)*)(\\/)?\\]/g;\n\n/**\n * Grapheme segmenter for all reveal bookkeeping. Pixi's `SplitText` /\n * `SplitBitmapText` create one glyph node per grapheme via\n * `CanvasTextMetrics.graphemeSegmenter`, which is `new Intl.Segmenter()`\n * (default \"grapheme\" granularity). We intentionally use the same segmentation\n * — without importing pixi (this file is in the pixi-free root entry) — so run\n * lengths, pause offsets, and per-glyph styles line up with rendered glyphs on\n * emoji / ZWJ sequences / combining marks.\n */\nconst GRAPHEME_SEGMENTER = new Intl.Segmenter();\n\n/**\n * Split a string into graphemes (user-perceived characters) — the unit the\n * renderer creates one glyph node per, and the unit every reveal-side count\n * (`ParsedText.length`, `TextRun.graphemeCount`, `PauseToken.atChar`) uses.\n */\nexport function splitGraphemes(text: string): string[] {\n const out: string[] = [];\n for (const s of GRAPHEME_SEGMENTER.segment(text)) out.push(s.segment);\n return out;\n}\n\nexport function parseMarkup(input: string): ParsedText {\n const runs: TextRun[] = [];\n const tokens: RevealToken[] = [];\n const stack: Frame[] = [];\n let charCount = 0;\n let buffer = \"\";\n\n const flush = (): void => {\n if (buffer.length === 0) return;\n // Segment once per flushed run (O(n) over the whole input in total).\n const graphemeCount = splitGraphemes(buffer).length;\n runs.push({ text: buffer, style: effectiveStyle(stack), graphemeCount });\n charCount += graphemeCount;\n buffer = \"\";\n };\n\n // Walk the string, copying literal text into `buffer` and acting on tags.\n let lastIndex = 0;\n TAG_RE.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = TAG_RE.exec(input)) !== null) {\n // Escape-awareness: count the contiguous backslashes immediately before the\n // `[`. An odd count means the bracket itself is escaped (`\\[b]` → literal\n // \"[b]\"), so emit the tag text verbatim — consuming the escaping backslash —\n // instead of acting on it. An even count is just escaped backslashes\n // (`\\\\[b]` → \"\\\" + a REAL [b] tag), handled by unescape() as usual.\n let backslashes = 0;\n for (let i = m.index - 1; i >= lastIndex && input[i] === \"\\\\\"; i--) backslashes++;\n if (backslashes % 2 === 1) {\n buffer += unescape(input.slice(lastIndex, m.index - 1)) + m[0];\n lastIndex = TAG_RE.lastIndex;\n continue;\n }\n const literal = input.slice(lastIndex, m.index);\n buffer += unescape(literal);\n lastIndex = TAG_RE.lastIndex;\n\n const closing = m[1] === \"/\";\n const name = m[2]!.toLowerCase();\n const arg = m[3];\n const propsStr = m[4];\n const selfClosing = m[5] === \"/\";\n\n // A tag carrying `key=value` props but no trailing `/` is neither a styled\n // span (those take no props) nor a self-closing marker (those require the\n // `/`). Emit it verbatim as literal text — restoring the pre-marker behavior\n // where a space-bearing `[name k=v]` simply didn't match the tag regex, so a\n // forgotten slash shows up as visible text instead of silently opening an\n // effect span over the rest of the line and dropping the props.\n if (propsStr && !selfClosing) {\n buffer += m[0];\n continue;\n }\n\n if (closing) {\n flush();\n // Pop the innermost frame opened by a tag of this name (BBCode rule);\n // a stray close with no match is ignored (permissive / forward-compatible).\n for (let i = stack.length - 1; i >= 0; i--) {\n if (stack[i]!.name === name) {\n stack.splice(i, 1);\n break;\n }\n }\n continue;\n }\n\n // Self-closing reveal token (`[name k=v/]`): the trailing `/` distinguishes\n // it from a styling tag of the same name (`[shake]…[/shake]` is an effect\n // span; `[shake/]` is a marker). `pause` is the one parser-reserved name —\n // it becomes a typed PauseToken (hold), everything else a MarkerToken.\n if (selfClosing) {\n flush();\n if (name === \"pause\") {\n const ms = Number(arg ?? \"0\");\n if (Number.isFinite(ms) && ms > 0) {\n tokens.push({ kind: \"pause\", atChar: charCount, ms });\n }\n } else {\n tokens.push({\n kind: \"marker\",\n atChar: charCount,\n name,\n props: markerProps(name, arg, propsStr),\n });\n }\n continue;\n }\n\n const override = styleForTag(name, arg);\n if (override) {\n flush();\n stack.push({ name, override });\n }\n // A built-in styling tag with a bad/missing arg (color/speed) → ignore; text flows.\n }\n buffer += unescape(input.slice(lastIndex));\n flush();\n\n return { runs: mergeAdjacent(runs), tokens, length: charCount };\n}\n\n/**\n * Build a marker's props from the Yarn-style forms. The self-named shortcut\n * `[name=val/]` (group 3) ≡ `[name name=val/]`, so it seeds `{ [name]: val }`,\n * and explicit space-separated `key=value` pairs (group 4) merge on top — the two\n * compose (`[shake=500 amount=3/]` → `{ shake: \"500\", amount: \"3\" }`), because the\n * value group excludes whitespace and so stops at the first space. `[name/]` →\n * `{}`. Keys lower-cased (like tag names); values kept verbatim (no whitespace).\n */\nfunction markerProps(\n name: string,\n arg: string | undefined,\n propsStr: string | undefined,\n): Record<string, string> {\n const props: Record<string, string> = {};\n if (arg !== undefined) props[name] = arg;\n if (propsStr) {\n for (const tok of propsStr.trim().split(/\\s+/)) {\n const eq = tok.indexOf(\"=\");\n if (eq > 0) props[tok.slice(0, eq).toLowerCase()] = tok.slice(eq + 1);\n }\n }\n return props;\n}\n\nfunction styleForTag(name: string, arg?: string): Partial<RunStyle> | null {\n switch (name) {\n case \"b\":\n case \"bold\":\n return { bold: true };\n case \"i\":\n case \"italic\":\n return { italic: true };\n case \"color\":\n case \"c\": {\n const color = arg ? parseColor(arg) : undefined;\n return color !== undefined ? { color } : null;\n }\n case \"speed\": {\n const s = Number(arg);\n return Number.isFinite(s) && s > 0 ? { speed: s } : null;\n }\n default:\n // Any other paired (non-self-closing) tag opens an effect span carrying its\n // name — an OPEN vocabulary the presenter interprets. The bundled text view\n // animates the BuiltinEffectIds; an unrecognized name renders as plain\n // styled text. (Built-in text attributes are handled above; a marker is the\n // self-closing `[name/]` form, parsed before styleForTag is reached.)\n return { effect: name };\n }\n}\n\n/** `\\[` → `[`, `\\]` → `]`, `\\\\` → `\\`. */\nfunction unescape(s: string): string {\n return s.replace(/\\\\([[\\]\\\\])/g, \"$1\");\n}\n\n/** Coalesce neighbouring runs that ended up with identical styles. */\nfunction mergeAdjacent(runs: readonly TextRun[]): TextRun[] {\n const out: TextRun[] = [];\n for (const run of runs) {\n const prev = out[out.length - 1];\n if (prev && sameStyle(prev.style, run.style)) {\n // Sum the per-flush counts rather than re-segmenting the joined text:\n // pause offsets were tallied per flush, so this keeps `length`/`atChar`/\n // run counts on one consistent basis. (A grapheme split across a tag\n // boundary — e.g. a combining mark right after `[/b]` — would join when\n // the renderer segments the full line; the cursor then finishes one\n // step past the last glyph, which the reveal clamps harmlessly.)\n out[out.length - 1] = {\n text: prev.text + run.text,\n style: prev.style,\n graphemeCount: prev.graphemeCount + run.graphemeCount,\n };\n } else {\n out.push(run);\n }\n }\n return out;\n}\n\nfunction sameStyle(a: RunStyle, b: RunStyle): boolean {\n return (\n !!a.bold === !!b.bold &&\n !!a.italic === !!b.italic &&\n a.color === b.color &&\n a.effect === b.effect &&\n (a.speed ?? 1) === (b.speed ?? 1)\n );\n}\n\n/** Strip every tag, returning plain text (useful for measuring / a11y / logs). */\nexport function stripMarkup(input: string): string {\n return parseMarkup(input)\n .runs.map((r) => r.text)\n .join(\"\");\n}\n\n/**\n * The name of the first bracketed `[tag]` in `input` that {@link parseMarkup}\n * would drop silently, or `null` when every tag is meaningful. Escape-aware:\n * `\\[x]` is literal text, not a tag, matching the parser.\n *\n * Because the effect vocabulary is open, every paired `[name]…[/name]` opens an\n * effect span and every `[name/]` is a marker — both are meaningful, so neither\n * is flagged. What remains droppable is a built-in styling tag with a malformed\n * argument (`[color=notacolor]`, `[speed=abc]`), which {@link styleForTag} can't\n * act on and discards — almost always a typo.\n *\n * The compact authoring front-end calls this on choice text, where `[..]` is\n * reserved for inline markup, to surface such a dropped tag instead of letting a\n * mistyped attribute vanish. (Say-line text is passed through untouched.)\n */\nexport function firstUnknownTag(input: string): string | null {\n TAG_RE.lastIndex = 0;\n let lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = TAG_RE.exec(input)) !== null) {\n // Same odd-backslash escape test parseMarkup uses: an escaped `\\[` is text.\n let backslashes = 0;\n for (let i = m.index - 1; i >= lastIndex && input[i] === \"\\\\\"; i--) backslashes++;\n lastIndex = TAG_RE.lastIndex;\n if (backslashes % 2 === 1) continue;\n // None of these is dropped, so none is a typo: a closing tag (`m[1]`) pops a\n // frame, a self-closing marker (`[sfx=ding/]`, `m[5]`) parses to a\n // MarkerToken, and a props-bearing tag with no slash (`[name k=v]`, `m[4]`)\n // is kept as literal text.\n if (m[1] === \"/\" || m[4] || m[5] === \"/\") continue;\n const name = m[2]!.toLowerCase();\n // Every other opening tag opens a span — a built-in text attribute or, for\n // any other name, an effect. The lone droppable case is a built-in styling\n // tag whose argument doesn't parse (styleForTag returns null).\n if (styleForTag(name, m[3]) === null) return name;\n }\n return null;\n}\n","/**\n * LineReveal — the headless typewriter clock. Given a {@link ParsedText} (markup\n * already parsed into runs + an ordered {@link RevealToken} stream), a base\n * `charsPerSec`, a per-line speed multiplier, and `update(dt)` ticks, it advances\n * a reveal cursor in **graphemes** (the unit `markup.ts` counts and the renderer\n * splits glyphs by), drains the tokens IN SOURCE ORDER (a `pause` holds, a\n * `marker` fires a {@link RevealBeat}), applies per-run `[speed]`, and fires\n * completion **exactly once** per line.\n *\n * It is renderer-free on purpose: a DOM-overlay or per-word presenter can drive\n * the same reveal logic and map the grapheme cursor onto its own rendering,\n * without pulling the renderer in. The default `DialogueTextView` consumes it\n * and keeps only the SplitText concerns — the code-unit→glyph prefix mapping and\n * per-glyph style fan-out — which LineReveal deliberately does NOT own.\n *\n * What it owns: the reveal cursor, the `ParsedText.tokens` drain (one ordered\n * index over pauses + markers), the hold-to-fast-forward multiplier, the per-line\n * and per-run (`RunStyle.speed`) speeds, and the fired-once completion. What it\n * does NOT own: anything that touches a glyph, a texture, or a layout. Counts are\n * graphemes throughout — it reads the pre-computed grapheme counts off\n * `ParsedText` (`length`, `TextRun.graphemeCount`, a token's `atChar`) and never\n * re-segments.\n */\n\nimport type { MarkerToken, ParsedText } from \"./types.js\";\n\n/**\n * A reveal-time beat the clock emits as the cursor advances: a per-grapheme\n * `tick` (one per revealed grapheme — raw, including whitespace; the host\n * filters) and a `marker` when the cursor reaches a {@link MarkerToken}'s\n * offset. `viaSkip` is true when the marker was drained by {@link\n * LineReveal.complete} (a skip / fast-forward) rather than reached during normal\n * typing, so a host can suppress a loud one-shot that only fired because of a\n * skip click. Ticks are NOT emitted on a skip (replaying dozens at once would\n * machine-gun).\n */\nexport type RevealBeat =\n | { readonly kind: \"tick\"; readonly index: number }\n | { readonly kind: \"marker\"; readonly marker: MarkerToken; readonly viaSkip: boolean };\n\nexport class LineReveal {\n private parsed: ParsedText | undefined;\n /** Reveal cursor, in graphemes (fractional while typing). */\n private cursor = 0;\n private pauseTimer = 0;\n /** Next un-drained token in `parsed.tokens` (one ordered cursor over pauses +\n * markers — source order is drain order). */\n private tokenIdx = 0;\n /** Graphemes already ticked (so each grapheme ticks exactly once). */\n private tickCount = 0;\n /** Hold-to-fast-forward rate (1 = normal). */\n private speedMul = 1;\n /** Per-line `say.speed` multiplier (1 = base). */\n private lineSpeed = 1;\n private done = false;\n private completed = false;\n /** Fired exactly once when the line finishes revealing. The consuming view\n * wires this to the session-owned reveal listener (NOT a public mutable\n * field a game could clobber). */\n private onComplete: (() => void) | undefined;\n /** Per-grapheme ticks + inline markers, wired by the consuming view to the\n * session-owned beat listener (like {@link onComplete}, never a public field). */\n private onBeat: ((beat: RevealBeat) => void) | undefined;\n\n /** @param charsPerSec base reveal rate (graphemes/second), scaled by the\n * hold, per-line, and per-run multipliers. */\n constructor(private readonly charsPerSec: number) {}\n\n /**\n * Register the completion listener — fires once per line, the moment the\n * cursor reaches the end (or synchronously from {@link begin} for an empty\n * line, or from {@link complete}). Pass `undefined` to clear.\n */\n setCompletionListener(listener: (() => void) | undefined): void {\n this.onComplete = listener;\n }\n\n /**\n * Register the reveal-beat listener — per-grapheme ticks and inline markers,\n * in char order, the moment the cursor reaches each. Session-owned (set once,\n * like {@link setCompletionListener}); pass `undefined` to clear.\n */\n setBeatListener(listener: ((beat: RevealBeat) => void) | undefined): void {\n this.onBeat = listener;\n }\n\n /**\n * Start revealing a new line. Resets the cursor, pauses, and the hold\n * multiplier (a stale fast-forward must not leak into the next line — an\n * active binding re-asserts it on its next poll). An **empty** line\n * (`parsed.length === 0`) is complete immediately and fires the completion\n * listener synchronously, matching the no-typewriter contract.\n */\n begin(parsed: ParsedText, lineSpeed = 1): void {\n this.parsed = parsed;\n this.lineSpeed = lineSpeed > 0 ? lineSpeed : 1;\n this.cursor = 0;\n this.pauseTimer = 0;\n this.tokenIdx = 0;\n this.tickCount = 0;\n this.speedMul = 1;\n this.done = parsed.length === 0;\n this.completed = false;\n // Drain any offset-0 tokens synchronously (a marker-only / length-0 line, a\n // line that opens with a marker, or a leading `[pause/]` that delays the\n // first glyph) — the beat listener is session-owned and wired before begin(),\n // like the completion listener. Tokens come before the empty-line completion:\n // they're part of the line, completion ends it.\n this.drainTokens();\n if (this.done) this.finish();\n }\n\n /** Hold-to-fast-forward multiplier (1 = normal, e.g. 4 while skip is held). */\n setSpeedMultiplier(m: number): void {\n this.speedMul = Math.max(1, m);\n }\n\n /** Advance the reveal cursor by `dt` (ms). Honours armed pauses and per-run\n * speed; fires completion once the cursor reaches the end. No-op after the\n * line is done or before the first {@link begin}. */\n update(dt: number): void {\n const parsed = this.parsed;\n if (!parsed || this.done) return;\n if (this.pauseTimer > 0) {\n this.pauseTimer = Math.max(0, this.pauseTimer - dt);\n } else {\n // A token sitting exactly at the current cursor (a leading token, or the\n // next one when resuming after a hold) drains before we advance — a marker\n // fires, a pause re-arms (and re-clamps), so the advance is skipped.\n this.drainTokens();\n if (this.pauseTimer === 0) {\n const rate =\n this.charsPerSec * this.speedMul * this.lineSpeed * this.runSpeedAt(this.cursor);\n this.cursor = Math.min(parsed.length, this.cursor + (rate * dt) / 1000);\n // Drain tokens up to the new cursor IN SOURCE ORDER: a marker fires, a\n // pause clamps the cursor back to its offset and stops the drain (so a\n // later token waits for the next hold-resume). emitTicks runs AFTER, so\n // ticks stop at the clamp and glyphs past a [pause] don't reveal early.\n this.drainTokens();\n this.emitTicks();\n }\n }\n if (this.cursor >= parsed.length && this.pauseTimer === 0) this.finish();\n }\n\n /** Reveal everything now (skip-to-end on a click/tap). Drains any not-yet-fired\n * markers in order so their consequences still happen (`viaSkip=true` lets a\n * host suppress a loud one-shot) and blows straight through pending pauses (a\n * skip ignores holds), but DISCARDS pending ticks — replaying dozens of\n * typewriter blips at once would machine-gun. Fires completion. */\n complete(): void {\n const parsed = this.parsed;\n if (!parsed) return;\n this.cursor = parsed.length;\n this.pauseTimer = 0;\n this.drainTokens(true);\n this.tickCount = parsed.length; // swallow the pending ticks\n this.finish();\n }\n\n /** Revealed grapheme count (fractional while typing). The view floors this to\n * map onto its glyph prefix table. */\n get revealed(): number {\n return this.cursor;\n }\n\n /** True once the line is fully revealed (also true for an empty line). */\n isComplete(): boolean {\n return this.done;\n }\n\n /** True while glyphs are still appearing. */\n isRevealing(): boolean {\n return !this.done;\n }\n\n private finish(): void {\n this.done = true;\n if (this.completed) return;\n this.completed = true;\n this.onComplete?.();\n }\n\n /**\n * Drain tokens whose offset the cursor has reached, IN SOURCE ORDER. A `marker`\n * emits a beat; a `pause` arms the hold, clamps the cursor to its offset, and\n * STOPS the drain for this frame (a one-frame advance can overshoot the offset,\n * so the clamp keeps glyphs past the beat from popping in early, and a later\n * token waits until the hold resumes). `viaSkip` (from {@link complete}) tags\n * drained markers and blows straight through pauses without holding. Monotonic\n * `tokenIdx` → each token is handled exactly once.\n */\n private drainTokens(viaSkip = false): void {\n const tokens = this.parsed?.tokens;\n if (!tokens) return;\n while (this.tokenIdx < tokens.length && this.cursor >= tokens[this.tokenIdx]!.atChar) {\n const tok = tokens[this.tokenIdx]!;\n this.tokenIdx++;\n if (tok.kind === \"marker\") {\n this.onBeat?.({ kind: \"marker\", marker: tok, viaSkip });\n } else if (!viaSkip && tok.ms > 0) {\n this.pauseTimer = tok.ms;\n this.cursor = Math.min(this.cursor, tok.atChar);\n return; // hold here this frame; later tokens wait\n }\n }\n }\n\n /** Emit a `tick` for each grapheme newly revealed since the last call (raw —\n * no whitespace test; the host filters). Multiple in order on a large-dt\n * frame; `tickCount` is monotonic so none repeat. */\n private emitTicks(): void {\n const next = Math.floor(this.cursor);\n for (let i = this.tickCount; i < next; i++) {\n this.onBeat?.({ kind: \"tick\", index: i });\n }\n this.tickCount = next;\n }\n\n /** Reveal speed multiplier for whichever run the cursor currently sits in. */\n private runSpeedAt(reveal: number): number {\n const parsed = this.parsed;\n if (!parsed) return 1;\n const at = Math.floor(reveal);\n let acc = 0;\n for (const run of parsed.runs) {\n if (at < acc + run.graphemeCount) return run.style.speed ?? 1;\n acc += run.graphemeCount;\n }\n return 1;\n }\n}\n"],"mappings":"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","names":[]}
|