chalk-talk 0.6.0 → 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/README.md +1 -1
- package/assets/player.html +2 -2
- package/package.json +1 -1
- package/player.js +3 -3
- package/scripts/build.mjs +30 -9
- package/scripts/contract.mjs +751 -192
- package/scripts/mermaid.mjs +199 -0
package/scripts/contract.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by tools/assets.mjs from the drawing engine v0.
|
|
1
|
+
// GENERATED by tools/assets.mjs from the drawing engine v0.243.2. Do not edit.
|
|
2
2
|
var WORDS = ["and", "or", "not"];
|
|
3
3
|
|
|
4
4
|
var PROP_NAMES = [
|
|
@@ -327,34 +327,34 @@ var STEP_TYPES = [
|
|
|
327
327
|
var PLACEABLE = ["text", "prop", "card", "counter", "circle", "ellipse"];
|
|
328
328
|
var ARRANGEMENTS = ["row", "column", "grid"];
|
|
329
329
|
var MAX_REPEAT = 50;
|
|
330
|
-
function object(
|
|
331
|
-
if (!
|
|
330
|
+
function object(value2, name) {
|
|
331
|
+
if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) throw new Error(`${name}: expected an object.`);
|
|
332
332
|
}
|
|
333
|
-
function text(
|
|
334
|
-
if (typeof
|
|
333
|
+
function text(value2, name) {
|
|
334
|
+
if (typeof value2 !== "string" || !value2.trim() || value2.length > 2e3) {
|
|
335
335
|
throw new Error(`${name}: expected text of 1 to 2000 characters.`);
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
|
-
function finite(
|
|
339
|
-
if (typeof
|
|
338
|
+
function finite(value2, name, min = -1e4, max = 1e4) {
|
|
339
|
+
if (typeof value2 !== "number" || !Number.isFinite(value2) || value2 < min || value2 > max) {
|
|
340
340
|
throw new Error(`${name}: expected a number between ${min} and ${max}.`);
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
|
-
function numeric(
|
|
344
|
-
if (typeof
|
|
345
|
-
text(
|
|
346
|
-
if (
|
|
347
|
-
} else finite(
|
|
343
|
+
function numeric(value2, name) {
|
|
344
|
+
if (typeof value2 === "string") {
|
|
345
|
+
text(value2, name);
|
|
346
|
+
if (value2.length > 300) throw new Error(`${name}: expression too long.`);
|
|
347
|
+
} else finite(value2, name);
|
|
348
348
|
}
|
|
349
|
-
function point(
|
|
350
|
-
if (!Array.isArray(
|
|
351
|
-
|
|
349
|
+
function point(value2, name) {
|
|
350
|
+
if (!Array.isArray(value2) || value2.length !== 2) throw new Error(`${name}: expected [x, y].`);
|
|
351
|
+
value2.forEach((v, i) => numeric(v, `${name}[${i}]`));
|
|
352
352
|
}
|
|
353
|
-
function optionalText(
|
|
354
|
-
if (
|
|
353
|
+
function optionalText(value2, name) {
|
|
354
|
+
if (value2 !== void 0) text(value2, name);
|
|
355
355
|
}
|
|
356
|
-
function choice(
|
|
357
|
-
if (
|
|
356
|
+
function choice(value2, choices, name) {
|
|
357
|
+
if (value2 !== void 0 && (typeof value2 !== "string" || !choices.includes(value2))) {
|
|
358
358
|
throw new Error(`${name}: choose one of ${choices.join(", ")}.`);
|
|
359
359
|
}
|
|
360
360
|
}
|
|
@@ -364,215 +364,215 @@ function identifier(key) {
|
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
var known = (ids, id) => id.includes("{") || ids.exact.has(id) || [...ids.repeated].some((r) => id.startsWith(`${r}-`) && /^\d+$/.test(id.slice(r.length + 1)));
|
|
367
|
-
function pointOrPlaced(
|
|
368
|
-
if (placed &&
|
|
369
|
-
point(
|
|
367
|
+
function pointOrPlaced(step2, key, name, placed) {
|
|
368
|
+
if (placed && step2[key] === void 0) step2[key] = [0, 0];
|
|
369
|
+
point(step2[key], `${name}.${key}`);
|
|
370
370
|
}
|
|
371
|
-
function validateStep(
|
|
372
|
-
object(
|
|
371
|
+
function validateStep(step2, name, ids, inside, inRow = false) {
|
|
372
|
+
object(step2, name);
|
|
373
373
|
if (++ids.count > 500) throw new Error(`${name}: a recipe draws at most 500 steps, counting those inside repeat and when.`);
|
|
374
374
|
if (inside) {
|
|
375
375
|
for (const key of ["chapter", "say", "duration"]) {
|
|
376
|
-
if (
|
|
376
|
+
if (step2[key] !== void 0) throw new Error(`${name}.${key}: put it on the ${inside} step itself, not on a step inside it.`);
|
|
377
377
|
}
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
380
|
-
if (ARRANGEMENTS.includes(inside) &&
|
|
378
|
+
if (step2.type === "pause") throw new Error(`${name}: a pause cannot go inside ${inside}.`);
|
|
379
|
+
if (step2.type === "repeat" && inside === "repeat") throw new Error(`${name}: a repeat cannot go inside another repeat.`);
|
|
380
|
+
if (ARRANGEMENTS.includes(inside) && step2.place !== void 0) throw new Error(`${name}.place: the ${inside} places the steps inside it.`);
|
|
381
381
|
}
|
|
382
|
-
if (!STEP_TYPES.includes(
|
|
383
|
-
for (const key of ["id", "say", "chapter"]) optionalText(
|
|
384
|
-
if (
|
|
385
|
-
if (
|
|
386
|
-
if (["pause", "move", "link"].includes(
|
|
387
|
-
if (!Array.isArray(
|
|
388
|
-
|
|
382
|
+
if (!STEP_TYPES.includes(step2.type)) throw new Error(`${name}: unknown type "${step2.type}".`);
|
|
383
|
+
for (const key of ["id", "say", "chapter"]) optionalText(step2[key], `${name}.${key}`);
|
|
384
|
+
if (step2.duration !== void 0) finite(step2.duration, `${name}.duration`, 0, 3e4);
|
|
385
|
+
if (step2.nudge !== void 0) {
|
|
386
|
+
if (["pause", "move", "link"].includes(step2.type)) throw new Error(`${name}.nudge: a ${step2.type} has no place of its own to move.`);
|
|
387
|
+
if (!Array.isArray(step2.nudge) || step2.nudge.length !== 2) throw new Error(`${name}.nudge: use [dx, dy].`);
|
|
388
|
+
step2.nudge.forEach((v, k) => finite(v, `${name}.nudge[${k}]`, -900, 900));
|
|
389
389
|
}
|
|
390
390
|
const arranged = inside !== null && ARRANGEMENTS.includes(inside) || inRow;
|
|
391
|
-
if (
|
|
392
|
-
if (!PLACEABLE.includes(
|
|
393
|
-
object(
|
|
394
|
-
text(
|
|
395
|
-
if (!known(ids, String(
|
|
396
|
-
if (!["below", "above", "left", "right"].includes(
|
|
397
|
-
if (
|
|
398
|
-
choice(
|
|
399
|
-
}
|
|
400
|
-
const placed = arranged ||
|
|
401
|
-
if (
|
|
402
|
-
object(
|
|
391
|
+
if (step2.place !== void 0) {
|
|
392
|
+
if (!PLACEABLE.includes(step2.type)) throw new Error(`${name}.place: only ${PLACEABLE.join(", ")} can be placed.`);
|
|
393
|
+
object(step2.place, `${name}.place`);
|
|
394
|
+
text(step2.place.to, `${name}.place.to`);
|
|
395
|
+
if (!known(ids, String(step2.place.to))) throw new Error(`${name}.place.to: no earlier step has the id "${step2.place.to}".`);
|
|
396
|
+
if (!["below", "above", "left", "right"].includes(step2.place.side)) throw new Error(`${name}.place.side: choose one of below, above, left, right.`);
|
|
397
|
+
if (step2.place.gap !== void 0) finite(step2.place.gap, `${name}.place.gap`, -200, 400);
|
|
398
|
+
choice(step2.place.align, ["center", "start", "end"], `${name}.place.align`);
|
|
399
|
+
}
|
|
400
|
+
const placed = arranged || step2.place !== void 0;
|
|
401
|
+
if (step2.style !== void 0) {
|
|
402
|
+
object(step2.style, `${name}.style`);
|
|
403
403
|
for (const key of ["color", "fill"]) {
|
|
404
|
-
const color =
|
|
404
|
+
const color = step2.style[key];
|
|
405
405
|
if (color !== void 0 && (typeof color !== "string" || !/^(#[\da-f]{3,8}|[a-z]+)$/i.test(color))) {
|
|
406
406
|
throw new Error(`${name}.style.${key}: use a hex colour or a colour name, no URLs.`);
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
for (const key of ["width", "roughness", "bowing"]) {
|
|
410
|
-
if (
|
|
410
|
+
if (step2.style[key] !== void 0) finite(step2.style[key], `${name}.style.${key}`, 0, 20);
|
|
411
411
|
}
|
|
412
|
-
if (
|
|
413
|
-
if (
|
|
414
|
-
choice(
|
|
412
|
+
if (step2.style.opacity !== void 0) finite(step2.style.opacity, `${name}.style.opacity`, 0, 1);
|
|
413
|
+
if (step2.style.dashed !== void 0 && typeof step2.style.dashed !== "boolean") throw new Error(`${name}: dashed must be a boolean.`);
|
|
414
|
+
choice(step2.style.fillStyle, ["hachure", "solid", "zigzag", "cross-hatch", "dots"], `${name}.style.fillStyle`);
|
|
415
415
|
}
|
|
416
|
-
switch (
|
|
416
|
+
switch (step2.type) {
|
|
417
417
|
case "line":
|
|
418
418
|
case "arrow":
|
|
419
419
|
case "highlight":
|
|
420
|
-
point(
|
|
421
|
-
point(
|
|
422
|
-
if (
|
|
420
|
+
point(step2.from, `${name}.from`);
|
|
421
|
+
point(step2.to, `${name}.to`);
|
|
422
|
+
if (step2.thickness !== void 0) finite(step2.thickness, `${name}.thickness`, 1, 200);
|
|
423
423
|
break;
|
|
424
424
|
case "polyline":
|
|
425
425
|
case "polygon":
|
|
426
426
|
case "curve":
|
|
427
|
-
if (!Array.isArray(
|
|
428
|
-
throw new Error(`${name}: points needs ${
|
|
427
|
+
if (!Array.isArray(step2.points) || step2.points.length < (step2.type === "polygon" ? 3 : 2) || step2.points.length > 500) {
|
|
428
|
+
throw new Error(`${name}: points needs ${step2.type === "polygon" ? 3 : 2} to 500 points.`);
|
|
429
429
|
}
|
|
430
|
-
|
|
430
|
+
step2.points.forEach((p, j) => point(p, `${name}.points[${j}]`));
|
|
431
431
|
break;
|
|
432
432
|
case "circle":
|
|
433
433
|
case "ellipse":
|
|
434
|
-
pointOrPlaced(
|
|
435
|
-
for (const key of
|
|
436
|
-
numeric(
|
|
437
|
-
if (typeof
|
|
434
|
+
pointOrPlaced(step2, "center", name, placed);
|
|
435
|
+
for (const key of step2.type === "circle" ? ["r"] : ["rx", "ry"]) {
|
|
436
|
+
numeric(step2[key], `${name}.${key}`);
|
|
437
|
+
if (typeof step2[key] === "number") finite(step2[key], `${name}.${key}`, 0);
|
|
438
438
|
}
|
|
439
439
|
break;
|
|
440
440
|
case "text":
|
|
441
|
-
pointOrPlaced(
|
|
442
|
-
text(
|
|
443
|
-
if (
|
|
444
|
-
if (
|
|
445
|
-
choice(
|
|
446
|
-
choice(
|
|
441
|
+
pointOrPlaced(step2, "at", name, placed);
|
|
442
|
+
text(step2.text, `${name}.text`);
|
|
443
|
+
if (step2.size !== void 0) finite(step2.size, `${name}.size`, 1, 200);
|
|
444
|
+
if (step2.rotate !== void 0) finite(step2.rotate, `${name}.rotate`, -360, 360);
|
|
445
|
+
choice(step2.font, ["hand", "body"], `${name}.font`);
|
|
446
|
+
choice(step2.anchor, ["start", "middle", "end"], `${name}.anchor`);
|
|
447
447
|
break;
|
|
448
448
|
case "card":
|
|
449
|
-
pointOrPlaced(
|
|
450
|
-
numeric(
|
|
451
|
-
numeric(
|
|
452
|
-
for (const key of ["w", "h"]) if (typeof
|
|
453
|
-
text(
|
|
454
|
-
if (
|
|
455
|
-
if (!Array.isArray(
|
|
456
|
-
|
|
449
|
+
pointOrPlaced(step2, "at", name, placed);
|
|
450
|
+
numeric(step2.w, `${name}.w`);
|
|
451
|
+
numeric(step2.h, `${name}.h`);
|
|
452
|
+
for (const key of ["w", "h"]) if (typeof step2[key] === "number") finite(step2[key], `${name}.${key}`, 40);
|
|
453
|
+
text(step2.title, `${name}.title`);
|
|
454
|
+
if (step2.lines !== void 0) {
|
|
455
|
+
if (!Array.isArray(step2.lines) || step2.lines.length > 6) throw new Error(`${name}: at most 6 lines per card.`);
|
|
456
|
+
step2.lines.forEach((line2, j) => text(line2, `${name}.lines[${j}]`));
|
|
457
457
|
}
|
|
458
458
|
break;
|
|
459
459
|
case "plot":
|
|
460
|
-
text(
|
|
461
|
-
if (
|
|
462
|
-
text(
|
|
463
|
-
identifier(String(
|
|
460
|
+
text(step2.fn, `${name}.fn`);
|
|
461
|
+
if (step2.var !== void 0) {
|
|
462
|
+
text(step2.var, `${name}.var`);
|
|
463
|
+
identifier(String(step2.var));
|
|
464
464
|
}
|
|
465
|
-
point(
|
|
466
|
-
point(
|
|
467
|
-
object(
|
|
468
|
-
for (const key of ["x", "y", "w", "h"]) numeric(
|
|
469
|
-
if (
|
|
470
|
-
finite(
|
|
471
|
-
if (!Number.isInteger(
|
|
465
|
+
point(step2.domain, `${name}.domain`);
|
|
466
|
+
point(step2.range, `${name}.range`);
|
|
467
|
+
object(step2.box, `${name}.box`);
|
|
468
|
+
for (const key of ["x", "y", "w", "h"]) numeric(step2.box[key], `${name}.box.${key}`);
|
|
469
|
+
if (step2.samples !== void 0) {
|
|
470
|
+
finite(step2.samples, `${name}.samples`, 2, 1e3);
|
|
471
|
+
if (!Number.isInteger(step2.samples)) throw new Error(`${name}: samples must be an integer.`);
|
|
472
472
|
}
|
|
473
473
|
break;
|
|
474
474
|
case "pause":
|
|
475
|
-
finite(
|
|
476
|
-
optionalText(
|
|
475
|
+
finite(step2.ms, `${name}.ms`, 0, 3e4);
|
|
476
|
+
optionalText(step2.ask, `${name}.ask`);
|
|
477
477
|
break;
|
|
478
478
|
case "prop":
|
|
479
|
-
if (!isPropName(
|
|
480
|
-
throw new Error(`${name}.name: unknown prop "${String(
|
|
479
|
+
if (!isPropName(step2.name)) {
|
|
480
|
+
throw new Error(`${name}.name: unknown prop "${String(step2.name)}"; use one of ${PROP_NAMES.join(", ")}.`);
|
|
481
481
|
}
|
|
482
|
-
pointOrPlaced(
|
|
483
|
-
if (
|
|
484
|
-
numeric(
|
|
485
|
-
if (typeof
|
|
482
|
+
pointOrPlaced(step2, "at", name, placed);
|
|
483
|
+
if (step2.size !== void 0) {
|
|
484
|
+
numeric(step2.size, `${name}.size`);
|
|
485
|
+
if (typeof step2.size === "number") finite(step2.size, `${name}.size`, 10, 1e3);
|
|
486
486
|
}
|
|
487
|
-
optionalText(
|
|
488
|
-
if (
|
|
489
|
-
if (
|
|
490
|
-
if (!hasFace(
|
|
491
|
-
choice(
|
|
487
|
+
optionalText(step2.label, `${name}.label`);
|
|
488
|
+
if (step2.flip !== void 0 && typeof step2.flip !== "boolean") throw new Error(`${name}.flip: expected true or false.`);
|
|
489
|
+
if (step2.mood !== void 0) {
|
|
490
|
+
if (!hasFace(step2.name)) throw new Error(`${name}.mood: only a person (person, person-pointing, person-sitting) has a face.`);
|
|
491
|
+
choice(step2.mood, MOODS, `${name}.mood`);
|
|
492
492
|
}
|
|
493
493
|
break;
|
|
494
494
|
case "repeat":
|
|
495
|
-
numeric(
|
|
496
|
-
if (typeof
|
|
497
|
-
finite(
|
|
498
|
-
if (!Number.isInteger(
|
|
495
|
+
numeric(step2.times, `${name}.times`);
|
|
496
|
+
if (typeof step2.times === "number") {
|
|
497
|
+
finite(step2.times, `${name}.times`, 0, MAX_REPEAT);
|
|
498
|
+
if (!Number.isInteger(step2.times)) throw new Error(`${name}.times: expected a whole number.`);
|
|
499
499
|
}
|
|
500
|
-
validateStep(
|
|
500
|
+
validateStep(step2.step, `${name}.step`, ids, "repeat", arranged);
|
|
501
501
|
break;
|
|
502
502
|
case "when":
|
|
503
|
-
numeric(
|
|
504
|
-
if (!Array.isArray(
|
|
503
|
+
numeric(step2.if, `${name}.if`);
|
|
504
|
+
if (!Array.isArray(step2.steps) || step2.steps.length === 0 || step2.steps.length > 50) {
|
|
505
505
|
throw new Error(`${name}.steps: expected 1 to 50 steps.`);
|
|
506
506
|
}
|
|
507
|
-
|
|
507
|
+
step2.steps.forEach((inner, j) => validateStep(inner, `${name}.steps[${j}]`, ids, inside ?? "when"));
|
|
508
508
|
break;
|
|
509
509
|
case "move":
|
|
510
|
-
text(
|
|
511
|
-
if (!known(ids, String(
|
|
512
|
-
point(
|
|
513
|
-
choice(
|
|
514
|
-
if (
|
|
510
|
+
text(step2.target, `${name}.target`);
|
|
511
|
+
if (!known(ids, String(step2.target))) throw new Error(`${name}.target: no earlier step has the id "${step2.target}".`);
|
|
512
|
+
point(step2.to, `${name}.to`);
|
|
513
|
+
choice(step2.along, ["line", "curve"], `${name}.along`);
|
|
514
|
+
if (step2.ms !== void 0) finite(step2.ms, `${name}.ms`, 200, 5e3);
|
|
515
515
|
break;
|
|
516
516
|
case "link":
|
|
517
517
|
for (const key of ["from", "to"]) {
|
|
518
|
-
text(
|
|
519
|
-
if (!known(ids, String(
|
|
518
|
+
text(step2[key], `${name}.${key}`);
|
|
519
|
+
if (!known(ids, String(step2[key]))) throw new Error(`${name}.${key}: no earlier step has the id "${step2[key]}".`);
|
|
520
520
|
}
|
|
521
|
-
optionalText(
|
|
521
|
+
optionalText(step2.label, `${name}.label`);
|
|
522
522
|
break;
|
|
523
523
|
case "counter":
|
|
524
|
-
pointOrPlaced(
|
|
525
|
-
numeric(
|
|
526
|
-
if (
|
|
527
|
-
text(
|
|
528
|
-
if (!String(
|
|
524
|
+
pointOrPlaced(step2, "at", name, placed);
|
|
525
|
+
numeric(step2.value, `${name}.value`);
|
|
526
|
+
if (step2.format !== void 0) {
|
|
527
|
+
text(step2.format, `${name}.format`);
|
|
528
|
+
if (!String(step2.format).includes("{v")) throw new Error(`${name}.format: show the number as {v}, e.g. "R$ {v:0}".`);
|
|
529
529
|
}
|
|
530
|
-
if (
|
|
531
|
-
choice(
|
|
530
|
+
if (step2.size !== void 0) finite(step2.size, `${name}.size`, 1, 200);
|
|
531
|
+
choice(step2.anchor, ["start", "middle", "end"], `${name}.anchor`);
|
|
532
532
|
break;
|
|
533
533
|
case "row":
|
|
534
534
|
case "column":
|
|
535
535
|
case "grid":
|
|
536
|
-
if (arranged) throw new Error(`${name}: a ${
|
|
537
|
-
pointOrPlaced(
|
|
538
|
-
if (
|
|
539
|
-
if (
|
|
540
|
-
finite(
|
|
541
|
-
if (!Number.isInteger(
|
|
542
|
-
} else choice(
|
|
543
|
-
if (!Array.isArray(
|
|
536
|
+
if (arranged) throw new Error(`${name}: a ${step2.type} cannot go inside another ${inside}.`);
|
|
537
|
+
pointOrPlaced(step2, "at", name, false);
|
|
538
|
+
if (step2.gap !== void 0) finite(step2.gap, `${name}.gap`, 0, 400);
|
|
539
|
+
if (step2.type === "grid") {
|
|
540
|
+
finite(step2.columns, `${name}.columns`, 1, 12);
|
|
541
|
+
if (!Number.isInteger(step2.columns)) throw new Error(`${name}.columns: expected a whole number.`);
|
|
542
|
+
} else choice(step2.align, ["center", "start", "end"], `${name}.align`);
|
|
543
|
+
if (!Array.isArray(step2.steps) || step2.steps.length === 0 || step2.steps.length > 50) {
|
|
544
544
|
throw new Error(`${name}.steps: expected 1 to 50 steps.`);
|
|
545
545
|
}
|
|
546
|
-
|
|
546
|
+
step2.steps.forEach((inner, j) => {
|
|
547
547
|
object(inner, `${name}.steps[${j}]`);
|
|
548
548
|
const kind = inner.type === "repeat" ? inner.step?.type : inner.type;
|
|
549
|
-
if (!PLACEABLE.includes(kind)) throw new Error(`${name}.steps[${j}]: a ${
|
|
550
|
-
validateStep(inner, `${name}.steps[${j}]`, ids,
|
|
549
|
+
if (!PLACEABLE.includes(kind)) throw new Error(`${name}.steps[${j}]: a ${step2.type} holds only ${PLACEABLE.join(", ")} (or a repeat of one).`);
|
|
550
|
+
validateStep(inner, `${name}.steps[${j}]`, ids, step2.type);
|
|
551
551
|
});
|
|
552
552
|
break;
|
|
553
553
|
case "axis":
|
|
554
554
|
case "timeline":
|
|
555
|
-
point(
|
|
556
|
-
point(
|
|
557
|
-
point(
|
|
558
|
-
if (
|
|
559
|
-
if (
|
|
560
|
-
if (Array.isArray(
|
|
561
|
-
if (
|
|
562
|
-
|
|
555
|
+
point(step2.from, `${name}.from`);
|
|
556
|
+
point(step2.to, `${name}.to`);
|
|
557
|
+
point(step2.domain, `${name}.domain`);
|
|
558
|
+
if (step2.type === "axis") {
|
|
559
|
+
if (step2.ticks !== void 0) {
|
|
560
|
+
if (Array.isArray(step2.ticks)) {
|
|
561
|
+
if (step2.ticks.length > 30) throw new Error(`${name}.ticks: at most 30 marks.`);
|
|
562
|
+
step2.ticks.forEach((v, j) => finite(v, `${name}.ticks[${j}]`));
|
|
563
563
|
} else {
|
|
564
|
-
finite(
|
|
565
|
-
if (!Number.isInteger(
|
|
564
|
+
finite(step2.ticks, `${name}.ticks`, 0, 30);
|
|
565
|
+
if (!Number.isInteger(step2.ticks)) throw new Error(`${name}.ticks: expected a whole number.`);
|
|
566
566
|
}
|
|
567
567
|
}
|
|
568
|
-
optionalText(
|
|
569
|
-
if (
|
|
570
|
-
text(
|
|
571
|
-
if (!String(
|
|
568
|
+
optionalText(step2.label, `${name}.label`);
|
|
569
|
+
if (step2.format !== void 0) {
|
|
570
|
+
text(step2.format, `${name}.format`);
|
|
571
|
+
if (!String(step2.format).includes("{v")) throw new Error(`${name}.format: show the number as {v}, e.g. "{v} km".`);
|
|
572
572
|
}
|
|
573
573
|
} else {
|
|
574
|
-
if (!Array.isArray(
|
|
575
|
-
|
|
574
|
+
if (!Array.isArray(step2.marks) || step2.marks.length === 0 || step2.marks.length > 20) throw new Error(`${name}.marks: expected 1 to 20 marks.`);
|
|
575
|
+
step2.marks.forEach((m, j) => {
|
|
576
576
|
object(m, `${name}.marks[${j}]`);
|
|
577
577
|
numeric(m.at, `${name}.marks[${j}].at`);
|
|
578
578
|
optionalText(m.label, `${name}.marks[${j}].label`);
|
|
@@ -581,60 +581,60 @@ function validateStep(step, name, ids, inside, inRow = false) {
|
|
|
581
581
|
}
|
|
582
582
|
break;
|
|
583
583
|
case "balloon":
|
|
584
|
-
text(
|
|
585
|
-
if (!known(ids, String(
|
|
586
|
-
text(
|
|
587
|
-
if (String(
|
|
588
|
-
choice(
|
|
589
|
-
choice(
|
|
590
|
-
if (
|
|
591
|
-
if (
|
|
584
|
+
text(step2.to, `${name}.to`);
|
|
585
|
+
if (!known(ids, String(step2.to))) throw new Error(`${name}.to: no earlier step has the id "${step2.to}".`);
|
|
586
|
+
text(step2.text, `${name}.text`);
|
|
587
|
+
if (String(step2.text).length > 300) throw new Error(`${name}.text: keep a balloon under 300 characters.`);
|
|
588
|
+
choice(step2.side, ["below", "above", "left", "right"], `${name}.side`);
|
|
589
|
+
choice(step2.kind, ["speech", "thought"], `${name}.kind`);
|
|
590
|
+
if (step2.width !== void 0) finite(step2.width, `${name}.width`, 80, 600);
|
|
591
|
+
if (step2.size !== void 0) finite(step2.size, `${name}.size`, 12, 60);
|
|
592
592
|
break;
|
|
593
593
|
case "brace":
|
|
594
|
-
point(
|
|
595
|
-
point(
|
|
596
|
-
optionalText(
|
|
597
|
-
if (
|
|
594
|
+
point(step2.from, `${name}.from`);
|
|
595
|
+
point(step2.to, `${name}.to`);
|
|
596
|
+
optionalText(step2.label, `${name}.label`);
|
|
597
|
+
if (step2.flip !== void 0 && typeof step2.flip !== "boolean") throw new Error(`${name}.flip: expected true or false.`);
|
|
598
598
|
break;
|
|
599
599
|
case "note":
|
|
600
|
-
point(
|
|
601
|
-
text(
|
|
602
|
-
if (
|
|
603
|
-
if (
|
|
604
|
-
text(
|
|
605
|
-
if (!known(ids, String(
|
|
606
|
-
} else point(
|
|
607
|
-
if (
|
|
600
|
+
point(step2.at, `${name}.at`);
|
|
601
|
+
text(step2.text, `${name}.text`);
|
|
602
|
+
if (step2.to === void 0 === (step2.point === void 0)) throw new Error(`${name}: a note points either to an element (to) or to a point (point).`);
|
|
603
|
+
if (step2.to !== void 0) {
|
|
604
|
+
text(step2.to, `${name}.to`);
|
|
605
|
+
if (!known(ids, String(step2.to))) throw new Error(`${name}.to: no earlier step has the id "${step2.to}".`);
|
|
606
|
+
} else point(step2.point, `${name}.point`);
|
|
607
|
+
if (step2.size !== void 0) finite(step2.size, `${name}.size`, 12, 60);
|
|
608
608
|
break;
|
|
609
609
|
case "table":
|
|
610
|
-
point(
|
|
611
|
-
if (!Array.isArray(
|
|
612
|
-
|
|
610
|
+
point(step2.at, `${name}.at`);
|
|
611
|
+
if (!Array.isArray(step2.rows) || step2.rows.length === 0 || step2.rows.length > 12) throw new Error(`${name}.rows: expected 1 to 12 rows.`);
|
|
612
|
+
step2.rows.forEach((row, r) => {
|
|
613
613
|
if (!Array.isArray(row) || row.length === 0 || row.length > 6) throw new Error(`${name}.rows[${r}]: expected 1 to 6 cells.`);
|
|
614
614
|
row.forEach((cell, c) => {
|
|
615
615
|
if (typeof cell !== "string" || cell.length > 120) throw new Error(`${name}.rows[${r}][${c}]: expected text of up to 120 characters.`);
|
|
616
616
|
});
|
|
617
617
|
});
|
|
618
|
-
if (
|
|
619
|
-
if (
|
|
618
|
+
if (step2.header !== void 0 && typeof step2.header !== "boolean") throw new Error(`${name}.header: expected true or false.`);
|
|
619
|
+
if (step2.size !== void 0) finite(step2.size, `${name}.size`, 12, 40);
|
|
620
620
|
break;
|
|
621
621
|
case "bars":
|
|
622
|
-
object(
|
|
623
|
-
for (const key of ["x", "y", "w", "h"]) numeric(
|
|
624
|
-
if (!Array.isArray(
|
|
625
|
-
|
|
626
|
-
if (
|
|
627
|
-
if (!Array.isArray(
|
|
628
|
-
|
|
622
|
+
object(step2.box, `${name}.box`);
|
|
623
|
+
for (const key of ["x", "y", "w", "h"]) numeric(step2.box[key], `${name}.box.${key}`);
|
|
624
|
+
if (!Array.isArray(step2.values) || step2.values.length === 0 || step2.values.length > 12) throw new Error(`${name}.values: expected 1 to 12 values.`);
|
|
625
|
+
step2.values.forEach((v, j) => numeric(v, `${name}.values[${j}]`));
|
|
626
|
+
if (step2.labels !== void 0) {
|
|
627
|
+
if (!Array.isArray(step2.labels) || step2.labels.length > step2.values.length) throw new Error(`${name}.labels: at most one label per value.`);
|
|
628
|
+
step2.labels.forEach((l, j) => text(l, `${name}.labels[${j}]`));
|
|
629
629
|
}
|
|
630
|
-
if (
|
|
631
|
-
if (
|
|
632
|
-
text(
|
|
633
|
-
if (!String(
|
|
630
|
+
if (step2.max !== void 0) numeric(step2.max, `${name}.max`);
|
|
631
|
+
if (step2.format !== void 0) {
|
|
632
|
+
text(step2.format, `${name}.format`);
|
|
633
|
+
if (!String(step2.format).includes("{v")) throw new Error(`${name}.format: show the number as {v}, e.g. "{v}%".`);
|
|
634
634
|
}
|
|
635
635
|
break;
|
|
636
636
|
}
|
|
637
|
-
if (typeof
|
|
637
|
+
if (typeof step2.id === "string") (inside === "repeat" ? ids.repeated : ids.exact).add(step2.id);
|
|
638
638
|
}
|
|
639
639
|
function validateScene(data) {
|
|
640
640
|
object(data, "The recipe");
|
|
@@ -679,9 +679,9 @@ function validateScene(data) {
|
|
|
679
679
|
}
|
|
680
680
|
if (s.vars !== void 0) {
|
|
681
681
|
object(s.vars, "vars");
|
|
682
|
-
for (const [key,
|
|
682
|
+
for (const [key, value2] of Object.entries(s.vars)) {
|
|
683
683
|
identifier(key);
|
|
684
|
-
numeric(
|
|
684
|
+
numeric(value2, `vars.${key}`);
|
|
685
685
|
}
|
|
686
686
|
}
|
|
687
687
|
if (s.switches !== void 0) {
|
|
@@ -702,7 +702,7 @@ function validateScene(data) {
|
|
|
702
702
|
throw new Error("A recipe needs 1 to 250 steps.");
|
|
703
703
|
}
|
|
704
704
|
const ids = { exact: /* @__PURE__ */ new Set(), repeated: /* @__PURE__ */ new Set(), count: 0 };
|
|
705
|
-
s.steps.forEach((
|
|
705
|
+
s.steps.forEach((step2, i) => validateStep(step2, `Step ${i + 1}`, ids, null));
|
|
706
706
|
return {
|
|
707
707
|
...s,
|
|
708
708
|
id: s.id ?? "pasted",
|
|
@@ -711,14 +711,573 @@ function validateScene(data) {
|
|
|
711
711
|
};
|
|
712
712
|
}
|
|
713
713
|
function chaptersFor(scene, first = "Start") {
|
|
714
|
-
const starts = scene.steps.flatMap((
|
|
714
|
+
const starts = scene.steps.flatMap((step2, start) => step2.chapter ? [{ title: step2.chapter, start }] : []);
|
|
715
715
|
if (!starts.length || starts[0].start !== 0) starts.unshift({ title: first, start: 0 });
|
|
716
716
|
return starts.map((chapter, i) => ({ ...chapter, end: (starts[i + 1]?.start ?? scene.steps.length) - 1 }));
|
|
717
717
|
}
|
|
718
|
+
|
|
719
|
+
var ChalkTextError = class extends Error {
|
|
720
|
+
line;
|
|
721
|
+
constructor(line2, message) {
|
|
722
|
+
super(`line ${line2}: ${message}`);
|
|
723
|
+
this.line = line2;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
function tokenize(text2, no) {
|
|
727
|
+
const out = [];
|
|
728
|
+
let cur = "";
|
|
729
|
+
let quoted = false;
|
|
730
|
+
for (let i = 0; i < text2.length; i++) {
|
|
731
|
+
const c = text2[i];
|
|
732
|
+
if (c === "\\" && quoted && i + 1 < text2.length) {
|
|
733
|
+
cur += c + text2[++i];
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (c === '"') quoted = !quoted;
|
|
737
|
+
if (/\s/.test(c) && !quoted) {
|
|
738
|
+
if (cur) out.push(cur);
|
|
739
|
+
cur = "";
|
|
740
|
+
} else cur += c;
|
|
741
|
+
}
|
|
742
|
+
if (quoted) throw new ChalkTextError(no, "a quote is not closed.");
|
|
743
|
+
if (cur) out.push(cur);
|
|
744
|
+
return out;
|
|
745
|
+
}
|
|
746
|
+
function treeOf(text2) {
|
|
747
|
+
const root = { no: 0, indent: -1, tokens: [], children: [] };
|
|
748
|
+
const stack = [root];
|
|
749
|
+
text2.split(/\r?\n/).forEach((raw, i) => {
|
|
750
|
+
const no = i + 1;
|
|
751
|
+
if (!raw.trim() || raw.trim().startsWith("//")) return;
|
|
752
|
+
const indent = raw.length - raw.trimStart().length;
|
|
753
|
+
const body = raw.trim();
|
|
754
|
+
const tokens = body.startsWith("#") ? ["#", body.slice(1).trim()] : body.startsWith(">") ? [">", body.slice(1).trim()] : body.startsWith("json ") ? ["json", body.slice(5).trim()] : body.startsWith("|") ? ["|", body] : tokenize(body, no);
|
|
755
|
+
const line2 = { no, indent, tokens, children: [] };
|
|
756
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
757
|
+
stack[stack.length - 1].children.push(line2);
|
|
758
|
+
stack.push(line2);
|
|
759
|
+
});
|
|
760
|
+
return root.children;
|
|
761
|
+
}
|
|
762
|
+
var isQuoted = (t) => t.length >= 2 && t.startsWith('"') && t.endsWith('"');
|
|
763
|
+
var unquote = (t) => isQuoted(t) ? t.slice(1, -1).replace(/\\(.)/g, "$1") : t;
|
|
764
|
+
var NUMBER = /^-?\d+(\.\d+)?$/;
|
|
765
|
+
function value(t, no) {
|
|
766
|
+
if (NUMBER.test(t)) return Number(t);
|
|
767
|
+
if (isQuoted(t)) return unquote(t);
|
|
768
|
+
if (/^[A-Za-z_][A-Za-z_0-9]*$/.test(t)) return t;
|
|
769
|
+
throw new ChalkTextError(no, `expected a number or an expression in quotes, got ${t}.`);
|
|
770
|
+
}
|
|
771
|
+
function parts(t) {
|
|
772
|
+
const out = [];
|
|
773
|
+
let cur = "";
|
|
774
|
+
let quoted = false;
|
|
775
|
+
for (const c of t) {
|
|
776
|
+
if (c === '"') quoted = !quoted;
|
|
777
|
+
if (c === "," && !quoted) {
|
|
778
|
+
out.push(cur);
|
|
779
|
+
cur = "";
|
|
780
|
+
} else cur += c;
|
|
781
|
+
}
|
|
782
|
+
out.push(cur);
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
function point2(t, no) {
|
|
786
|
+
const p = t ? parts(t) : [];
|
|
787
|
+
if (p.length !== 2) throw new ChalkTextError(no, `expected a point x,y, got ${t ?? "nothing"}.`);
|
|
788
|
+
return [value(p[0], no), value(p[1], no)];
|
|
789
|
+
}
|
|
790
|
+
function range(t, no) {
|
|
791
|
+
const m = t?.match(/^(.+?)\.\.(.+)$/);
|
|
792
|
+
if (!m) throw new ChalkTextError(no, `expected a range a..b, got ${t ?? "nothing"}.`);
|
|
793
|
+
return [value(m[1], no), value(m[2], no)];
|
|
794
|
+
}
|
|
795
|
+
var STEP_WORDS = /* @__PURE__ */ new Set([
|
|
796
|
+
"text",
|
|
797
|
+
"card",
|
|
798
|
+
"line",
|
|
799
|
+
"arrow",
|
|
800
|
+
"highlight",
|
|
801
|
+
"polyline",
|
|
802
|
+
"polygon",
|
|
803
|
+
"curve",
|
|
804
|
+
"circle",
|
|
805
|
+
"ellipse",
|
|
806
|
+
"plot",
|
|
807
|
+
"pause",
|
|
808
|
+
"row",
|
|
809
|
+
"column",
|
|
810
|
+
"grid",
|
|
811
|
+
"repeat",
|
|
812
|
+
"when",
|
|
813
|
+
"move",
|
|
814
|
+
"counter",
|
|
815
|
+
"axis",
|
|
816
|
+
"timeline",
|
|
817
|
+
"brace",
|
|
818
|
+
"note",
|
|
819
|
+
"table",
|
|
820
|
+
"bars",
|
|
821
|
+
"json"
|
|
822
|
+
]);
|
|
823
|
+
var SIDES = { "right-of": "right", "left-of": "left", above: "above", below: "below" };
|
|
824
|
+
var STYLE = ["color", "fill", "roughness", "bowing", "fillStyle", "opacity"];
|
|
825
|
+
var SPACED = /* @__PURE__ */ new Set(["gap", "size", "r", "rx", "ry", "w", "h", "columns", "ms", "thickness"]);
|
|
826
|
+
var FLAGS = /* @__PURE__ */ new Set(["dashed", "flip", "noheader"]);
|
|
827
|
+
function attrs(tokens, step2, no) {
|
|
828
|
+
const style = {};
|
|
829
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
830
|
+
const t = tokens[i];
|
|
831
|
+
let key;
|
|
832
|
+
let raw;
|
|
833
|
+
if (FLAGS.has(t)) {
|
|
834
|
+
if (t === "dashed") style.dashed = true;
|
|
835
|
+
else if (t === "flip") step2.flip = true;
|
|
836
|
+
else step2.header = false;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const eq = t.indexOf("=");
|
|
840
|
+
if (eq > 0 && !isQuoted(t)) [key, raw] = [t.slice(0, eq), t.slice(eq + 1)];
|
|
841
|
+
else if (SPACED.has(t) && i + 1 < tokens.length) [key, raw] = [t, tokens[++i]];
|
|
842
|
+
else throw new ChalkTextError(no, `did not understand \xAB${t}\xBB.`);
|
|
843
|
+
if (key === "stroke") style.width = Number(raw);
|
|
844
|
+
else if (STYLE.includes(key)) style[key] = NUMBER.test(raw) ? Number(raw) : unquote(raw);
|
|
845
|
+
else if (key === "nudge") step2.nudge = point2(raw, no).map(Number);
|
|
846
|
+
else if (key === "ticks") step2.ticks = raw.includes(",") ? parts(raw).map(Number) : Number(raw);
|
|
847
|
+
else if (key === "labels") step2.labels = parts(raw).map(unquote);
|
|
848
|
+
else if (key === "id" || key === "anchor" || key === "font" || key === "mood" || key === "along" || key === "side" || key === "kind" || key === "var" || key === "align" || key === "label" || key === "format" || key === "fn") step2[key] = unquote(raw);
|
|
849
|
+
else step2[key] = value(raw, no);
|
|
850
|
+
}
|
|
851
|
+
if (Object.keys(style).length) step2.style = style;
|
|
852
|
+
return step2;
|
|
853
|
+
}
|
|
854
|
+
function placement(tokens, step2, key, no) {
|
|
855
|
+
const [w, arg] = tokens;
|
|
856
|
+
if (w === "at") {
|
|
857
|
+
step2[key] = point2(arg, no);
|
|
858
|
+
return tokens.slice(2);
|
|
859
|
+
}
|
|
860
|
+
if (w in SIDES) {
|
|
861
|
+
if (!arg || isQuoted(arg)) throw new ChalkTextError(no, `${w} needs the id of an earlier step.`);
|
|
862
|
+
step2.place = { to: arg, side: SIDES[w] };
|
|
863
|
+
let rest = tokens.slice(2);
|
|
864
|
+
if (rest[0] === "gap" || rest[0]?.startsWith("gap=")) {
|
|
865
|
+
const g = rest[0] === "gap" ? rest[1] : rest[0].slice(4);
|
|
866
|
+
step2.place.gap = Number(g);
|
|
867
|
+
rest = rest.slice(rest[0] === "gap" ? 2 : 1);
|
|
868
|
+
}
|
|
869
|
+
const align = rest.findIndex((t) => t.startsWith("align="));
|
|
870
|
+
if (align >= 0) {
|
|
871
|
+
;
|
|
872
|
+
step2.place.align = rest[align].slice(6);
|
|
873
|
+
rest = rest.filter((_, i) => i !== align);
|
|
874
|
+
}
|
|
875
|
+
return rest;
|
|
876
|
+
}
|
|
877
|
+
return tokens;
|
|
878
|
+
}
|
|
879
|
+
function step(line2, props) {
|
|
880
|
+
const { no, tokens } = line2;
|
|
881
|
+
const [w, ...rest] = tokens;
|
|
882
|
+
const kids = () => line2.children.map((c) => step(c, props));
|
|
883
|
+
const quoted = (t, what) => {
|
|
884
|
+
if (!t || !isQuoted(t)) throw new ChalkTextError(no, `${w} needs ${what} in quotes.`);
|
|
885
|
+
return unquote(t);
|
|
886
|
+
};
|
|
887
|
+
const fromTo = (type) => {
|
|
888
|
+
if (rest[1] !== "->") throw new ChalkTextError(no, `write ${type} x,y -> x,y.`);
|
|
889
|
+
return { type, from: point2(rest[0], no), to: point2(rest[2], no) };
|
|
890
|
+
};
|
|
891
|
+
if (w === "json") {
|
|
892
|
+
try {
|
|
893
|
+
return JSON.parse(rest[0]);
|
|
894
|
+
} catch {
|
|
895
|
+
throw new ChalkTextError(no, "the JSON on this line is not valid.");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (rest[0] === "->" && !STEP_WORDS.has(w) && !props.has(w)) {
|
|
899
|
+
const s = { type: "link", from: w, to: rest[1] };
|
|
900
|
+
let more = rest.slice(2);
|
|
901
|
+
if (more[0] && isQuoted(more[0])) [s.label, more] = [unquote(more[0]), more.slice(1)];
|
|
902
|
+
return attrs(more, s, no);
|
|
903
|
+
}
|
|
904
|
+
if (rest[0] === "says" || rest[0] === "thinks") {
|
|
905
|
+
const s = { type: "balloon", to: w, text: quoted(rest[1], "what is said"), ...rest[0] === "thinks" ? { kind: "thought" } : {} };
|
|
906
|
+
return attrs(rest.slice(2), s, no);
|
|
907
|
+
}
|
|
908
|
+
if (props.has(w)) {
|
|
909
|
+
const s = { type: "prop", name: w };
|
|
910
|
+
let more = rest;
|
|
911
|
+
const word = more[0];
|
|
912
|
+
if (word && !isQuoted(word) && !word.includes("=") && word !== "at" && !(word in SIDES) && !SPACED.has(word) && !FLAGS.has(word)) [s.id, more] = [word, more.slice(1)];
|
|
913
|
+
more = placement(more, s, "at", no);
|
|
914
|
+
if (more[0] && isQuoted(more[0])) [s.label, more] = [unquote(more[0]), more.slice(1)];
|
|
915
|
+
return attrs(more, s, no);
|
|
916
|
+
}
|
|
917
|
+
switch (w) {
|
|
918
|
+
case "text": {
|
|
919
|
+
const s = { type: "text", text: quoted(rest[0], "its text") };
|
|
920
|
+
return attrs(placement(rest.slice(1), s, "at", no), s, no);
|
|
921
|
+
}
|
|
922
|
+
case "card": {
|
|
923
|
+
const s = { type: "card" };
|
|
924
|
+
let more = placement(rest, s, "at", no);
|
|
925
|
+
const texts = more.filter(isQuoted).map(unquote);
|
|
926
|
+
more = more.filter((t) => !isQuoted(t));
|
|
927
|
+
if (!texts.length) throw new ChalkTextError(no, "a card needs a title in quotes.");
|
|
928
|
+
[s.title] = texts;
|
|
929
|
+
if (texts.length > 1) s.lines = texts.slice(1);
|
|
930
|
+
return attrs(more, s, no);
|
|
931
|
+
}
|
|
932
|
+
case "line":
|
|
933
|
+
case "arrow":
|
|
934
|
+
case "highlight":
|
|
935
|
+
return attrs(rest.slice(3), fromTo(w), no);
|
|
936
|
+
case "brace": {
|
|
937
|
+
const s = fromTo("brace");
|
|
938
|
+
let more = rest.slice(3);
|
|
939
|
+
if (more[0] && isQuoted(more[0])) [s.label, more] = [unquote(more[0]), more.slice(1)];
|
|
940
|
+
return attrs(more, s, no);
|
|
941
|
+
}
|
|
942
|
+
case "polyline":
|
|
943
|
+
case "polygon":
|
|
944
|
+
case "curve": {
|
|
945
|
+
const pts = rest.filter((t) => /,/.test(t) && !t.includes("="));
|
|
946
|
+
return attrs(rest.slice(pts.length), { type: w, points: pts.map((p) => point2(p, no)) }, no);
|
|
947
|
+
}
|
|
948
|
+
case "circle":
|
|
949
|
+
case "ellipse": {
|
|
950
|
+
const s = { type: w };
|
|
951
|
+
return attrs(placement(rest, s, "center", no), s, no);
|
|
952
|
+
}
|
|
953
|
+
case "plot": {
|
|
954
|
+
const s = { type: "plot", fn: quoted(rest[0], "the formula") };
|
|
955
|
+
const more = [];
|
|
956
|
+
for (let i = 1; i < rest.length; i++) {
|
|
957
|
+
if (rest[i] === "domain") s.domain = range(rest[++i], no);
|
|
958
|
+
else if (rest[i] === "range") s.range = range(rest[++i], no);
|
|
959
|
+
else if (rest[i] === "box") {
|
|
960
|
+
const b = parts(rest[++i] ?? "");
|
|
961
|
+
if (b.length !== 4) throw new ChalkTextError(no, "write box x,y,w,h.");
|
|
962
|
+
const [x, y, bw, bh] = b.map((v) => value(v, no));
|
|
963
|
+
s.box = { x, y, w: bw, h: bh };
|
|
964
|
+
} else more.push(rest[i]);
|
|
965
|
+
}
|
|
966
|
+
return attrs(more, s, no);
|
|
967
|
+
}
|
|
968
|
+
case "pause": {
|
|
969
|
+
const s = { type: "pause", ms: 600 };
|
|
970
|
+
let more = rest;
|
|
971
|
+
if (more[0] && NUMBER.test(more[0])) [s.ms, more] = [Number(more[0]), more.slice(1)];
|
|
972
|
+
if (more[0] === "ask") [s.ask, more] = [quoted(more[1], "the question"), more.slice(2)];
|
|
973
|
+
return attrs(more, s, no);
|
|
974
|
+
}
|
|
975
|
+
case "row":
|
|
976
|
+
case "column":
|
|
977
|
+
case "grid": {
|
|
978
|
+
const s = { type: w };
|
|
979
|
+
const more = placement(rest, s, "at", no);
|
|
980
|
+
attrs(more, s, no);
|
|
981
|
+
s.steps = kids();
|
|
982
|
+
return s;
|
|
983
|
+
}
|
|
984
|
+
case "repeat": {
|
|
985
|
+
const head = rest[0]?.replace(/:$/, "");
|
|
986
|
+
if (!head) throw new ChalkTextError(no, "write repeat n: <step>.");
|
|
987
|
+
const s = { type: "repeat", times: value(head, no) };
|
|
988
|
+
const inline = rest.slice(1);
|
|
989
|
+
if (inline.length) s.step = step({ ...line2, tokens: inline, children: [] }, props);
|
|
990
|
+
else if (line2.children.length === 1) [s.step] = kids();
|
|
991
|
+
else throw new ChalkTextError(no, "a repeat holds one step: on the same line after \xAB:\xBB, or one line under it.");
|
|
992
|
+
return s;
|
|
993
|
+
}
|
|
994
|
+
case "when": {
|
|
995
|
+
const cond = rest[0]?.replace(/:$/, "");
|
|
996
|
+
if (!cond) throw new ChalkTextError(no, "write when <condition>:.");
|
|
997
|
+
const inline = rest.slice(1);
|
|
998
|
+
return { type: "when", if: value(cond, no), steps: inline.length ? [step({ ...line2, tokens: inline, children: [] }, props)] : kids() };
|
|
999
|
+
}
|
|
1000
|
+
case "move": {
|
|
1001
|
+
if (rest[1] !== "to") throw new ChalkTextError(no, "write move <id> to x,y.");
|
|
1002
|
+
return attrs(rest.slice(3), { type: "move", target: rest[0], to: point2(rest[2], no) }, no);
|
|
1003
|
+
}
|
|
1004
|
+
case "counter": {
|
|
1005
|
+
const s = { type: "counter" };
|
|
1006
|
+
let more = placement(rest, s, "at", no);
|
|
1007
|
+
s.value = value(more[0] ?? "", no);
|
|
1008
|
+
more = more.slice(1);
|
|
1009
|
+
if (more[0] && isQuoted(more[0])) [s.format, more] = [unquote(more[0]), more.slice(1)];
|
|
1010
|
+
return attrs(more, s, no);
|
|
1011
|
+
}
|
|
1012
|
+
case "axis":
|
|
1013
|
+
case "timeline": {
|
|
1014
|
+
const s = fromTo(w);
|
|
1015
|
+
let more = rest.slice(3);
|
|
1016
|
+
if (more[0] !== "domain") throw new ChalkTextError(no, `write ${w} x,y -> x,y domain a..b.`);
|
|
1017
|
+
s.domain = range(more[1], no);
|
|
1018
|
+
more = more.slice(2);
|
|
1019
|
+
if (w === "timeline") {
|
|
1020
|
+
s.marks = line2.children.map((c) => {
|
|
1021
|
+
const [m, at, label, text2] = c.tokens;
|
|
1022
|
+
if (m !== "mark") throw new ChalkTextError(c.no, 'under a timeline, write mark <when> "label" "text".');
|
|
1023
|
+
return { at: value(at ?? "", c.no), ...label ? { label: unquote(label) } : {}, ...text2 ? { text: unquote(text2) } : {} };
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return attrs(more, s, no);
|
|
1027
|
+
}
|
|
1028
|
+
case "note": {
|
|
1029
|
+
const s = { type: "note" };
|
|
1030
|
+
let more = placement(rest, s, "at", no);
|
|
1031
|
+
s.text = quoted(more[0], "its text");
|
|
1032
|
+
if (more[1] !== "->") throw new ChalkTextError(no, 'write note at x,y "text" -> <id or x,y>.');
|
|
1033
|
+
const target = more[2] ?? "";
|
|
1034
|
+
if (/,/.test(target)) s.point = point2(target, no);
|
|
1035
|
+
else s.to = target;
|
|
1036
|
+
more = more.slice(3);
|
|
1037
|
+
return attrs(more, s, no);
|
|
1038
|
+
}
|
|
1039
|
+
case "table": {
|
|
1040
|
+
const s = { type: "table" };
|
|
1041
|
+
attrs(placement(rest, s, "at", no), s, no);
|
|
1042
|
+
s.rows = line2.children.map((c) => {
|
|
1043
|
+
if (c.tokens[0] !== "|") throw new ChalkTextError(c.no, "under a table, write rows as | a | b |.");
|
|
1044
|
+
return c.tokens[1].replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim());
|
|
1045
|
+
});
|
|
1046
|
+
return s;
|
|
1047
|
+
}
|
|
1048
|
+
case "bars": {
|
|
1049
|
+
const s = { type: "bars" };
|
|
1050
|
+
const more = [];
|
|
1051
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1052
|
+
if (rest[i] === "box") {
|
|
1053
|
+
const b = parts(rest[++i] ?? "");
|
|
1054
|
+
if (b.length !== 4) throw new ChalkTextError(no, "write box x,y,w,h.");
|
|
1055
|
+
const [x, y, bw, bh] = b.map((v) => value(v, no));
|
|
1056
|
+
s.box = { x, y, w: bw, h: bh };
|
|
1057
|
+
} else if (rest[i] === "values") s.values = parts(rest[++i] ?? "").map((v) => value(v, no));
|
|
1058
|
+
else if (rest[i] === "labels") s.labels = parts(rest[++i] ?? "").map(unquote);
|
|
1059
|
+
else more.push(rest[i]);
|
|
1060
|
+
}
|
|
1061
|
+
return attrs(more, s, no);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
throw new ChalkTextError(no, `\xAB${w}\xBB is not a step, a prop or an id followed by -> / says.`);
|
|
1065
|
+
}
|
|
1066
|
+
function parseChalk(text2, props) {
|
|
1067
|
+
const names = new Set(props);
|
|
1068
|
+
const scene = { id: "lesson", title: "", width: 900, height: 560 };
|
|
1069
|
+
const steps = [];
|
|
1070
|
+
let chapter;
|
|
1071
|
+
let say;
|
|
1072
|
+
for (const line2 of treeOf(text2)) {
|
|
1073
|
+
const { no, tokens } = line2;
|
|
1074
|
+
const [w, ...rest] = tokens;
|
|
1075
|
+
const one = (what) => {
|
|
1076
|
+
if (!rest[0]) throw new ChalkTextError(no, `${w} needs ${what}.`);
|
|
1077
|
+
return unquote(rest.join(" "));
|
|
1078
|
+
};
|
|
1079
|
+
const header = !["->", "says", "thinks"].includes(rest[0]);
|
|
1080
|
+
switch (header ? w : "") {
|
|
1081
|
+
case "#":
|
|
1082
|
+
chapter = rest[0];
|
|
1083
|
+
continue;
|
|
1084
|
+
case ">":
|
|
1085
|
+
say = rest[0];
|
|
1086
|
+
continue;
|
|
1087
|
+
case "title":
|
|
1088
|
+
scene.title = one("a title");
|
|
1089
|
+
continue;
|
|
1090
|
+
case "id":
|
|
1091
|
+
scene.id = one("an id");
|
|
1092
|
+
continue;
|
|
1093
|
+
case "category":
|
|
1094
|
+
scene.category = one("a category");
|
|
1095
|
+
continue;
|
|
1096
|
+
case "theme":
|
|
1097
|
+
scene.theme = one("paper or chalkboard");
|
|
1098
|
+
continue;
|
|
1099
|
+
case "locale":
|
|
1100
|
+
scene.locale = one("a language tag");
|
|
1101
|
+
continue;
|
|
1102
|
+
case "background":
|
|
1103
|
+
scene.background = one("dots, grid or plain");
|
|
1104
|
+
continue;
|
|
1105
|
+
case "size": {
|
|
1106
|
+
const m = rest[0]?.match(/^(\d+)x(\d+)$/);
|
|
1107
|
+
if (!m) throw new ChalkTextError(no, "write size 900x560.");
|
|
1108
|
+
[scene.width, scene.height] = [Number(m[1]), Number(m[2])];
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
case "param": {
|
|
1112
|
+
const [name, label, span, ...more] = rest;
|
|
1113
|
+
const [min, max] = range(span, no).map(Number);
|
|
1114
|
+
const p = { label: unquote(label ?? ""), min, max, step: 1, value: min };
|
|
1115
|
+
for (let i = 0; i < more.length; i++) {
|
|
1116
|
+
if (more[i].startsWith("step=")) p.step = Number(more[i].slice(5));
|
|
1117
|
+
else if (more[i] === "=") p.value = Number(more[++i]);
|
|
1118
|
+
else throw new ChalkTextError(no, `did not understand \xAB${more[i]}\xBB.`);
|
|
1119
|
+
}
|
|
1120
|
+
scene.params = { ...scene.params, [name]: p };
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
case "switch": {
|
|
1124
|
+
const [name, label, on] = rest;
|
|
1125
|
+
scene.switches = { ...scene.switches, [name]: { label: unquote(label ?? ""), on: on === "on" } };
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
case "var": {
|
|
1129
|
+
if (rest[1] !== "=") throw new ChalkTextError(no, "write var name = value.");
|
|
1130
|
+
scene.vars = { ...scene.vars, [rest[0]]: value(rest[2] ?? "", no) };
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
case "learning": {
|
|
1134
|
+
const [part, t] = rest;
|
|
1135
|
+
if (!["goal", "question", "answer", "caveat"].includes(part)) throw new ChalkTextError(no, 'write learning goal|question|answer|caveat "\u2026".');
|
|
1136
|
+
scene.learning = { ...scene.learning, [part]: unquote(t ?? "") };
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
case "quiz": {
|
|
1140
|
+
scene.quiz = {
|
|
1141
|
+
question: unquote(rest[0] ?? ""),
|
|
1142
|
+
options: line2.children.map((c) => {
|
|
1143
|
+
const [mark, ...words] = c.tokens;
|
|
1144
|
+
if (mark !== "*" && mark !== "-") throw new ChalkTextError(c.no, "under a quiz, write * right option -- why, or - wrong option -- why.");
|
|
1145
|
+
const body = words.map(unquote).join(" ");
|
|
1146
|
+
const [t, why = ""] = body.split(/\s+--\s+/);
|
|
1147
|
+
return { text: t.trim(), right: mark === "*", why: why.trim() };
|
|
1148
|
+
})
|
|
1149
|
+
};
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const s = step(line2, names);
|
|
1154
|
+
if (chapter !== void 0) [s.chapter, chapter] = [chapter, void 0];
|
|
1155
|
+
if (say !== void 0) [s.say, say] = [say, void 0];
|
|
1156
|
+
steps.push(s);
|
|
1157
|
+
}
|
|
1158
|
+
scene.steps = steps;
|
|
1159
|
+
return scene;
|
|
1160
|
+
}
|
|
1161
|
+
var q = (s) => `"${String(s).replace(/(["\\])/g, "\\$1")}"`;
|
|
1162
|
+
var num = (v) => typeof v === "number" ? String(v) : /^[A-Za-z_][A-Za-z_0-9]*$/.test(String(v)) && !NUMBER.test(String(v)) ? String(v) : q(v);
|
|
1163
|
+
var pt = (p) => p.map(num).join(",");
|
|
1164
|
+
function printAttrs(s, skip) {
|
|
1165
|
+
const out = [];
|
|
1166
|
+
for (const [k, v] of Object.entries(s)) {
|
|
1167
|
+
if (skip.includes(k) || ["type", "chapter", "say"].includes(k)) continue;
|
|
1168
|
+
if (k === "style") {
|
|
1169
|
+
for (const [sk, sv] of Object.entries(v)) {
|
|
1170
|
+
if (sk === "dashed") {
|
|
1171
|
+
if (sv === true) out.push("dashed");
|
|
1172
|
+
} else if (sk === "width") out.push(`stroke=${sv}`);
|
|
1173
|
+
else if (STYLE.includes(sk)) out.push(`${sk}=${typeof sv === "number" ? sv : q(sv)}`);
|
|
1174
|
+
else return null;
|
|
1175
|
+
}
|
|
1176
|
+
} else if (k === "nudge") out.push(`nudge=${v.join(",")}`);
|
|
1177
|
+
else if (k === "flip") {
|
|
1178
|
+
if (v === true) out.push("flip");
|
|
1179
|
+
} else if (typeof v === "number" || typeof v === "string") out.push(`${k}=${typeof v === "number" ? v : num(v)}`);
|
|
1180
|
+
else return null;
|
|
1181
|
+
}
|
|
1182
|
+
return out;
|
|
1183
|
+
}
|
|
1184
|
+
function place(s, key) {
|
|
1185
|
+
const p = s.place;
|
|
1186
|
+
if (!p) return `at ${pt(s[key])}`;
|
|
1187
|
+
return [`${Object.keys(SIDES).find((w) => SIDES[w] === p.side)} ${p.to}`, p.gap !== void 0 ? `gap ${p.gap}` : "", p.align ? `align=${p.align}` : ""].filter(Boolean).join(" ");
|
|
1188
|
+
}
|
|
1189
|
+
function printStep(s, props, indent) {
|
|
1190
|
+
const json = () => [`${indent}json ${JSON.stringify(Object.fromEntries(Object.entries(s).filter(([k]) => k !== "chapter" && k !== "say")))}`];
|
|
1191
|
+
const line2 = (head, skip) => {
|
|
1192
|
+
const a = printAttrs(s, skip);
|
|
1193
|
+
return a ? [`${indent}${[head, ...a].join(" ")}`] : json();
|
|
1194
|
+
};
|
|
1195
|
+
const placedSkip = s.place ? ["at", "center", "place"] : ["at", "center"];
|
|
1196
|
+
switch (s.type) {
|
|
1197
|
+
case "prop":
|
|
1198
|
+
if (!props.has(String(s.name)) || s.id && !/^[A-Za-z_][\w-]*$/.test(String(s.id))) return json();
|
|
1199
|
+
return line2([s.name, s.id ?? "", place(s, "at"), s.label !== void 0 ? q(s.label) : ""].filter(Boolean).join(" "), ["name", "id", "label", ...placedSkip]);
|
|
1200
|
+
case "text":
|
|
1201
|
+
return line2(`text ${q(s.text)} ${place(s, "at")}`, ["text", ...placedSkip]);
|
|
1202
|
+
case "circle":
|
|
1203
|
+
case "ellipse":
|
|
1204
|
+
return line2(`${s.type} ${place(s, "center")}`, placedSkip);
|
|
1205
|
+
case "line":
|
|
1206
|
+
case "arrow":
|
|
1207
|
+
case "highlight":
|
|
1208
|
+
return line2(`${s.type} ${pt(s.from)} -> ${pt(s.to)}`, ["from", "to"]);
|
|
1209
|
+
case "polyline":
|
|
1210
|
+
case "polygon":
|
|
1211
|
+
case "curve":
|
|
1212
|
+
return line2(`${s.type} ${s.points.map(pt).join(" ")}`, ["points"]);
|
|
1213
|
+
case "link":
|
|
1214
|
+
return line2(`${s.from} -> ${s.to}${s.label !== void 0 ? ` ${q(s.label)}` : ""}`, ["from", "to", "label"]);
|
|
1215
|
+
case "balloon":
|
|
1216
|
+
return line2(`${s.to} ${s.kind === "thought" ? "thinks" : "says"} ${q(s.text)}`, ["to", "text", "kind"]);
|
|
1217
|
+
case "counter":
|
|
1218
|
+
return line2(`counter ${place(s, "at")} ${num(s.value)}${s.format !== void 0 ? ` ${q(s.format)}` : ""}`, ["value", "format", ...placedSkip]);
|
|
1219
|
+
case "plot": {
|
|
1220
|
+
const b = s.box;
|
|
1221
|
+
const r = (v) => v.map(num).join("..");
|
|
1222
|
+
return line2(`plot ${q(s.fn)} domain ${r(s.domain)} range ${r(s.range)} box ${[b.x, b.y, b.w, b.h].map(num).join(",")}`, ["fn", "domain", "range", "box"]);
|
|
1223
|
+
}
|
|
1224
|
+
case "pause":
|
|
1225
|
+
return line2(`pause ${s.ms}${s.ask !== void 0 ? ` ask ${q(s.ask)}` : ""}`, ["ms", "ask"]);
|
|
1226
|
+
case "repeat": {
|
|
1227
|
+
const inner = printStep(s.step, props, "");
|
|
1228
|
+
if (inner.length !== 1 || s.step.chapter || s.step.say) return json();
|
|
1229
|
+
const head = printAttrs(s, ["times", "step"]);
|
|
1230
|
+
return head && !head.length ? [`${indent}repeat ${num(s.times)}: ${inner[0]}`] : json();
|
|
1231
|
+
}
|
|
1232
|
+
case "row":
|
|
1233
|
+
case "column":
|
|
1234
|
+
case "grid": {
|
|
1235
|
+
const head = line2(`${s.type} ${place(s, "at")}`, ["steps", ...placedSkip]);
|
|
1236
|
+
if (head[0].trimStart().startsWith("json")) return head;
|
|
1237
|
+
return [...head, ...s.steps.flatMap((c) => printStep(c, props, `${indent} `))];
|
|
1238
|
+
}
|
|
1239
|
+
case "when": {
|
|
1240
|
+
const head = printAttrs(s, ["if", "steps"]);
|
|
1241
|
+
if (!head || head.length) return json();
|
|
1242
|
+
return [`${indent}when ${num(s.if)}:`, ...s.steps.flatMap((c) => printStep(c, props, `${indent} `))];
|
|
1243
|
+
}
|
|
1244
|
+
default:
|
|
1245
|
+
return json();
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
function printChalk(scene, props) {
|
|
1249
|
+
const names = new Set(props);
|
|
1250
|
+
const out = [];
|
|
1251
|
+
if (scene.id !== void 0) out.push(`id ${scene.id}`);
|
|
1252
|
+
if (scene.title !== void 0) out.push(`title ${scene.title}`);
|
|
1253
|
+
for (const k of ["category", "theme", "locale", "background"]) if (scene[k] !== void 0) out.push(`${k} ${scene[k]}`);
|
|
1254
|
+
if (scene.width !== 900 || scene.height !== 560) out.push(`size ${scene.width}x${scene.height}`);
|
|
1255
|
+
for (const [k, p] of Object.entries(scene.params ?? {})) {
|
|
1256
|
+
out.push(`param ${k} ${q(p.label)} ${p.min}..${p.max}${p.step !== 1 ? ` step=${p.step}` : ""} = ${p.value}`);
|
|
1257
|
+
}
|
|
1258
|
+
for (const [k, sw] of Object.entries(scene.switches ?? {})) out.push(`switch ${k} ${q(sw.label)} ${sw.on ? "on" : "off"}`);
|
|
1259
|
+
for (const [k, v] of Object.entries(scene.vars ?? {})) out.push(`var ${k} = ${num(v)}`);
|
|
1260
|
+
for (const [k, v] of Object.entries(scene.learning ?? {})) out.push(`learning ${k} ${q(v)}`);
|
|
1261
|
+
for (const s of scene.steps ?? []) {
|
|
1262
|
+
if (s.chapter !== void 0 || s.say !== void 0) out.push("");
|
|
1263
|
+
if (s.chapter !== void 0) out.push(`# ${s.chapter}`);
|
|
1264
|
+
if (s.say !== void 0) out.push(`> ${s.say}`);
|
|
1265
|
+
out.push(...printStep(s, names, ""));
|
|
1266
|
+
}
|
|
1267
|
+
const quiz = scene.quiz;
|
|
1268
|
+
if (quiz) {
|
|
1269
|
+
out.push("", `quiz ${q(quiz.question)}`);
|
|
1270
|
+
for (const o of quiz.options) out.push(` ${o.right ? "*" : "-"} ${o.text} -- ${o.why}`);
|
|
1271
|
+
}
|
|
1272
|
+
return out.join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
1273
|
+
}
|
|
718
1274
|
export {
|
|
1275
|
+
ChalkTextError,
|
|
719
1276
|
PROP_ALIASES,
|
|
720
1277
|
PROP_NAMES,
|
|
721
1278
|
chaptersFor,
|
|
722
1279
|
defaultValues,
|
|
1280
|
+
parseChalk,
|
|
1281
|
+
printChalk,
|
|
723
1282
|
validateScene
|
|
724
1283
|
};
|