@mathscan/math-exercise-engine 1.2.2 → 1.2.4
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/index.d.ts +21869 -7530
- package/dist/index.js +1751 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1751 -60
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +810 -4
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -77,6 +77,25 @@ const AST_TYPES = {
|
|
|
77
77
|
/** Visual fraction inline widget */
|
|
78
78
|
VISUAL_FRACTION: "VisualFraction",
|
|
79
79
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
|
+
// Chart
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
82
|
+
/** Generic chart layout block (bar, pictograph, …) */
|
|
83
|
+
CHART_BLOCK: "ChartBlock",
|
|
84
|
+
/**
|
|
85
|
+
* X axis sub-block: when present the Bar label inlines are hidden.
|
|
86
|
+
* If both X_AXIS and Y_AXIS exist, X_AXIS acts as category axis.
|
|
87
|
+
*/
|
|
88
|
+
CHART_X_AXIS: "ChartXAxis",
|
|
89
|
+
/**
|
|
90
|
+
* Y axis sub-block: value axis for vertical bar charts.
|
|
91
|
+
* If only X_AXIS is present, the transformer uses a default Y value axis.
|
|
92
|
+
*/
|
|
93
|
+
CHART_Y_AXIS: "ChartYAxis",
|
|
94
|
+
/** A single bar inside a CHART[type="bar"] block */
|
|
95
|
+
CHART_BAR: "ChartBar",
|
|
96
|
+
/** A single pictograph item inside a CHART[type="pictograph"] block */
|
|
97
|
+
CHART_PICTO: "ChartPicto",
|
|
98
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
99
|
// Tools
|
|
81
100
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
82
101
|
/** Drag-and-drop options tool */
|
|
@@ -171,7 +190,14 @@ const JSON_TYPES = {
|
|
|
171
190
|
/** Pair-matcher widget (top-row ↔ bottom-row pairing) */
|
|
172
191
|
PAIR_MATCHER: "pair-matcher",
|
|
173
192
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
174
|
-
//
|
|
193
|
+
// Chart
|
|
194
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
|
+
/** Generic chart block (bar, pictograph, …) */
|
|
196
|
+
CHART: "chart",
|
|
197
|
+
/** A single bar rendered inside a chart */
|
|
198
|
+
CHART_BAR: "chart-bar",
|
|
199
|
+
/** A single pictograph item rendered inside a chart */
|
|
200
|
+
CHART_PICTO: "chart-picto",
|
|
175
201
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
176
202
|
/** Inline input widget (string or numeric) */
|
|
177
203
|
INLINE_INPUT: "inline-input",
|
|
@@ -239,6 +265,10 @@ const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
|
|
239
265
|
const isNumberLineHighlightColor = (value) => NUMBER_LINE_HIGHLIGHT_COLORS.includes(value) || HEX_COLOR_REGEX.test(value);
|
|
240
266
|
const highlightColorMessage = (block) => `${block}: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")} or a hex value (e.g. "#3B82F6")`;
|
|
241
267
|
const NUMBER_LINE_DRAG_MODES = ["axis", "inputs"];
|
|
268
|
+
const DEFAULT_TABLE_BORDER_COLOR = "#7c6cf2";
|
|
269
|
+
const isTableBorderColor = (value) => value.toLowerCase() === "transparent" || HEX_COLOR_REGEX.test(value);
|
|
270
|
+
const TABLE_BORDER_COLOR_ERROR = 'TABLE: bordercolor must be "transparent" or a hex value (e.g. "#E67E22" or "#F00")';
|
|
271
|
+
const TableBorderColorSchema = z.string({ message: TABLE_BORDER_COLOR_ERROR }).refine(isTableBorderColor, { message: TABLE_BORDER_COLOR_ERROR }).default(DEFAULT_TABLE_BORDER_COLOR);
|
|
242
272
|
function getFractionKey(widgetId, part) {
|
|
243
273
|
return `${widgetId}__${part}`;
|
|
244
274
|
}
|
|
@@ -424,6 +454,26 @@ const RULER = createToken({
|
|
|
424
454
|
name: "RULER",
|
|
425
455
|
pattern: /RULER/
|
|
426
456
|
});
|
|
457
|
+
const CHART = createToken({
|
|
458
|
+
name: "CHART",
|
|
459
|
+
pattern: /CHART/
|
|
460
|
+
});
|
|
461
|
+
const X_AXIS = createToken({
|
|
462
|
+
name: "X_AXIS",
|
|
463
|
+
pattern: /X_AXIS/
|
|
464
|
+
});
|
|
465
|
+
const Y_AXIS = createToken({
|
|
466
|
+
name: "Y_AXIS",
|
|
467
|
+
pattern: /Y_AXIS/
|
|
468
|
+
});
|
|
469
|
+
const BAR = createToken({
|
|
470
|
+
name: "BAR",
|
|
471
|
+
pattern: /BAR/
|
|
472
|
+
});
|
|
473
|
+
const PICTO = createToken({
|
|
474
|
+
name: "PICTO",
|
|
475
|
+
pattern: /PICTO/
|
|
476
|
+
});
|
|
427
477
|
const SEP = createToken({
|
|
428
478
|
name: "SEP",
|
|
429
479
|
pattern: /SEP(?![a-zA-Z0-9_])/
|
|
@@ -566,6 +616,11 @@ const allTokens = [
|
|
|
566
616
|
PROTRACTOR,
|
|
567
617
|
SET_SQUARE,
|
|
568
618
|
RULER,
|
|
619
|
+
CHART,
|
|
620
|
+
X_AXIS,
|
|
621
|
+
Y_AXIS,
|
|
622
|
+
BAR,
|
|
623
|
+
PICTO,
|
|
569
624
|
SEP,
|
|
570
625
|
CARRY,
|
|
571
626
|
INPUT,
|
|
@@ -735,6 +790,10 @@ class MathExerciseParser extends CstParser {
|
|
|
735
790
|
GATE: this.BACKTRACK(this.verticalOperation),
|
|
736
791
|
ALT: () => this.SUBRULE(this.verticalOperation)
|
|
737
792
|
},
|
|
793
|
+
{
|
|
794
|
+
GATE: this.BACKTRACK(this.chartBlock),
|
|
795
|
+
ALT: () => this.SUBRULE(this.chartBlock)
|
|
796
|
+
},
|
|
738
797
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
739
798
|
]);
|
|
740
799
|
});
|
|
@@ -1590,6 +1649,168 @@ class MathExerciseParser extends CstParser {
|
|
|
1590
1649
|
this.MANY4(() => this.CONSUME4(Space));
|
|
1591
1650
|
this.CONSUME(RBracket);
|
|
1592
1651
|
});
|
|
1652
|
+
this.chartBlock = this.RULE("chartBlock", () => {
|
|
1653
|
+
this.MANY(() => {
|
|
1654
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1655
|
+
});
|
|
1656
|
+
this.CONSUME(CHART);
|
|
1657
|
+
this.CONSUME(LBracket);
|
|
1658
|
+
this.SUBRULE(this.parameterList);
|
|
1659
|
+
this.CONSUME(RBracket);
|
|
1660
|
+
this.MANY2(() => {
|
|
1661
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1662
|
+
});
|
|
1663
|
+
this.CONSUME(LBrace);
|
|
1664
|
+
this.MANY3(() => {
|
|
1665
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1666
|
+
});
|
|
1667
|
+
this.MANY5({
|
|
1668
|
+
GATE: () => this.LA(1).tokenType === X_AXIS || this.LA(1).tokenType === Y_AXIS,
|
|
1669
|
+
DEF: () => {
|
|
1670
|
+
this.OR5([
|
|
1671
|
+
{ ALT: () => this.SUBRULE(this.chartXAxis) },
|
|
1672
|
+
{ ALT: () => this.SUBRULE(this.chartYAxis) }
|
|
1673
|
+
]);
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
this.MANY6({
|
|
1677
|
+
GATE: () => {
|
|
1678
|
+
let k = 1;
|
|
1679
|
+
while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
|
|
1680
|
+
const t = this.LA(k).tokenType;
|
|
1681
|
+
return t === BAR || t === PICTO;
|
|
1682
|
+
},
|
|
1683
|
+
DEF: () => {
|
|
1684
|
+
this.OR6([
|
|
1685
|
+
{
|
|
1686
|
+
GATE: () => {
|
|
1687
|
+
let k = 1;
|
|
1688
|
+
while (this.LA(k).tokenType === Space || this.LA(k).tokenType === Newline) k++;
|
|
1689
|
+
return this.LA(k).tokenType === BAR;
|
|
1690
|
+
},
|
|
1691
|
+
ALT: () => this.SUBRULE(this.chartBarItem)
|
|
1692
|
+
},
|
|
1693
|
+
{ ALT: () => this.SUBRULE(this.chartPictoItem) }
|
|
1694
|
+
]);
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
this.CONSUME(RBrace);
|
|
1698
|
+
this.MANY4(() => {
|
|
1699
|
+
this.OR4([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
1700
|
+
});
|
|
1701
|
+
});
|
|
1702
|
+
this.chartXAxis = this.RULE("chartXAxis", () => {
|
|
1703
|
+
this.CONSUME(X_AXIS);
|
|
1704
|
+
this.OPTION(() => {
|
|
1705
|
+
this.CONSUME(LBracket);
|
|
1706
|
+
this.OPTION2(() => {
|
|
1707
|
+
this.SUBRULE(this.parameterList);
|
|
1708
|
+
});
|
|
1709
|
+
this.CONSUME(RBracket);
|
|
1710
|
+
});
|
|
1711
|
+
this.MANY(() => {
|
|
1712
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1713
|
+
});
|
|
1714
|
+
this.OPTION3(() => {
|
|
1715
|
+
this.CONSUME(LBrace);
|
|
1716
|
+
this.MANY2(() => {
|
|
1717
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1718
|
+
});
|
|
1719
|
+
this.MANY3(() => {
|
|
1720
|
+
this.OR3([
|
|
1721
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1722
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1723
|
+
]);
|
|
1724
|
+
});
|
|
1725
|
+
this.CONSUME(RBrace);
|
|
1726
|
+
});
|
|
1727
|
+
this.MANY4(() => {
|
|
1728
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1729
|
+
});
|
|
1730
|
+
});
|
|
1731
|
+
this.chartYAxis = this.RULE("chartYAxis", () => {
|
|
1732
|
+
this.CONSUME(Y_AXIS);
|
|
1733
|
+
this.OPTION(() => {
|
|
1734
|
+
this.CONSUME(LBracket);
|
|
1735
|
+
this.OPTION2(() => {
|
|
1736
|
+
this.SUBRULE(this.parameterList);
|
|
1737
|
+
});
|
|
1738
|
+
this.CONSUME(RBracket);
|
|
1739
|
+
});
|
|
1740
|
+
this.MANY(() => {
|
|
1741
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1742
|
+
});
|
|
1743
|
+
this.OPTION3(() => {
|
|
1744
|
+
this.CONSUME(LBrace);
|
|
1745
|
+
this.MANY2(() => {
|
|
1746
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1747
|
+
});
|
|
1748
|
+
this.MANY3(() => {
|
|
1749
|
+
this.OR3([
|
|
1750
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1751
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1752
|
+
]);
|
|
1753
|
+
});
|
|
1754
|
+
this.CONSUME(RBrace);
|
|
1755
|
+
});
|
|
1756
|
+
this.MANY4(() => {
|
|
1757
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1758
|
+
});
|
|
1759
|
+
});
|
|
1760
|
+
this.chartBarItem = this.RULE("chartBarItem", () => {
|
|
1761
|
+
this.MANY(() => {
|
|
1762
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1763
|
+
});
|
|
1764
|
+
this.CONSUME(BAR);
|
|
1765
|
+
this.CONSUME(LBracket);
|
|
1766
|
+
this.SUBRULE(this.parameterList);
|
|
1767
|
+
this.CONSUME(RBracket);
|
|
1768
|
+
this.MANY2(() => {
|
|
1769
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1770
|
+
});
|
|
1771
|
+
this.OPTION(() => {
|
|
1772
|
+
this.CONSUME(LBrace);
|
|
1773
|
+
this.MANY3(() => {
|
|
1774
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1775
|
+
});
|
|
1776
|
+
this.MANY4(() => {
|
|
1777
|
+
this.OR4([
|
|
1778
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1779
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
1780
|
+
]);
|
|
1781
|
+
});
|
|
1782
|
+
this.CONSUME(RBrace);
|
|
1783
|
+
});
|
|
1784
|
+
this.MANY5(() => {
|
|
1785
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
1786
|
+
});
|
|
1787
|
+
});
|
|
1788
|
+
this.chartPictoItem = this.RULE("chartPictoItem", () => {
|
|
1789
|
+
this.MANY(() => {
|
|
1790
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1791
|
+
});
|
|
1792
|
+
this.CONSUME(PICTO);
|
|
1793
|
+
this.CONSUME(LBracket);
|
|
1794
|
+
this.SUBRULE(this.parameterList);
|
|
1795
|
+
this.CONSUME(RBracket);
|
|
1796
|
+
this.MANY2(() => {
|
|
1797
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1798
|
+
});
|
|
1799
|
+
this.CONSUME(LBrace);
|
|
1800
|
+
this.MANY3(() => {
|
|
1801
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1802
|
+
});
|
|
1803
|
+
this.MANY4(() => {
|
|
1804
|
+
this.OR4([
|
|
1805
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
1806
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
1807
|
+
]);
|
|
1808
|
+
});
|
|
1809
|
+
this.CONSUME(RBrace);
|
|
1810
|
+
this.MANY5(() => {
|
|
1811
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1593
1814
|
this.performSelfAnalysis();
|
|
1594
1815
|
}
|
|
1595
1816
|
/** Distinguish `{[number, number], ...}` and the valid empty form `{}` from string lists. */
|
|
@@ -1604,6 +1825,18 @@ class MathExerciseParser extends CstParser {
|
|
|
1604
1825
|
}
|
|
1605
1826
|
}
|
|
1606
1827
|
const parser = new MathExerciseParser();
|
|
1828
|
+
function normalize(value) {
|
|
1829
|
+
if (typeof value === "number") {
|
|
1830
|
+
return String(Math.round(value * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
1831
|
+
}
|
|
1832
|
+
const trimmed = String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
1833
|
+
const asNum = Number(trimmed);
|
|
1834
|
+
if (trimmed !== "" && !isNaN(asNum)) {
|
|
1835
|
+
return String(Math.round(asNum * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
1836
|
+
}
|
|
1837
|
+
return trimmed;
|
|
1838
|
+
}
|
|
1839
|
+
const IMAGE_SRC_SCHEME_RE = /^(https?:\/\/|data:image\/)/i;
|
|
1607
1840
|
function parseOrThrow(schema, data, label) {
|
|
1608
1841
|
const result = schema.safeParse(data);
|
|
1609
1842
|
if (!result.success) {
|
|
@@ -1673,11 +1906,10 @@ const FractionInlineSchema = z.object({
|
|
|
1673
1906
|
simplified: z.boolean().default(true),
|
|
1674
1907
|
placeholder: z.string().max(32).optional()
|
|
1675
1908
|
});
|
|
1676
|
-
const IMAGE_SRC_SCHEME_RE$1 = /^(https?:\/\/|data:image\/)/i;
|
|
1677
1909
|
const ImageInlineSchema = z.object({
|
|
1678
1910
|
type: z.literal(AST_TYPES.IMAGE),
|
|
1679
1911
|
name: z.string().min(1),
|
|
1680
|
-
src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE
|
|
1912
|
+
src: z.string().url().refine((s) => IMAGE_SRC_SCHEME_RE.test(s), {
|
|
1681
1913
|
message: "src must use http(s) or data:image scheme"
|
|
1682
1914
|
}),
|
|
1683
1915
|
alt: z.string(),
|
|
@@ -2306,12 +2538,14 @@ function createTableRow(cells, backgroundColor) {
|
|
|
2306
2538
|
const TableBlockSchema = z.object({
|
|
2307
2539
|
type: z.literal(AST_TYPES.TABLE_BLOCK),
|
|
2308
2540
|
cols: z.number().int().positive(),
|
|
2541
|
+
borderColor: TableBorderColorSchema,
|
|
2309
2542
|
rows: z.array(TableRowSchema).min(1)
|
|
2310
2543
|
});
|
|
2311
|
-
function createTableBlock(cols, rows) {
|
|
2544
|
+
function createTableBlock(cols, rows, borderColor) {
|
|
2312
2545
|
return TableBlockSchema.parse({
|
|
2313
2546
|
type: AST_TYPES.TABLE_BLOCK,
|
|
2314
2547
|
cols,
|
|
2548
|
+
borderColor,
|
|
2315
2549
|
rows
|
|
2316
2550
|
});
|
|
2317
2551
|
}
|
|
@@ -2719,6 +2953,350 @@ function isSetSquareTool(value) {
|
|
|
2719
2953
|
function isToolNode(value) {
|
|
2720
2954
|
return ToolNodeSchema.safeParse(value).success;
|
|
2721
2955
|
}
|
|
2956
|
+
const CHART_TYPES = ["bar", "pictograph"];
|
|
2957
|
+
const CHART_AXIS_DEFAULTS = { min: 0, max: 100, step: 10, visible: true };
|
|
2958
|
+
const CHART_MAX_AXIS_TICKS = 200;
|
|
2959
|
+
const CHART_MAX_PICTO_SLOTS = 60;
|
|
2960
|
+
const ChartAxisSchema = z.object({
|
|
2961
|
+
type: z.literal(AST_TYPES.CHART_X_AXIS).or(z.literal(AST_TYPES.CHART_Y_AXIS)),
|
|
2962
|
+
min: z.number().optional(),
|
|
2963
|
+
max: z.number().optional(),
|
|
2964
|
+
step: z.number().positive().optional(),
|
|
2965
|
+
/** When false, the entire axis is hidden. Defaults to true. */
|
|
2966
|
+
visible: z.boolean().optional(),
|
|
2967
|
+
/** Inline elements used as the axis title label */
|
|
2968
|
+
inlines: z.array(InlineNodeSchema).default([])
|
|
2969
|
+
}).superRefine((axis, ctx) => {
|
|
2970
|
+
if (axis.min !== void 0 && axis.max !== void 0 && axis.min >= axis.max) {
|
|
2971
|
+
ctx.addIssue({
|
|
2972
|
+
code: z.ZodIssueCode.custom,
|
|
2973
|
+
message: `AXIS: min (${axis.min}) must be strictly less than max (${axis.max})`,
|
|
2974
|
+
path: ["min"]
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
function createChartAxis(axisType, opts) {
|
|
2979
|
+
return ChartAxisSchema.parse({
|
|
2980
|
+
type: axisType,
|
|
2981
|
+
min: opts.min,
|
|
2982
|
+
max: opts.max,
|
|
2983
|
+
step: opts.step,
|
|
2984
|
+
visible: opts.visible,
|
|
2985
|
+
inlines: opts.inlines ?? []
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
function isChartAxis(value) {
|
|
2989
|
+
return ChartAxisSchema.safeParse(value).success;
|
|
2990
|
+
}
|
|
2991
|
+
const ChartPictoSchema = z.object({
|
|
2992
|
+
type: z.literal(AST_TYPES.CHART_PICTO),
|
|
2993
|
+
id: z.string().min(1),
|
|
2994
|
+
value: z.number().default(0),
|
|
2995
|
+
answer: z.number().optional(),
|
|
2996
|
+
image: z.string().min(1).refine(
|
|
2997
|
+
(s) => IMAGE_SRC_SCHEME_RE.test(s),
|
|
2998
|
+
{ message: "image must use http(s) or data:image scheme" }
|
|
2999
|
+
),
|
|
3000
|
+
inlines: z.array(InlineNodeSchema).default([])
|
|
3001
|
+
});
|
|
3002
|
+
function createChartPicto(id, opts) {
|
|
3003
|
+
return ChartPictoSchema.parse({
|
|
3004
|
+
type: AST_TYPES.CHART_PICTO,
|
|
3005
|
+
id,
|
|
3006
|
+
value: opts.value ?? 0,
|
|
3007
|
+
answer: opts.answer,
|
|
3008
|
+
image: opts.image,
|
|
3009
|
+
inlines: opts.inlines ?? []
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
function isChartPicto(value) {
|
|
3013
|
+
return ChartPictoSchema.safeParse(value).success;
|
|
3014
|
+
}
|
|
3015
|
+
const ChartBarSchema = z.object({
|
|
3016
|
+
type: z.literal(AST_TYPES.CHART_BAR),
|
|
3017
|
+
id: z.string().min(1),
|
|
3018
|
+
value: z.number().default(0),
|
|
3019
|
+
answer: z.number().optional(),
|
|
3020
|
+
color: z.string().optional(),
|
|
3021
|
+
inlines: z.array(InlineNodeSchema).default([])
|
|
3022
|
+
});
|
|
3023
|
+
function createChartBar(id, opts) {
|
|
3024
|
+
return ChartBarSchema.parse({
|
|
3025
|
+
type: AST_TYPES.CHART_BAR,
|
|
3026
|
+
id,
|
|
3027
|
+
value: opts.value ?? 0,
|
|
3028
|
+
answer: opts.answer,
|
|
3029
|
+
color: opts.color,
|
|
3030
|
+
inlines: opts.inlines ?? []
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
function isChartBar(value) {
|
|
3034
|
+
return ChartBarSchema.safeParse(value).success;
|
|
3035
|
+
}
|
|
3036
|
+
const ChartBlockSchema = z.object({
|
|
3037
|
+
type: z.literal(AST_TYPES.CHART_BLOCK),
|
|
3038
|
+
name: z.string().optional(),
|
|
3039
|
+
title: z.string().optional(),
|
|
3040
|
+
chartType: z.enum(CHART_TYPES),
|
|
3041
|
+
grid: z.boolean().default(false),
|
|
3042
|
+
scale: z.number().positive().default(1),
|
|
3043
|
+
snap: z.number().positive().optional(),
|
|
3044
|
+
xAxis: ChartAxisSchema.optional(),
|
|
3045
|
+
yAxis: ChartAxisSchema.optional(),
|
|
3046
|
+
bars: z.array(ChartBarSchema).default([]),
|
|
3047
|
+
pictos: z.array(ChartPictoSchema).default([])
|
|
3048
|
+
}).superRefine((block, ctx) => {
|
|
3049
|
+
validateChartBlock(block, ctx);
|
|
3050
|
+
});
|
|
3051
|
+
function resolveChartAxes(block) {
|
|
3052
|
+
const horizontal = axisHasNumericParams(block.xAxis) && !axisHasNumericParams(block.yAxis);
|
|
3053
|
+
const orientation = horizontal ? "horizontal" : "vertical";
|
|
3054
|
+
return {
|
|
3055
|
+
orientation,
|
|
3056
|
+
valueAxis: resolveNumericAxis(horizontal ? block.xAxis : block.yAxis),
|
|
3057
|
+
categoryAxis: horizontal ? block.yAxis : block.xAxis
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
function validateChartBlock(block, ctx) {
|
|
3061
|
+
if (hasAmbiguousValueAxes(block)) {
|
|
3062
|
+
ctx.addIssue({
|
|
3063
|
+
code: z.ZodIssueCode.custom,
|
|
3064
|
+
message: `CHART[type="${block.chartType}"]: both X_AXIS and Y_AXIS declare numeric params (min/max/step) — ambiguous orientation. Remove params from one axis: the axis with params becomes the value axis.`,
|
|
3065
|
+
path: ["xAxis"]
|
|
3066
|
+
});
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
const { valueAxis } = resolveChartAxes(block);
|
|
3070
|
+
validateValueAxisRange(valueAxis, ctx);
|
|
3071
|
+
if (block.chartType === "bar") {
|
|
3072
|
+
if (block.pictos.length > 0) {
|
|
3073
|
+
ctx.addIssue({
|
|
3074
|
+
code: z.ZodIssueCode.custom,
|
|
3075
|
+
message: 'CHART[type="bar"]: body must contain BAR items only, not PICTO',
|
|
3076
|
+
path: ["pictos"]
|
|
3077
|
+
});
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
if (block.bars.length === 0) {
|
|
3081
|
+
ctx.addIssue({
|
|
3082
|
+
code: z.ZodIssueCode.custom,
|
|
3083
|
+
message: 'CHART[type="bar"]: at least one BAR is required',
|
|
3084
|
+
path: ["bars"]
|
|
3085
|
+
});
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
validateUniqueIds(block.bars, ctx);
|
|
3089
|
+
validateChartSnap(block.snap, valueAxis.step, ctx);
|
|
3090
|
+
validateBarAnswers(block.bars, valueAxis, block.snap, ctx);
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
if (block.chartType === "pictograph") {
|
|
3094
|
+
if (block.bars.length > 0) {
|
|
3095
|
+
ctx.addIssue({
|
|
3096
|
+
code: z.ZodIssueCode.custom,
|
|
3097
|
+
message: 'CHART[type="pictograph"]: body must contain PICTO items only, not BAR',
|
|
3098
|
+
path: ["bars"]
|
|
3099
|
+
});
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
if (block.pictos.length === 0) {
|
|
3103
|
+
ctx.addIssue({
|
|
3104
|
+
code: z.ZodIssueCode.custom,
|
|
3105
|
+
message: 'CHART[type="pictograph"]: at least one PICTO is required',
|
|
3106
|
+
path: ["pictos"]
|
|
3107
|
+
});
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
if (block.snap !== void 0) {
|
|
3111
|
+
ctx.addIssue({
|
|
3112
|
+
code: z.ZodIssueCode.custom,
|
|
3113
|
+
message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
|
|
3114
|
+
path: ["snap"]
|
|
3115
|
+
});
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
validateUniqueIds(block.pictos, ctx);
|
|
3119
|
+
if (!axisHasNumericParams(valueAxis.axis)) {
|
|
3120
|
+
ctx.addIssue({
|
|
3121
|
+
code: z.ZodIssueCode.custom,
|
|
3122
|
+
message: 'CHART[type="pictograph"]: a value axis with min/max/step is required (e.g. Y_AXIS[min=0, max=8, step=2]) — without it the chart defaults to a 0–100 range and renders 100 icons per column.',
|
|
3123
|
+
path: ["yAxis"]
|
|
3124
|
+
});
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
const slotCount = Math.round(valueAxis.max - valueAxis.min);
|
|
3128
|
+
if (slotCount > CHART_MAX_PICTO_SLOTS) {
|
|
3129
|
+
ctx.addIssue({
|
|
3130
|
+
code: z.ZodIssueCode.custom,
|
|
3131
|
+
message: `CHART[type="pictograph"]: the value axis spans ${slotCount} units, so each column would render ${slotCount} icons — over the ${CHART_MAX_PICTO_SLOTS} limit. Narrow the range.`,
|
|
3132
|
+
path: ["yAxis"]
|
|
3133
|
+
});
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
validatePictoAnswers(block.pictos, valueAxis, ctx);
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
ctx.addIssue({
|
|
3140
|
+
code: z.ZodIssueCode.custom,
|
|
3141
|
+
message: `CHART: unsupported chartType "${block.chartType}" — add a validation branch to validateChartBlock`,
|
|
3142
|
+
path: ["chartType"]
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
function axisHasNumericParams(axis) {
|
|
3146
|
+
return axis?.min !== void 0 || axis?.max !== void 0 || axis?.step !== void 0;
|
|
3147
|
+
}
|
|
3148
|
+
function resolveNumericAxis(axis) {
|
|
3149
|
+
return {
|
|
3150
|
+
axis,
|
|
3151
|
+
min: axis?.min ?? CHART_AXIS_DEFAULTS.min,
|
|
3152
|
+
max: axis?.max ?? CHART_AXIS_DEFAULTS.max,
|
|
3153
|
+
step: axis?.step ?? CHART_AXIS_DEFAULTS.step
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
function hasAmbiguousValueAxes(block) {
|
|
3157
|
+
return axisHasNumericParams(block.xAxis) && axisHasNumericParams(block.yAxis);
|
|
3158
|
+
}
|
|
3159
|
+
function validateUniqueIds(items, ctx) {
|
|
3160
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3161
|
+
items.forEach((item, index) => {
|
|
3162
|
+
if (seen.has(item.id)) {
|
|
3163
|
+
ctx.addIssue({
|
|
3164
|
+
code: z.ZodIssueCode.custom,
|
|
3165
|
+
message: `CHART: duplicate id "${item.id}" — ids must be unique within the chart`,
|
|
3166
|
+
path: [index, "id"]
|
|
3167
|
+
});
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
seen.add(item.id);
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
function validateValueAxisRange(axis, ctx) {
|
|
3174
|
+
if (axis.min >= axis.max) {
|
|
3175
|
+
ctx.addIssue({
|
|
3176
|
+
code: z.ZodIssueCode.custom,
|
|
3177
|
+
message: `CHART: value axis min (${axis.min}) must be strictly less than max (${axis.max})`
|
|
3178
|
+
});
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const tickCount = Math.floor((axis.max - axis.min) / axis.step) + 1;
|
|
3182
|
+
if (tickCount > CHART_MAX_AXIS_TICKS) {
|
|
3183
|
+
ctx.addIssue({
|
|
3184
|
+
code: z.ZodIssueCode.custom,
|
|
3185
|
+
message: `CHART: value axis would render ${tickCount} ticks ((${axis.max} − ${axis.min}) / step ${axis.step}), over the ${CHART_MAX_AXIS_TICKS} limit — widen the step or narrow the range.`
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function validateChartSnap(snap, step, ctx) {
|
|
3190
|
+
if (snap === void 0) return;
|
|
3191
|
+
if (snap > step) {
|
|
3192
|
+
ctx.addIssue({
|
|
3193
|
+
code: z.ZodIssueCode.custom,
|
|
3194
|
+
message: `CHART: snap (${snap}) must be ≤ value-axis step (${step}).`,
|
|
3195
|
+
path: ["snap"]
|
|
3196
|
+
});
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
const remainder = step % snap;
|
|
3200
|
+
if (remainder > 1e-9 && remainder < snap - 1e-9) {
|
|
3201
|
+
ctx.addIssue({
|
|
3202
|
+
code: z.ZodIssueCode.custom,
|
|
3203
|
+
message: `CHART: snap (${snap}) must evenly divide the value-axis step (${step}) — otherwise bars snap to positions between tick marks.`,
|
|
3204
|
+
path: ["snap"]
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
function validateBarAnswers(bars, valueAxis, snap, ctx) {
|
|
3209
|
+
const gridStep = snap ?? valueAxis.step;
|
|
3210
|
+
bars.forEach((bar, index) => {
|
|
3211
|
+
if (bar.value < valueAxis.min || bar.value > valueAxis.max) {
|
|
3212
|
+
ctx.addIssue({
|
|
3213
|
+
code: z.ZodIssueCode.custom,
|
|
3214
|
+
message: `CHART: BAR "${bar.id}" value (${bar.value}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the bar will not render correctly`,
|
|
3215
|
+
path: ["bars", index, "value"]
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
if (bar.answer === void 0) return;
|
|
3219
|
+
if (bar.answer < valueAxis.min || bar.answer > valueAxis.max) {
|
|
3220
|
+
ctx.addIssue({
|
|
3221
|
+
code: z.ZodIssueCode.custom,
|
|
3222
|
+
message: `CHART: BAR "${bar.id}" answer (${bar.answer}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the student can never reach this value`,
|
|
3223
|
+
path: ["bars", index, "answer"]
|
|
3224
|
+
});
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
const offGrid = Math.abs(bar.answer - valueAxis.min) % gridStep;
|
|
3228
|
+
if (offGrid > 1e-9 && offGrid < gridStep - 1e-9) {
|
|
3229
|
+
ctx.addIssue({
|
|
3230
|
+
code: z.ZodIssueCode.custom,
|
|
3231
|
+
message: `CHART: BAR "${bar.id}" answer (${bar.answer}) is not on the drag grid (min ${valueAxis.min} + multiples of ${gridStep}) — the student can never land on it. Align the answer to the step/snap grid.`,
|
|
3232
|
+
path: ["bars", index, "answer"]
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
function isOnPictoGrid(value, axisMin) {
|
|
3238
|
+
const offset = value - axisMin;
|
|
3239
|
+
return Math.abs(offset - Math.round(offset)) <= 1e-9;
|
|
3240
|
+
}
|
|
3241
|
+
function validatePictoAnswers(pictos, valueAxis, ctx) {
|
|
3242
|
+
pictos.forEach((picto, index) => {
|
|
3243
|
+
if (picto.value < valueAxis.min || picto.value > valueAxis.max) {
|
|
3244
|
+
ctx.addIssue({
|
|
3245
|
+
code: z.ZodIssueCode.custom,
|
|
3246
|
+
message: `CHART: PICTO "${picto.id}" value (${picto.value}) is outside axis range [${valueAxis.min}, ${valueAxis.max}]`,
|
|
3247
|
+
path: ["pictos", index, "value"]
|
|
3248
|
+
});
|
|
3249
|
+
} else if (!isOnPictoGrid(picto.value, valueAxis.min)) {
|
|
3250
|
+
ctx.addIssue({
|
|
3251
|
+
code: z.ZodIssueCode.custom,
|
|
3252
|
+
message: `CHART: PICTO "${picto.id}" value (${picto.value}) is not a whole number of icons above the axis minimum (${valueAxis.min}) — icons can't be drawn in fractions.`,
|
|
3253
|
+
path: ["pictos", index, "value"]
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
if (picto.answer === void 0) return;
|
|
3257
|
+
if (picto.answer < valueAxis.min || picto.answer > valueAxis.max) {
|
|
3258
|
+
ctx.addIssue({
|
|
3259
|
+
code: z.ZodIssueCode.custom,
|
|
3260
|
+
message: `CHART: PICTO "${picto.id}" answer (${picto.answer}) is outside axis range [${valueAxis.min}, ${valueAxis.max}] — the student can never reach this value`,
|
|
3261
|
+
path: ["pictos", index, "answer"]
|
|
3262
|
+
});
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
if (!isOnPictoGrid(picto.answer, valueAxis.min)) {
|
|
3266
|
+
ctx.addIssue({
|
|
3267
|
+
code: z.ZodIssueCode.custom,
|
|
3268
|
+
message: `CHART: PICTO "${picto.id}" answer (${picto.answer}) is not a whole number of icons above the axis minimum (${valueAxis.min}) — the student can never land on it.`,
|
|
3269
|
+
path: ["pictos", index, "answer"]
|
|
3270
|
+
});
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
if (picto.answer < picto.value) {
|
|
3274
|
+
ctx.addIssue({
|
|
3275
|
+
code: z.ZodIssueCode.custom,
|
|
3276
|
+
message: `CHART: PICTO "${picto.id}" answer (${picto.answer}) is below its prefilled value (${picto.value}) — prefilled icons can't be removed, so the exercise is unsolvable.`,
|
|
3277
|
+
path: ["pictos", index, "answer"]
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
function createChartBlock(chartType, opts = {}) {
|
|
3283
|
+
return ChartBlockSchema.parse({
|
|
3284
|
+
type: AST_TYPES.CHART_BLOCK,
|
|
3285
|
+
name: opts.name,
|
|
3286
|
+
title: opts.title,
|
|
3287
|
+
chartType,
|
|
3288
|
+
grid: opts.grid ?? false,
|
|
3289
|
+
scale: opts.scale ?? 1,
|
|
3290
|
+
snap: opts.snap,
|
|
3291
|
+
xAxis: opts.xAxis,
|
|
3292
|
+
yAxis: opts.yAxis,
|
|
3293
|
+
bars: opts.bars ?? [],
|
|
3294
|
+
pictos: opts.pictos ?? []
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
function isChartBlock(value) {
|
|
3298
|
+
return ChartBlockSchema.safeParse(value).success;
|
|
3299
|
+
}
|
|
2722
3300
|
const RadioOptionsBlockSchema = z.object({
|
|
2723
3301
|
type: z.literal(AST_TYPES.RADIO_OPTIONS),
|
|
2724
3302
|
name: z.string(),
|
|
@@ -2778,7 +3356,7 @@ function isMixedValidation(value) {
|
|
|
2778
3356
|
function isValidationNode(value) {
|
|
2779
3357
|
return ValidationNodeSchema.safeParse(value).success;
|
|
2780
3358
|
}
|
|
2781
|
-
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema, GridSymmetryBlockSchema]);
|
|
3359
|
+
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema, GridSymmetryBlockSchema, ChartBlockSchema]);
|
|
2782
3360
|
const ExerciseDocumentSchema = z.object({
|
|
2783
3361
|
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
2784
3362
|
question: TextBlockSchema.optional(),
|
|
@@ -2884,6 +3462,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2884
3462
|
if (ctx.pairMatcher) return this.visit(ctx.pairMatcher);
|
|
2885
3463
|
if (ctx.matcher) return this.visit(ctx.matcher);
|
|
2886
3464
|
if (ctx.verticalOperation) return this.visit(ctx.verticalOperation);
|
|
3465
|
+
if (ctx.chartBlock) return this.visit(ctx.chartBlock);
|
|
2887
3466
|
if (ctx.line) return this.visit(ctx.line);
|
|
2888
3467
|
throw new Error("Unknown content block");
|
|
2889
3468
|
}
|
|
@@ -2911,8 +3490,10 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2911
3490
|
table(ctx) {
|
|
2912
3491
|
const params = this.visit(ctx.parameterList);
|
|
2913
3492
|
const cols = params.cols;
|
|
3493
|
+
const borderColorParam = params.bordercolor;
|
|
3494
|
+
const borderColor = borderColorParam === void 0 ? void 0 : String(borderColorParam);
|
|
2914
3495
|
const rows = ctx.row.map((r) => this.visit(r));
|
|
2915
|
-
return createTableBlock(cols, rows);
|
|
3496
|
+
return createTableBlock(cols, rows, borderColor);
|
|
2916
3497
|
}
|
|
2917
3498
|
row(ctx) {
|
|
2918
3499
|
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
@@ -3362,6 +3943,70 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3362
3943
|
return createInlineDragOption(value);
|
|
3363
3944
|
}
|
|
3364
3945
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3946
|
+
// Chart Block
|
|
3947
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3948
|
+
chartBlock(ctx) {
|
|
3949
|
+
const params = this.visit(ctx.parameterList);
|
|
3950
|
+
const chartType = params.type;
|
|
3951
|
+
if (!chartType) throw new Error('CHART: type is required ("bar" | "pictograph")');
|
|
3952
|
+
if (!CHART_TYPES.includes(chartType)) {
|
|
3953
|
+
throw new Error(`CHART: type must be one of ${CHART_TYPES.join(", ")} (got "${chartType}")`);
|
|
3954
|
+
}
|
|
3955
|
+
const name = params.name;
|
|
3956
|
+
const title = params.title;
|
|
3957
|
+
const grid = params.grid === true || params.grid === "true";
|
|
3958
|
+
const scale = params.scale !== void 0 ? Number(params.scale) : 1;
|
|
3959
|
+
const snap = params.snap !== void 0 ? Number(params.snap) : void 0;
|
|
3960
|
+
if (ctx.chartXAxis && ctx.chartXAxis.length > 1) {
|
|
3961
|
+
throw new Error("CHART: only one X_AXIS is allowed");
|
|
3962
|
+
}
|
|
3963
|
+
if (ctx.chartYAxis && ctx.chartYAxis.length > 1) {
|
|
3964
|
+
throw new Error("CHART: only one Y_AXIS is allowed");
|
|
3965
|
+
}
|
|
3966
|
+
const xAxis = ctx.chartXAxis ? this.visit(ctx.chartXAxis) : void 0;
|
|
3967
|
+
const yAxis = ctx.chartYAxis ? this.visit(ctx.chartYAxis) : void 0;
|
|
3968
|
+
const bars = ctx.chartBarItem ? ctx.chartBarItem.map((b) => this.visit(b)) : [];
|
|
3969
|
+
const pictos = ctx.chartPictoItem ? ctx.chartPictoItem.map((p) => this.visit(p)) : [];
|
|
3970
|
+
return createChartBlock(chartType, { bars, pictos, name, title, grid, scale, snap, xAxis, yAxis });
|
|
3971
|
+
}
|
|
3972
|
+
/** X_AXIS and Y_AXIS carry identical params — only the produced AST type differs.
|
|
3973
|
+
* Chevrotain needs two separate grammar rules, but one visitor body serves both. */
|
|
3974
|
+
buildChartAxis(ctx, astType) {
|
|
3975
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
3976
|
+
const min = params.min !== void 0 ? Number(params.min) : void 0;
|
|
3977
|
+
const max = params.max !== void 0 ? Number(params.max) : void 0;
|
|
3978
|
+
const step = params.step !== void 0 ? Number(params.step) : void 0;
|
|
3979
|
+
const visible = params.visible !== void 0 ? params.visible === true || params.visible === "true" : void 0;
|
|
3980
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
|
|
3981
|
+
return createChartAxis(astType, { min, max, step, visible, inlines });
|
|
3982
|
+
}
|
|
3983
|
+
chartXAxis(ctx) {
|
|
3984
|
+
return this.buildChartAxis(ctx, AST_TYPES.CHART_X_AXIS);
|
|
3985
|
+
}
|
|
3986
|
+
chartYAxis(ctx) {
|
|
3987
|
+
return this.buildChartAxis(ctx, AST_TYPES.CHART_Y_AXIS);
|
|
3988
|
+
}
|
|
3989
|
+
chartBarItem(ctx) {
|
|
3990
|
+
const params = this.visit(ctx.parameterList);
|
|
3991
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
|
|
3992
|
+
return createChartBar(params.id, {
|
|
3993
|
+
value: params.value !== void 0 ? Number(params.value) : void 0,
|
|
3994
|
+
answer: params.answer !== void 0 ? Number(params.answer) : void 0,
|
|
3995
|
+
color: params.color,
|
|
3996
|
+
inlines
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
chartPictoItem(ctx) {
|
|
4000
|
+
const params = this.visit(ctx.parameterList);
|
|
4001
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((e) => this.visit(e)) : [];
|
|
4002
|
+
return createChartPicto(params.id, {
|
|
4003
|
+
value: params.value !== void 0 ? Number(params.value) : void 0,
|
|
4004
|
+
answer: params.answer !== void 0 ? Number(params.answer) : void 0,
|
|
4005
|
+
image: params.image,
|
|
4006
|
+
inlines
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3365
4010
|
// Tools
|
|
3366
4011
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3367
4012
|
tool(ctx) {
|
|
@@ -3548,7 +4193,6 @@ const BoldTextSchema = z.object({
|
|
|
3548
4193
|
value: z.string(),
|
|
3549
4194
|
highlight: z.tuple([z.number(), z.number()]).optional()
|
|
3550
4195
|
});
|
|
3551
|
-
const IMAGE_SRC_SCHEME_RE = /^(https?:\/\/|data:image\/)/i;
|
|
3552
4196
|
const ImageItemSchema = z.object({
|
|
3553
4197
|
id: z.string(),
|
|
3554
4198
|
type: z.literal(JSON_TYPES.IMAGE),
|
|
@@ -3617,7 +4261,8 @@ const LayoutItemSchema = z.lazy(
|
|
|
3617
4261
|
TableItemSchema,
|
|
3618
4262
|
NumberLineItemSchema,
|
|
3619
4263
|
VerticalOperationItemSchema,
|
|
3620
|
-
ClockItemSchema
|
|
4264
|
+
ClockItemSchema,
|
|
4265
|
+
ChartItemSchema
|
|
3621
4266
|
])
|
|
3622
4267
|
);
|
|
3623
4268
|
const GroupItemSchema = z.object({
|
|
@@ -3643,6 +4288,7 @@ const TableItemSchema = z.object({
|
|
|
3643
4288
|
id: z.string(),
|
|
3644
4289
|
type: z.literal(JSON_TYPES.TABLE),
|
|
3645
4290
|
cols: z.number().int().positive(),
|
|
4291
|
+
borderColor: TableBorderColorSchema,
|
|
3646
4292
|
rows: z.array(TableRowItemSchema)
|
|
3647
4293
|
});
|
|
3648
4294
|
const NumberLinePositionItemSchema = z.object({
|
|
@@ -3790,6 +4436,97 @@ const VerticalOperationItemSchema = z.object({
|
|
|
3790
4436
|
cols: z.number().int().positive(),
|
|
3791
4437
|
items: z.array(z.union([VopRowItemSchema, VopCarryRowItemSchema, VopSeparatorItemSchema]))
|
|
3792
4438
|
});
|
|
4439
|
+
const ChartAxisJSONSchema = z.object({
|
|
4440
|
+
min: z.number().optional(),
|
|
4441
|
+
max: z.number().optional(),
|
|
4442
|
+
step: z.number().positive().optional(),
|
|
4443
|
+
visible: z.boolean().default(true),
|
|
4444
|
+
title: z.array(LayoutItemSchema).optional()
|
|
4445
|
+
}).superRefine((axis, ctx) => {
|
|
4446
|
+
const numericFields = [axis.min, axis.max, axis.step];
|
|
4447
|
+
const numericFieldCount = numericFields.filter((value) => value !== void 0).length;
|
|
4448
|
+
if (numericFieldCount > 0 && numericFieldCount < numericFields.length) {
|
|
4449
|
+
ctx.addIssue({
|
|
4450
|
+
code: z.ZodIssueCode.custom,
|
|
4451
|
+
message: "AXIS: min, max, and step must be provided together for a numeric axis"
|
|
4452
|
+
});
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
if (axis.min !== void 0 && axis.max !== void 0 && axis.min >= axis.max) {
|
|
4456
|
+
ctx.addIssue({
|
|
4457
|
+
code: z.ZodIssueCode.custom,
|
|
4458
|
+
message: `AXIS: min (${axis.min}) must be strictly less than max (${axis.max})`,
|
|
4459
|
+
path: ["min"]
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
});
|
|
4463
|
+
const ChartBarJSONSchema = z.discriminatedUnion("interactive", [
|
|
4464
|
+
z.object({
|
|
4465
|
+
id: z.string(),
|
|
4466
|
+
type: z.literal(JSON_TYPES.CHART_BAR),
|
|
4467
|
+
interactive: z.literal(false),
|
|
4468
|
+
value: z.number(),
|
|
4469
|
+
color: z.string().optional(),
|
|
4470
|
+
label: z.array(LayoutItemSchema).optional()
|
|
4471
|
+
}),
|
|
4472
|
+
z.object({
|
|
4473
|
+
id: z.string(),
|
|
4474
|
+
type: z.literal(JSON_TYPES.CHART_BAR),
|
|
4475
|
+
interactive: z.literal(true),
|
|
4476
|
+
value: z.number(),
|
|
4477
|
+
widgetId: z.string(),
|
|
4478
|
+
color: z.string().optional(),
|
|
4479
|
+
label: z.array(LayoutItemSchema).optional()
|
|
4480
|
+
})
|
|
4481
|
+
]);
|
|
4482
|
+
const ChartPictoJSONSchema = z.object({
|
|
4483
|
+
id: z.string(),
|
|
4484
|
+
type: z.literal(JSON_TYPES.CHART_PICTO),
|
|
4485
|
+
value: z.number(),
|
|
4486
|
+
answer: z.number().optional(),
|
|
4487
|
+
image: z.string().min(1).refine(
|
|
4488
|
+
(s) => IMAGE_SRC_SCHEME_RE.test(s),
|
|
4489
|
+
{ message: "image must use http(s) or data:image scheme" }
|
|
4490
|
+
),
|
|
4491
|
+
label: z.array(LayoutItemSchema).optional()
|
|
4492
|
+
});
|
|
4493
|
+
const ChartItemSchema = z.object({
|
|
4494
|
+
id: z.string(),
|
|
4495
|
+
type: z.literal(JSON_TYPES.CHART),
|
|
4496
|
+
name: z.string().optional(),
|
|
4497
|
+
title: z.string().optional(),
|
|
4498
|
+
chartType: z.enum(CHART_TYPES),
|
|
4499
|
+
orientation: z.enum(["vertical", "horizontal"]).default("vertical"),
|
|
4500
|
+
grid: z.boolean().default(false),
|
|
4501
|
+
scale: z.number().positive().default(1),
|
|
4502
|
+
snap: z.number().positive().optional(),
|
|
4503
|
+
xAxis: ChartAxisJSONSchema.optional(),
|
|
4504
|
+
yAxis: ChartAxisJSONSchema.optional(),
|
|
4505
|
+
bars: z.array(ChartBarJSONSchema).default([]),
|
|
4506
|
+
pictos: z.array(ChartPictoJSONSchema).default([])
|
|
4507
|
+
}).superRefine((item, ctx) => {
|
|
4508
|
+
if (item.chartType === "bar" && item.bars.length === 0) {
|
|
4509
|
+
ctx.addIssue({
|
|
4510
|
+
code: z.ZodIssueCode.custom,
|
|
4511
|
+
message: 'CHART[type="bar"]: at least one BAR is required',
|
|
4512
|
+
path: ["bars"]
|
|
4513
|
+
});
|
|
4514
|
+
}
|
|
4515
|
+
if (item.chartType === "pictograph" && item.pictos.length === 0) {
|
|
4516
|
+
ctx.addIssue({
|
|
4517
|
+
code: z.ZodIssueCode.custom,
|
|
4518
|
+
message: 'CHART[type="pictograph"]: at least one PICTO is required',
|
|
4519
|
+
path: ["pictos"]
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
if (item.chartType === "pictograph" && item.snap !== void 0) {
|
|
4523
|
+
ctx.addIssue({
|
|
4524
|
+
code: z.ZodIssueCode.custom,
|
|
4525
|
+
message: 'CHART[type="pictograph"]: snap is not supported for pictograph charts',
|
|
4526
|
+
path: ["snap"]
|
|
4527
|
+
});
|
|
4528
|
+
}
|
|
4529
|
+
});
|
|
3793
4530
|
const StringInputJSONSchema = z.object({
|
|
3794
4531
|
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
3795
4532
|
inputType: z.literal(INPUT_TYPES.STRING),
|
|
@@ -3848,6 +4585,14 @@ const PairMatcherWidgetJSONSchema = z.object({
|
|
|
3848
4585
|
z.array(MatcherItemContentSchema).min(1)
|
|
3849
4586
|
])
|
|
3850
4587
|
});
|
|
4588
|
+
const ChartBarWidgetJSONSchema = z.object({
|
|
4589
|
+
type: z.literal(JSON_TYPES.CHART_BAR),
|
|
4590
|
+
value: z.number()
|
|
4591
|
+
});
|
|
4592
|
+
const ChartPictoWidgetJSONSchema = z.object({
|
|
4593
|
+
type: z.literal(JSON_TYPES.CHART_PICTO),
|
|
4594
|
+
value: z.number()
|
|
4595
|
+
});
|
|
3851
4596
|
const VisualFractionJSONSchema = z.object({
|
|
3852
4597
|
type: z.literal(JSON_TYPES.VISUAL_FRACTION),
|
|
3853
4598
|
shape: z.enum(["bar", "circle", "triangle", "grid"]).default("bar"),
|
|
@@ -3918,7 +4663,9 @@ const WidgetJSONSchema = z.union([
|
|
|
3918
4663
|
GridSymmetryJSONSchema,
|
|
3919
4664
|
RulerWidgetSchema,
|
|
3920
4665
|
ProtractorWidgetSchema,
|
|
3921
|
-
SetSquareWidgetSchema
|
|
4666
|
+
SetSquareWidgetSchema,
|
|
4667
|
+
ChartBarWidgetJSONSchema,
|
|
4668
|
+
ChartPictoWidgetJSONSchema
|
|
3922
4669
|
]);
|
|
3923
4670
|
const ValidationJSONSchema = z.object({
|
|
3924
4671
|
mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION, VALIDATION_MODES.MIXED]),
|
|
@@ -4012,6 +4759,9 @@ function isVerticalOperationItem(item) {
|
|
|
4012
4759
|
function isClockItem(item) {
|
|
4013
4760
|
return item.type === JSON_TYPES.CLOCK;
|
|
4014
4761
|
}
|
|
4762
|
+
function isChartItem(item) {
|
|
4763
|
+
return item.type === JSON_TYPES.CHART;
|
|
4764
|
+
}
|
|
4015
4765
|
function isInlineInputJSON(widget) {
|
|
4016
4766
|
return widget.type === JSON_TYPES.INLINE_INPUT;
|
|
4017
4767
|
}
|
|
@@ -4033,6 +4783,12 @@ function isFractionJSON(widget) {
|
|
|
4033
4783
|
function isGridSymmetryJSON(widget) {
|
|
4034
4784
|
return widget.type === JSON_TYPES.GRID_SYMMETRY;
|
|
4035
4785
|
}
|
|
4786
|
+
function isChartBarWidgetJSON(widget) {
|
|
4787
|
+
return widget.type === JSON_TYPES.CHART_BAR;
|
|
4788
|
+
}
|
|
4789
|
+
function isChartPictoWidgetJSON(widget) {
|
|
4790
|
+
return widget.type === JSON_TYPES.CHART_PICTO;
|
|
4791
|
+
}
|
|
4036
4792
|
function isRulerWidget(widget) {
|
|
4037
4793
|
return widget.type === JSON_TYPES.RULER;
|
|
4038
4794
|
}
|
|
@@ -8522,7 +9278,7 @@ lodash.exports;
|
|
|
8522
9278
|
function valuesIn(object) {
|
|
8523
9279
|
return object == null ? [] : baseValues(object, keysIn(object));
|
|
8524
9280
|
}
|
|
8525
|
-
function
|
|
9281
|
+
function clamp2(number, lower, upper) {
|
|
8526
9282
|
if (upper === undefined$1) {
|
|
8527
9283
|
upper = lower;
|
|
8528
9284
|
lower = undefined$1;
|
|
@@ -9199,7 +9955,7 @@ lodash.exports;
|
|
|
9199
9955
|
lodash2.camelCase = camelCase;
|
|
9200
9956
|
lodash2.capitalize = capitalize;
|
|
9201
9957
|
lodash2.ceil = ceil;
|
|
9202
|
-
lodash2.clamp =
|
|
9958
|
+
lodash2.clamp = clamp2;
|
|
9203
9959
|
lodash2.clone = clone;
|
|
9204
9960
|
lodash2.cloneDeep = cloneDeep;
|
|
9205
9961
|
lodash2.cloneDeepWith = cloneDeepWith;
|
|
@@ -9623,6 +10379,15 @@ function toJSON(ast) {
|
|
|
9623
10379
|
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
9624
10380
|
type: JSON_TYPES.LINEBREAK
|
|
9625
10381
|
});
|
|
10382
|
+
} else if (block.type === AST_TYPES.CHART_BLOCK) {
|
|
10383
|
+
const { item: chartItem, chartWidgets, chartAnswers } = transformChart(
|
|
10384
|
+
block,
|
|
10385
|
+
idGenerator,
|
|
10386
|
+
inlineDragIds
|
|
10387
|
+
);
|
|
10388
|
+
layout.push(chartItem);
|
|
10389
|
+
for (const [id, w] of Object.entries(chartWidgets)) widgets[id] = w;
|
|
10390
|
+
for (const [id, a] of Object.entries(chartAnswers)) answers[id] = a;
|
|
9626
10391
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
9627
10392
|
const radioBlock = block;
|
|
9628
10393
|
layout.push({
|
|
@@ -9771,12 +10536,18 @@ function toJSON(ast) {
|
|
|
9771
10536
|
}
|
|
9772
10537
|
}
|
|
9773
10538
|
}
|
|
10539
|
+
let validationSection = {};
|
|
10540
|
+
if (ast.validation) {
|
|
10541
|
+
validationSection = { validation: transformValidation(ast.validation, answers, widgets) };
|
|
10542
|
+
} else if (Object.keys(answers).length > 0) {
|
|
10543
|
+
validationSection = { validation: { mode: VALIDATION_MODES.SIMPLE, answers } };
|
|
10544
|
+
}
|
|
9774
10545
|
return {
|
|
9775
10546
|
version: "2.0",
|
|
9776
10547
|
// Natural DSL version
|
|
9777
10548
|
layout,
|
|
9778
10549
|
widgets,
|
|
9779
|
-
...
|
|
10550
|
+
...validationSection
|
|
9780
10551
|
};
|
|
9781
10552
|
}
|
|
9782
10553
|
function collectAllInlines(blocks) {
|
|
@@ -9809,10 +10580,100 @@ function collectAllInlines(blocks) {
|
|
|
9809
10580
|
for (const item of pm.groups.flat()) {
|
|
9810
10581
|
result.push(...item.inlines);
|
|
9811
10582
|
}
|
|
10583
|
+
} else if (block.type === AST_TYPES.CHART_BLOCK) {
|
|
10584
|
+
const chart = block;
|
|
10585
|
+
for (const bar of chart.bars) result.push(...bar.inlines);
|
|
10586
|
+
for (const picto of chart.pictos) result.push(...picto.inlines);
|
|
10587
|
+
if (chart.xAxis) result.push(...chart.xAxis.inlines);
|
|
10588
|
+
if (chart.yAxis) result.push(...chart.yAxis.inlines);
|
|
9812
10589
|
}
|
|
9813
10590
|
}
|
|
9814
10591
|
return result;
|
|
9815
10592
|
}
|
|
10593
|
+
function transformChart(block, idGenerator, inlineDragIds) {
|
|
10594
|
+
const { orientation, valueAxis, categoryAxis } = resolveChartAxes(block);
|
|
10595
|
+
function transformAxisTitle(axis) {
|
|
10596
|
+
return axis !== void 0 && axis.inlines.length > 0 ? axis.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds)) : void 0;
|
|
10597
|
+
}
|
|
10598
|
+
function transformNumericAxis(axis) {
|
|
10599
|
+
const title = transformAxisTitle(axis.axis);
|
|
10600
|
+
return {
|
|
10601
|
+
min: axis.min,
|
|
10602
|
+
max: axis.max,
|
|
10603
|
+
step: axis.step,
|
|
10604
|
+
visible: axis.axis?.visible ?? true,
|
|
10605
|
+
...title !== void 0 && { title }
|
|
10606
|
+
};
|
|
10607
|
+
}
|
|
10608
|
+
function transformCategoryAxis(axis) {
|
|
10609
|
+
const title = transformAxisTitle(axis);
|
|
10610
|
+
const visible = axis?.visible ?? true;
|
|
10611
|
+
if (!title && visible) return void 0;
|
|
10612
|
+
return { visible, ...title && { title } };
|
|
10613
|
+
}
|
|
10614
|
+
function toLabel(inlines) {
|
|
10615
|
+
return inlines.length > 0 ? inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds)) : void 0;
|
|
10616
|
+
}
|
|
10617
|
+
const xAxis = orientation === "horizontal" ? transformNumericAxis(valueAxis) : transformCategoryAxis(categoryAxis);
|
|
10618
|
+
const yAxis = orientation === "horizontal" ? transformCategoryAxis(categoryAxis) : transformNumericAxis(valueAxis);
|
|
10619
|
+
const chartWidgets = {};
|
|
10620
|
+
const chartAnswers = {};
|
|
10621
|
+
const bars = block.bars.map((bar) => {
|
|
10622
|
+
const label = toLabel(bar.inlines);
|
|
10623
|
+
if (bar.answer !== void 0) {
|
|
10624
|
+
chartWidgets[bar.id] = { type: JSON_TYPES.CHART_BAR, value: bar.value };
|
|
10625
|
+
chartAnswers[bar.id] = bar.answer;
|
|
10626
|
+
return {
|
|
10627
|
+
id: bar.id,
|
|
10628
|
+
type: JSON_TYPES.CHART_BAR,
|
|
10629
|
+
interactive: true,
|
|
10630
|
+
value: bar.value,
|
|
10631
|
+
widgetId: bar.id,
|
|
10632
|
+
...bar.color !== void 0 && { color: bar.color },
|
|
10633
|
+
...label !== void 0 && { label }
|
|
10634
|
+
};
|
|
10635
|
+
}
|
|
10636
|
+
return {
|
|
10637
|
+
id: bar.id,
|
|
10638
|
+
type: JSON_TYPES.CHART_BAR,
|
|
10639
|
+
interactive: false,
|
|
10640
|
+
value: bar.value,
|
|
10641
|
+
...bar.color !== void 0 && { color: bar.color },
|
|
10642
|
+
...label !== void 0 && { label }
|
|
10643
|
+
};
|
|
10644
|
+
});
|
|
10645
|
+
const pictos = block.pictos.map((picto) => {
|
|
10646
|
+
const label = toLabel(picto.inlines);
|
|
10647
|
+
if (picto.answer !== void 0) {
|
|
10648
|
+
chartWidgets[picto.id] = { type: JSON_TYPES.CHART_PICTO, value: picto.value };
|
|
10649
|
+
chartAnswers[picto.id] = picto.answer;
|
|
10650
|
+
}
|
|
10651
|
+
return {
|
|
10652
|
+
id: picto.id,
|
|
10653
|
+
type: JSON_TYPES.CHART_PICTO,
|
|
10654
|
+
value: picto.value,
|
|
10655
|
+
image: picto.image,
|
|
10656
|
+
...picto.answer !== void 0 && { answer: picto.answer },
|
|
10657
|
+
...label !== void 0 && { label }
|
|
10658
|
+
};
|
|
10659
|
+
});
|
|
10660
|
+
const item = {
|
|
10661
|
+
id: idGenerator.generate(JSON_TYPES.CHART),
|
|
10662
|
+
type: JSON_TYPES.CHART,
|
|
10663
|
+
...block.name !== void 0 && { name: block.name },
|
|
10664
|
+
...block.title !== void 0 && { title: block.title },
|
|
10665
|
+
chartType: block.chartType,
|
|
10666
|
+
orientation,
|
|
10667
|
+
grid: block.grid,
|
|
10668
|
+
scale: block.scale,
|
|
10669
|
+
...block.snap !== void 0 && { snap: block.snap },
|
|
10670
|
+
...xAxis !== void 0 && { xAxis },
|
|
10671
|
+
...yAxis !== void 0 && { yAxis },
|
|
10672
|
+
bars,
|
|
10673
|
+
pictos
|
|
10674
|
+
};
|
|
10675
|
+
return { item, chartWidgets, chartAnswers };
|
|
10676
|
+
}
|
|
9816
10677
|
function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
|
|
9817
10678
|
const positions = numberLine.positions.map((pos) => {
|
|
9818
10679
|
const content = pos.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds));
|
|
@@ -10012,6 +10873,7 @@ function transformTable(table, idGenerator, inlineDragIds) {
|
|
|
10012
10873
|
id: idGenerator.generate(JSON_TYPES.TABLE),
|
|
10013
10874
|
type: JSON_TYPES.TABLE,
|
|
10014
10875
|
cols: table.cols,
|
|
10876
|
+
borderColor: table.borderColor,
|
|
10015
10877
|
rows
|
|
10016
10878
|
};
|
|
10017
10879
|
}
|
|
@@ -10179,17 +11041,6 @@ function extractVariables$1(expression, validVariables) {
|
|
|
10179
11041
|
}
|
|
10180
11042
|
return uniqueVars;
|
|
10181
11043
|
}
|
|
10182
|
-
function normalize(value) {
|
|
10183
|
-
if (typeof value === "number") {
|
|
10184
|
-
return String(Math.round(value * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
10185
|
-
}
|
|
10186
|
-
const trimmed = String(value).trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
10187
|
-
const asNum = Number(trimmed);
|
|
10188
|
-
if (trimmed !== "" && !isNaN(asNum)) {
|
|
10189
|
-
return String(Math.round(asNum * 10 ** INTERNAL_PRECISION) / 10 ** INTERNAL_PRECISION);
|
|
10190
|
-
}
|
|
10191
|
-
return trimmed;
|
|
10192
|
-
}
|
|
10193
11044
|
const NL_DRAG_PREFIX = "\0nl-drag\0";
|
|
10194
11045
|
const isNLDragEncoded = (v) => v.startsWith(NL_DRAG_PREFIX);
|
|
10195
11046
|
const encodeNLDragValue = (label, position) => `${NL_DRAG_PREFIX}${label}@${String(position)}`;
|
|
@@ -10338,16 +11189,6 @@ function validateSimple(exercise, userInputs) {
|
|
|
10338
11189
|
allCorrect = false;
|
|
10339
11190
|
}
|
|
10340
11191
|
}
|
|
10341
|
-
for (const widgetId of Object.keys(userInputs)) {
|
|
10342
|
-
if (!(widgetId in answers) && !(widgetId in details)) {
|
|
10343
|
-
details[widgetId] = {
|
|
10344
|
-
widgetId,
|
|
10345
|
-
userValue: userInputs[widgetId],
|
|
10346
|
-
isCorrect: true
|
|
10347
|
-
// No validation rule = assume correct
|
|
10348
|
-
};
|
|
10349
|
-
}
|
|
10350
|
-
}
|
|
10351
11192
|
return {
|
|
10352
11193
|
isCorrect: allCorrect,
|
|
10353
11194
|
details,
|
|
@@ -14521,7 +15362,9 @@ const DEFAULT_SOUNDS = {
|
|
|
14521
15362
|
"input-select": inputSelect,
|
|
14522
15363
|
"answer-correct": answerCorrect,
|
|
14523
15364
|
"answer-wrong": answerWrong,
|
|
14524
|
-
"answer-reveal": answerReveal
|
|
15365
|
+
"answer-reveal": answerReveal,
|
|
15366
|
+
"bar-step": buttonClick,
|
|
15367
|
+
"bar-release": dragDrop
|
|
14525
15368
|
};
|
|
14526
15369
|
function resolveSounds(overrides) {
|
|
14527
15370
|
return overrides ? { ...DEFAULT_SOUNDS, ...overrides } : DEFAULT_SOUNDS;
|
|
@@ -14806,27 +15649,41 @@ function useRotate(options) {
|
|
|
14806
15649
|
return { rotation, setRotation };
|
|
14807
15650
|
}
|
|
14808
15651
|
const REPEAT_DELAY_MS = 5e3;
|
|
14809
|
-
function useJiggle(
|
|
15652
|
+
function useJiggle(isDiscovered, options = {}) {
|
|
15653
|
+
const {
|
|
15654
|
+
ignoreBubbledAnimationEnd = false,
|
|
15655
|
+
repeatDelayMs = REPEAT_DELAY_MS
|
|
15656
|
+
} = options;
|
|
14810
15657
|
const [jiggling, setJiggling] = useState(true);
|
|
14811
15658
|
const [discovered, setDiscovered] = useState(false);
|
|
15659
|
+
const discoveredRef = useRef(false);
|
|
14812
15660
|
const timerRef = useRef(null);
|
|
14813
|
-
|
|
14814
|
-
if (
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
setJiggling(false);
|
|
14818
|
-
}
|
|
14819
|
-
}, [isDragging, discovered]);
|
|
14820
|
-
useEffect(() => () => {
|
|
14821
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
15661
|
+
const clearRepeatTimer = useCallback(() => {
|
|
15662
|
+
if (!timerRef.current) return;
|
|
15663
|
+
clearTimeout(timerRef.current);
|
|
15664
|
+
timerRef.current = null;
|
|
14822
15665
|
}, []);
|
|
14823
|
-
const
|
|
15666
|
+
const discover = useCallback(() => {
|
|
15667
|
+
discoveredRef.current = true;
|
|
15668
|
+
clearRepeatTimer();
|
|
15669
|
+
setDiscovered(true);
|
|
14824
15670
|
setJiggling(false);
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
}, [
|
|
14829
|
-
|
|
15671
|
+
}, [clearRepeatTimer]);
|
|
15672
|
+
useEffect(() => {
|
|
15673
|
+
if (isDiscovered) discover();
|
|
15674
|
+
}, [isDiscovered, discover]);
|
|
15675
|
+
useEffect(() => () => clearRepeatTimer(), [clearRepeatTimer]);
|
|
15676
|
+
const onAnimationEnd = useCallback((event) => {
|
|
15677
|
+
if (ignoreBubbledAnimationEnd && event && event.currentTarget !== event.target) return;
|
|
15678
|
+
setJiggling(false);
|
|
15679
|
+
if (discoveredRef.current) return;
|
|
15680
|
+
clearRepeatTimer();
|
|
15681
|
+
timerRef.current = setTimeout(() => {
|
|
15682
|
+
timerRef.current = null;
|
|
15683
|
+
if (!discoveredRef.current) setJiggling(true);
|
|
15684
|
+
}, repeatDelayMs);
|
|
15685
|
+
}, [clearRepeatTimer, ignoreBubbledAnimationEnd, repeatDelayMs]);
|
|
15686
|
+
return { jiggling: jiggling && !discovered, onAnimationEnd, discover };
|
|
14830
15687
|
}
|
|
14831
15688
|
const VW$2 = 320;
|
|
14832
15689
|
const VH$2 = 44;
|
|
@@ -16331,6 +17188,743 @@ const GridSymmetry = ({
|
|
|
16331
17188
|
}
|
|
16332
17189
|
) });
|
|
16333
17190
|
};
|
|
17191
|
+
function getChartClassName(item) {
|
|
17192
|
+
const isHorizontal = item.orientation === "horizontal";
|
|
17193
|
+
return [
|
|
17194
|
+
"chart",
|
|
17195
|
+
isHorizontal ? "chart--horizontal" : "",
|
|
17196
|
+
item.chartType === "pictograph" ? "chart--pictograph" : "",
|
|
17197
|
+
item.xAxis?.visible === false ? "chart--no-xaxis" : "",
|
|
17198
|
+
item.yAxis?.visible === false ? "chart--no-yaxis" : ""
|
|
17199
|
+
].filter(Boolean).join(" ");
|
|
17200
|
+
}
|
|
17201
|
+
function resolveItemValue(id, defaultValue2, values) {
|
|
17202
|
+
const raw = values[id];
|
|
17203
|
+
const parsed = parseFloat(raw ?? "");
|
|
17204
|
+
return isNaN(parsed) ? defaultValue2 : parsed;
|
|
17205
|
+
}
|
|
17206
|
+
function resolveValue(bar, values) {
|
|
17207
|
+
return bar.interactive ? resolveItemValue(bar.widgetId, bar.value, values) : bar.value;
|
|
17208
|
+
}
|
|
17209
|
+
function clamp(v, min, max) {
|
|
17210
|
+
return Math.min(max, Math.max(min, v));
|
|
17211
|
+
}
|
|
17212
|
+
function snapToStep(v, min, step) {
|
|
17213
|
+
const snapped = min + Math.round((v - min) / step) * step;
|
|
17214
|
+
return +snapped.toFixed(10);
|
|
17215
|
+
}
|
|
17216
|
+
function nextStepValue(v, axis, direction, snap) {
|
|
17217
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17218
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17219
|
+
const step = snap ?? axis.step ?? CHART_AXIS_DEFAULTS.step;
|
|
17220
|
+
const raw = direction === 1 ? min + Math.ceil((v - min) / step + 1e-9) * step : min + Math.floor((v - min) / step - 1e-9) * step;
|
|
17221
|
+
return +clamp(raw, min, max).toFixed(10);
|
|
17222
|
+
}
|
|
17223
|
+
function toFill(v, axis) {
|
|
17224
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17225
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17226
|
+
return clamp((v - min) / (max - min), 0, 1);
|
|
17227
|
+
}
|
|
17228
|
+
function barClasses(base, validationStatus) {
|
|
17229
|
+
const statusClass = validationStatus === "correct" ? "chart-bar-correct" : validationStatus === "incorrect" ? "chart-bar-incorrect" : null;
|
|
17230
|
+
return [...base, ...statusClass ? [statusClass] : []].join(" ");
|
|
17231
|
+
}
|
|
17232
|
+
function buildAxisTicks(axis) {
|
|
17233
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17234
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17235
|
+
const step = axis.step ?? CHART_AXIS_DEFAULTS.step;
|
|
17236
|
+
const ticks = [];
|
|
17237
|
+
for (let v = min; v <= max + 1e-9; v = +(v + step).toFixed(10)) {
|
|
17238
|
+
ticks.push(v);
|
|
17239
|
+
}
|
|
17240
|
+
if (ticks.length === 0 || Math.abs(ticks[ticks.length - 1] - max) > 1e-9) {
|
|
17241
|
+
ticks.push(max);
|
|
17242
|
+
}
|
|
17243
|
+
return ticks;
|
|
17244
|
+
}
|
|
17245
|
+
function axisTickPercent(value, axis) {
|
|
17246
|
+
const min = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17247
|
+
const max = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17248
|
+
return (value - min) / (max - min) * 100;
|
|
17249
|
+
}
|
|
17250
|
+
function buildAxisTickPositions(axis) {
|
|
17251
|
+
return buildAxisTicks(axis).map((value) => ({
|
|
17252
|
+
value,
|
|
17253
|
+
percent: axisTickPercent(value, axis)
|
|
17254
|
+
}));
|
|
17255
|
+
}
|
|
17256
|
+
function formatAxisTick(value) {
|
|
17257
|
+
return Number.isInteger(value) ? value : parseFloat(value.toPrecision(10));
|
|
17258
|
+
}
|
|
17259
|
+
const CHART_METRICS = {
|
|
17260
|
+
scaleMin: 0.6,
|
|
17261
|
+
scaleMax: 1.6,
|
|
17262
|
+
slotWidth: 5.2,
|
|
17263
|
+
gapWidth: 1,
|
|
17264
|
+
axisWidth: 2.35,
|
|
17265
|
+
barWidth: 3.35,
|
|
17266
|
+
fontSize: 0.9,
|
|
17267
|
+
labelFontSize: 0.98,
|
|
17268
|
+
handleSize: 1.15,
|
|
17269
|
+
chartHeight: 16
|
|
17270
|
+
};
|
|
17271
|
+
function computeBaseChartVars(scale, barCount) {
|
|
17272
|
+
const visualScale = clamp(scale, CHART_METRICS.scaleMin, CHART_METRICS.scaleMax);
|
|
17273
|
+
const slotWidth = CHART_METRICS.slotWidth * visualScale;
|
|
17274
|
+
const gapWidth = CHART_METRICS.gapWidth * visualScale;
|
|
17275
|
+
const trackLength = barCount * slotWidth + Math.max(0, barCount - 1) * gapWidth + gapWidth * 2;
|
|
17276
|
+
return {
|
|
17277
|
+
visualScale,
|
|
17278
|
+
trackLength,
|
|
17279
|
+
cssVars: {
|
|
17280
|
+
"--chart-axis-width": `${CHART_METRICS.axisWidth * visualScale}rem`,
|
|
17281
|
+
"--chart-bar-slot-width": `${slotWidth}rem`,
|
|
17282
|
+
"--chart-bar-width": `${CHART_METRICS.barWidth * visualScale}rem`,
|
|
17283
|
+
"--chart-bar-gap": `${gapWidth}rem`,
|
|
17284
|
+
"--chart-font-size": `${CHART_METRICS.fontSize * visualScale}rem`,
|
|
17285
|
+
"--chart-label-font-size": `${CHART_METRICS.labelFontSize * visualScale}rem`,
|
|
17286
|
+
"--chart-handle-size": `${CHART_METRICS.handleSize * visualScale}rem`
|
|
17287
|
+
}
|
|
17288
|
+
};
|
|
17289
|
+
}
|
|
17290
|
+
function computeChartStyle(scale, barCount) {
|
|
17291
|
+
const { trackLength, visualScale, cssVars } = computeBaseChartVars(scale, barCount);
|
|
17292
|
+
return {
|
|
17293
|
+
...cssVars,
|
|
17294
|
+
"--chart-height": `${CHART_METRICS.chartHeight * visualScale}rem`,
|
|
17295
|
+
"--chart-content-width": `${trackLength}rem`
|
|
17296
|
+
};
|
|
17297
|
+
}
|
|
17298
|
+
function computePictographChartStyle(scale, columnCount, totalSlots, orientation = "vertical") {
|
|
17299
|
+
const slots = Math.max(0, totalSlots);
|
|
17300
|
+
const gaps = Math.max(0, slots - 1);
|
|
17301
|
+
const slotSpan = `calc(${slots} * var(--picto-cell-size) + ${gaps} * var(--picto-cell-gap))`;
|
|
17302
|
+
if (orientation === "horizontal") {
|
|
17303
|
+
return {
|
|
17304
|
+
...computeHorizontalChartStyle(scale, columnCount),
|
|
17305
|
+
"--picto-total-slots": slots,
|
|
17306
|
+
// Horizontal columns run along the width, so the value axis is the width here.
|
|
17307
|
+
// Both vars move together to keep the X-axis ticks aligned with the icons.
|
|
17308
|
+
"--chart-h-value-width": slotSpan,
|
|
17309
|
+
"--chart-content-width": slotSpan
|
|
17310
|
+
};
|
|
17311
|
+
}
|
|
17312
|
+
return {
|
|
17313
|
+
...computeChartStyle(scale, columnCount),
|
|
17314
|
+
"--picto-total-slots": slots,
|
|
17315
|
+
"--chart-height": slotSpan
|
|
17316
|
+
};
|
|
17317
|
+
}
|
|
17318
|
+
function computeHorizontalChartStyle(scale, barCount) {
|
|
17319
|
+
const { trackLength, visualScale, cssVars } = computeBaseChartVars(scale, barCount);
|
|
17320
|
+
const valueWidth = CHART_METRICS.chartHeight * visualScale;
|
|
17321
|
+
return {
|
|
17322
|
+
...cssVars,
|
|
17323
|
+
"--chart-height": `${trackLength}rem`,
|
|
17324
|
+
"--chart-h-value-width": `${valueWidth}rem`,
|
|
17325
|
+
"--chart-content-width": `${valueWidth}rem`
|
|
17326
|
+
};
|
|
17327
|
+
}
|
|
17328
|
+
const YAxis = ({ axis }) => {
|
|
17329
|
+
const ticks = buildAxisTickPositions(axis);
|
|
17330
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-axis-y", children: ticks.map(({ value, percent }) => /* @__PURE__ */ jsx("span", { className: "chart-axis-tick-y", style: { bottom: `${percent}%` }, children: formatAxisTick(value) }, value)) });
|
|
17331
|
+
};
|
|
17332
|
+
const Grid = ({ axis, orientation = "vertical" }) => {
|
|
17333
|
+
const lines = buildAxisTickPositions(axis);
|
|
17334
|
+
return /* @__PURE__ */ jsx(Fragment, { children: lines.map(({ value, percent }) => {
|
|
17335
|
+
const style = orientation === "horizontal" ? { left: `${percent}%` } : { bottom: `${percent}%` };
|
|
17336
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-gridline", style }, value);
|
|
17337
|
+
}) });
|
|
17338
|
+
};
|
|
17339
|
+
const XAxis = ({ axis, title }) => {
|
|
17340
|
+
const ticks = buildAxisTickPositions(axis);
|
|
17341
|
+
return /* @__PURE__ */ jsxs("div", { className: "chart-axis-x-numeric", children: [
|
|
17342
|
+
/* @__PURE__ */ jsx("div", { className: "chart-axis-x-numeric-ticks", children: ticks.map(({ value, percent }) => /* @__PURE__ */ jsx("span", { className: "chart-axis-tick-x-num", style: { left: `${percent}%` }, children: formatAxisTick(value) }, value)) }),
|
|
17343
|
+
title && /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--x", children: title })
|
|
17344
|
+
] });
|
|
17345
|
+
};
|
|
17346
|
+
const DraggableBar = ({
|
|
17347
|
+
bar,
|
|
17348
|
+
currentValue,
|
|
17349
|
+
axis,
|
|
17350
|
+
snap,
|
|
17351
|
+
validationStatus,
|
|
17352
|
+
orientation = "vertical",
|
|
17353
|
+
onChange
|
|
17354
|
+
}) => {
|
|
17355
|
+
const trackRef = useRef(null);
|
|
17356
|
+
const dragging = useRef(false);
|
|
17357
|
+
const lastSnap = useRef(null);
|
|
17358
|
+
const preDragValue = useRef(0);
|
|
17359
|
+
const playSound = useSound();
|
|
17360
|
+
const exerciseLocked = useExerciseLocked();
|
|
17361
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
17362
|
+
const { jiggling, onAnimationEnd, discover } = useJiggle(isDragging, {
|
|
17363
|
+
ignoreBubbledAnimationEnd: true
|
|
17364
|
+
});
|
|
17365
|
+
const axisMin = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17366
|
+
const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17367
|
+
const axisStep = axis.step ?? CHART_AXIS_DEFAULTS.step;
|
|
17368
|
+
const snapStep = snap ?? axisStep;
|
|
17369
|
+
const fillFromPointer = useCallback(
|
|
17370
|
+
(clientY, clientX) => {
|
|
17371
|
+
const track = trackRef.current;
|
|
17372
|
+
if (!track) return;
|
|
17373
|
+
const rect = track.getBoundingClientRect();
|
|
17374
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
17375
|
+
let ratio;
|
|
17376
|
+
if (orientation === "horizontal") {
|
|
17377
|
+
ratio = clamp((clientX - rect.left) / rect.width, 0, 1);
|
|
17378
|
+
} else {
|
|
17379
|
+
ratio = clamp(1 - (clientY - rect.top) / rect.height, 0, 1);
|
|
17380
|
+
}
|
|
17381
|
+
const rawValue = axisMin + ratio * (axisMax - axisMin);
|
|
17382
|
+
const snapped = clamp(snapToStep(rawValue, axisMin, snapStep), axisMin, axisMax);
|
|
17383
|
+
if (snapped !== lastSnap.current) {
|
|
17384
|
+
lastSnap.current = snapped;
|
|
17385
|
+
playSound("bar-step");
|
|
17386
|
+
onChange(bar.widgetId, String(snapped));
|
|
17387
|
+
}
|
|
17388
|
+
},
|
|
17389
|
+
[axisMin, axisMax, snapStep, bar.widgetId, onChange, orientation, playSound]
|
|
17390
|
+
);
|
|
17391
|
+
const onPointerDown = useCallback(
|
|
17392
|
+
(e) => {
|
|
17393
|
+
if (exerciseLocked) return;
|
|
17394
|
+
e.preventDefault();
|
|
17395
|
+
discover();
|
|
17396
|
+
dragging.current = true;
|
|
17397
|
+
preDragValue.current = currentValue;
|
|
17398
|
+
lastSnap.current = clamp(snapToStep(currentValue, axisMin, snapStep), axisMin, axisMax);
|
|
17399
|
+
setIsDragging(true);
|
|
17400
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17401
|
+
fillFromPointer(e.clientY, e.clientX);
|
|
17402
|
+
},
|
|
17403
|
+
[fillFromPointer, currentValue, axisMin, axisMax, snapStep, discover, exerciseLocked]
|
|
17404
|
+
);
|
|
17405
|
+
const onPointerMove = useCallback(
|
|
17406
|
+
(e) => {
|
|
17407
|
+
if (!dragging.current) return;
|
|
17408
|
+
fillFromPointer(e.clientY, e.clientX);
|
|
17409
|
+
},
|
|
17410
|
+
[fillFromPointer]
|
|
17411
|
+
);
|
|
17412
|
+
const onPointerUp = useCallback(() => {
|
|
17413
|
+
if (!dragging.current) return;
|
|
17414
|
+
dragging.current = false;
|
|
17415
|
+
setIsDragging(false);
|
|
17416
|
+
playSound("bar-release");
|
|
17417
|
+
}, [playSound]);
|
|
17418
|
+
const onPointerCancel = useCallback(() => {
|
|
17419
|
+
if (!dragging.current) return;
|
|
17420
|
+
dragging.current = false;
|
|
17421
|
+
setIsDragging(false);
|
|
17422
|
+
onChange(bar.widgetId, String(preDragValue.current));
|
|
17423
|
+
}, [bar.widgetId, onChange]);
|
|
17424
|
+
const onKeyDown = useCallback(
|
|
17425
|
+
(e) => {
|
|
17426
|
+
if (exerciseLocked) return;
|
|
17427
|
+
const isInc = e.key === "ArrowUp" || e.key === "ArrowRight";
|
|
17428
|
+
const isDec = e.key === "ArrowDown" || e.key === "ArrowLeft";
|
|
17429
|
+
if (isInc) {
|
|
17430
|
+
e.preventDefault();
|
|
17431
|
+
playSound("bar-step");
|
|
17432
|
+
onChange(bar.widgetId, String(nextStepValue(currentValue, axis, 1, snap)));
|
|
17433
|
+
} else if (isDec) {
|
|
17434
|
+
e.preventDefault();
|
|
17435
|
+
playSound("bar-step");
|
|
17436
|
+
onChange(bar.widgetId, String(nextStepValue(currentValue, axis, -1, snap)));
|
|
17437
|
+
} else if (e.key === "Home") {
|
|
17438
|
+
e.preventDefault();
|
|
17439
|
+
playSound("bar-step");
|
|
17440
|
+
onChange(bar.widgetId, String(axisMin));
|
|
17441
|
+
} else if (e.key === "End") {
|
|
17442
|
+
e.preventDefault();
|
|
17443
|
+
playSound("bar-step");
|
|
17444
|
+
onChange(bar.widgetId, String(axisMax));
|
|
17445
|
+
}
|
|
17446
|
+
},
|
|
17447
|
+
[axisMin, axisMax, bar.widgetId, currentValue, axis, snap, onChange, playSound, exerciseLocked]
|
|
17448
|
+
);
|
|
17449
|
+
const fill = toFill(currentValue, axis);
|
|
17450
|
+
const isAtMin = currentValue <= axisMin;
|
|
17451
|
+
const ariaLabel = bar.label?.map((item) => "value" in item ? String(item.value) : "").filter(Boolean).join(" ") || bar.id;
|
|
17452
|
+
return /* @__PURE__ */ jsx(
|
|
17453
|
+
"div",
|
|
17454
|
+
{
|
|
17455
|
+
ref: trackRef,
|
|
17456
|
+
className: "chart-bar-track chart-bar-track--interactive",
|
|
17457
|
+
children: /* @__PURE__ */ jsx(
|
|
17458
|
+
"div",
|
|
17459
|
+
{
|
|
17460
|
+
role: "slider",
|
|
17461
|
+
tabIndex: 0,
|
|
17462
|
+
"aria-valuemin": axisMin,
|
|
17463
|
+
"aria-valuemax": axisMax,
|
|
17464
|
+
"aria-valuenow": currentValue,
|
|
17465
|
+
"aria-orientation": orientation === "horizontal" ? "horizontal" : "vertical",
|
|
17466
|
+
"aria-label": ariaLabel,
|
|
17467
|
+
"aria-disabled": exerciseLocked ? "true" : void 0,
|
|
17468
|
+
className: barClasses(
|
|
17469
|
+
[
|
|
17470
|
+
"chart-bar",
|
|
17471
|
+
orientation === "horizontal" ? "chart-bar--horizontal" : "chart-bar--vertical",
|
|
17472
|
+
"chart-bar-interactive",
|
|
17473
|
+
isAtMin ? "chart-bar-at-min" : "",
|
|
17474
|
+
jiggling && !isDragging && !exerciseLocked && validationStatus === null ? "chart-bar--peek" : ""
|
|
17475
|
+
],
|
|
17476
|
+
validationStatus
|
|
17477
|
+
),
|
|
17478
|
+
onAnimationEnd,
|
|
17479
|
+
onPointerDown,
|
|
17480
|
+
onPointerMove,
|
|
17481
|
+
onPointerUp,
|
|
17482
|
+
onPointerCancel,
|
|
17483
|
+
onKeyDown,
|
|
17484
|
+
style: {
|
|
17485
|
+
"--bar-fill": fill,
|
|
17486
|
+
"--chart-bar-instance-color": bar.color ?? "var(--chart-bar-color)"
|
|
17487
|
+
},
|
|
17488
|
+
children: /* @__PURE__ */ jsx("span", { className: "chart-bar-handle", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 10 10", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
17489
|
+
/* @__PURE__ */ jsx("path", { d: "M1.5 4.8 L5 1.5 L8.5 4.8", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }),
|
|
17490
|
+
/* @__PURE__ */ jsx("path", { d: "M1.5 8.5 L5 5.2 L8.5 8.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })
|
|
17491
|
+
] }) })
|
|
17492
|
+
}
|
|
17493
|
+
)
|
|
17494
|
+
}
|
|
17495
|
+
);
|
|
17496
|
+
};
|
|
17497
|
+
const StaticBar = ({
|
|
17498
|
+
bar,
|
|
17499
|
+
currentValue,
|
|
17500
|
+
axis,
|
|
17501
|
+
validationStatus,
|
|
17502
|
+
orientation = "vertical"
|
|
17503
|
+
}) => {
|
|
17504
|
+
const fill = toFill(currentValue, axis);
|
|
17505
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-bar-track", children: /* @__PURE__ */ jsx(
|
|
17506
|
+
"div",
|
|
17507
|
+
{
|
|
17508
|
+
className: barClasses(
|
|
17509
|
+
[
|
|
17510
|
+
"chart-bar",
|
|
17511
|
+
orientation === "horizontal" ? "chart-bar--horizontal" : "chart-bar--vertical"
|
|
17512
|
+
],
|
|
17513
|
+
validationStatus
|
|
17514
|
+
),
|
|
17515
|
+
style: {
|
|
17516
|
+
"--bar-fill": fill,
|
|
17517
|
+
"--chart-bar-instance-color": bar.color ?? "var(--chart-bar-color)",
|
|
17518
|
+
backgroundColor: "var(--chart-bar-instance-color)"
|
|
17519
|
+
}
|
|
17520
|
+
}
|
|
17521
|
+
) });
|
|
17522
|
+
};
|
|
17523
|
+
const EMPTY_SET$2 = /* @__PURE__ */ new Set();
|
|
17524
|
+
const XAxisLabel = ({
|
|
17525
|
+
bar,
|
|
17526
|
+
widgets,
|
|
17527
|
+
values,
|
|
17528
|
+
validationDetails,
|
|
17529
|
+
onChange
|
|
17530
|
+
}) => {
|
|
17531
|
+
if (!bar.label || bar.label.length === 0) return /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x" });
|
|
17532
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x", children: /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x-inner", children: bar.label.map((subItem) => /* @__PURE__ */ jsx(
|
|
17533
|
+
LayoutRenderer,
|
|
17534
|
+
{
|
|
17535
|
+
item: subItem,
|
|
17536
|
+
widgets,
|
|
17537
|
+
values,
|
|
17538
|
+
droppedInputs: EMPTY_SET$2,
|
|
17539
|
+
validationDetails,
|
|
17540
|
+
onChange
|
|
17541
|
+
},
|
|
17542
|
+
subItem.id
|
|
17543
|
+
)) }) });
|
|
17544
|
+
};
|
|
17545
|
+
const EMPTY_SET$1 = /* @__PURE__ */ new Set();
|
|
17546
|
+
const BarChart = ({
|
|
17547
|
+
item,
|
|
17548
|
+
widgets,
|
|
17549
|
+
values,
|
|
17550
|
+
validationDetails,
|
|
17551
|
+
onChange
|
|
17552
|
+
}) => {
|
|
17553
|
+
const { bars, grid } = item;
|
|
17554
|
+
const isHorizontal = item.orientation === "horizontal";
|
|
17555
|
+
const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
|
|
17556
|
+
const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
|
|
17557
|
+
const chartClass = getChartClassName(item);
|
|
17558
|
+
const renderAxisTitle = (title) => {
|
|
17559
|
+
if (!title?.length) return null;
|
|
17560
|
+
return /* @__PURE__ */ jsx(Fragment, { children: title.map((subItem) => /* @__PURE__ */ jsx(
|
|
17561
|
+
LayoutRenderer,
|
|
17562
|
+
{
|
|
17563
|
+
item: subItem,
|
|
17564
|
+
widgets,
|
|
17565
|
+
values,
|
|
17566
|
+
droppedInputs: EMPTY_SET$1,
|
|
17567
|
+
validationDetails,
|
|
17568
|
+
onChange
|
|
17569
|
+
},
|
|
17570
|
+
subItem.id
|
|
17571
|
+
)) });
|
|
17572
|
+
};
|
|
17573
|
+
const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
|
|
17574
|
+
const renderBar = (bar, orient = "vertical") => {
|
|
17575
|
+
const currentValue = resolveValue(bar, values);
|
|
17576
|
+
const vStatus = bar.interactive ? validationDetails?.[bar.widgetId] : void 0;
|
|
17577
|
+
const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
|
|
17578
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-bar-wrapper", children: bar.interactive ? /* @__PURE__ */ jsx(
|
|
17579
|
+
DraggableBar,
|
|
17580
|
+
{
|
|
17581
|
+
bar,
|
|
17582
|
+
currentValue,
|
|
17583
|
+
axis: fillAxis,
|
|
17584
|
+
snap: item.snap,
|
|
17585
|
+
validationStatus,
|
|
17586
|
+
orientation: orient,
|
|
17587
|
+
onChange
|
|
17588
|
+
}
|
|
17589
|
+
) : /* @__PURE__ */ jsx(
|
|
17590
|
+
StaticBar,
|
|
17591
|
+
{
|
|
17592
|
+
bar,
|
|
17593
|
+
currentValue,
|
|
17594
|
+
axis: fillAxis,
|
|
17595
|
+
validationStatus,
|
|
17596
|
+
orientation: orient
|
|
17597
|
+
}
|
|
17598
|
+
) }, bar.id);
|
|
17599
|
+
};
|
|
17600
|
+
if (isHorizontal) {
|
|
17601
|
+
const chartStyle2 = computeHorizontalChartStyle(item.scale, bars.length);
|
|
17602
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle2, children: [
|
|
17603
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17604
|
+
categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y-h", children: renderAxisTitle(categoryAxis.title) }) : null,
|
|
17605
|
+
/* @__PURE__ */ jsx("div", { className: "chart-axis-y--categories", children: bars.map((bar) => /* @__PURE__ */ jsx("div", { className: "chart-bar-y-label", children: bar.label?.map((subItem) => /* @__PURE__ */ jsx(
|
|
17606
|
+
LayoutRenderer,
|
|
17607
|
+
{
|
|
17608
|
+
item: subItem,
|
|
17609
|
+
widgets,
|
|
17610
|
+
values,
|
|
17611
|
+
droppedInputs: EMPTY_SET$1,
|
|
17612
|
+
validationDetails,
|
|
17613
|
+
onChange
|
|
17614
|
+
},
|
|
17615
|
+
subItem.id
|
|
17616
|
+
)) }, bar.id)) }),
|
|
17617
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17618
|
+
grid && /* @__PURE__ */ jsx(Grid, { axis: fillAxis, orientation: "horizontal" }),
|
|
17619
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: bars.map((b) => renderBar(b, "horizontal")) })
|
|
17620
|
+
] }),
|
|
17621
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(XAxis, { axis: valueAxis, title: renderAxisTitle(valueAxis.title) })
|
|
17622
|
+
] }) }) });
|
|
17623
|
+
}
|
|
17624
|
+
const chartStyle = computeChartStyle(item.scale, bars.length);
|
|
17625
|
+
const hasAnyLabel = bars.some((b) => b.label && b.label.length > 0);
|
|
17626
|
+
const xAxisRow = hasAnyLabel ? /* @__PURE__ */ jsx("div", { className: "chart-axis-x", children: bars.map((bar) => /* @__PURE__ */ jsx(
|
|
17627
|
+
XAxisLabel,
|
|
17628
|
+
{
|
|
17629
|
+
bar,
|
|
17630
|
+
widgets,
|
|
17631
|
+
values,
|
|
17632
|
+
validationDetails,
|
|
17633
|
+
onChange
|
|
17634
|
+
},
|
|
17635
|
+
bar.id
|
|
17636
|
+
)) }) : null;
|
|
17637
|
+
const xAxisTitle = categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--x-category", children: renderAxisTitle(categoryAxis.title) }) : null;
|
|
17638
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle, children: [
|
|
17639
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17640
|
+
valueAxis?.visible && valueAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y", children: renderAxisTitle(valueAxis.title) }) : null,
|
|
17641
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(YAxis, { axis: valueAxis }),
|
|
17642
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17643
|
+
grid && /* @__PURE__ */ jsx(Grid, { axis: fillAxis }),
|
|
17644
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: bars.map((b) => renderBar(b)) })
|
|
17645
|
+
] }),
|
|
17646
|
+
xAxisRow,
|
|
17647
|
+
xAxisTitle
|
|
17648
|
+
] }) }) });
|
|
17649
|
+
};
|
|
17650
|
+
const clampSlotCount = (value, axisMin, totalSlots) => Math.round(Math.min(Math.max(value - axisMin, 0), totalSlots));
|
|
17651
|
+
const buildSelectedAnswerSlots = (totalSlots, baseFilledCount, selectedCount) => {
|
|
17652
|
+
const slots = Array.from({ length: totalSlots }, () => false);
|
|
17653
|
+
const end = Math.min(totalSlots, baseFilledCount + selectedCount);
|
|
17654
|
+
for (let i = baseFilledCount; i < end; i++) slots[i] = true;
|
|
17655
|
+
return slots;
|
|
17656
|
+
};
|
|
17657
|
+
const PictographColumn = ({
|
|
17658
|
+
picto,
|
|
17659
|
+
currentValue,
|
|
17660
|
+
axis,
|
|
17661
|
+
validationStatus,
|
|
17662
|
+
orientation = "vertical",
|
|
17663
|
+
onChange
|
|
17664
|
+
}) => {
|
|
17665
|
+
const axisMin = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17666
|
+
const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17667
|
+
const isInteractive = picto.answer !== void 0;
|
|
17668
|
+
const totalSlots = Math.round(axisMax - axisMin);
|
|
17669
|
+
const baseFilledCount = clampSlotCount(picto.value, axisMin, totalSlots);
|
|
17670
|
+
const currentFilledCount = clampSlotCount(currentValue, axisMin, totalSlots);
|
|
17671
|
+
const answerSlotCount = Math.max(0, totalSlots - baseFilledCount);
|
|
17672
|
+
const selectedAnswerCount = Math.min(
|
|
17673
|
+
Math.max(currentFilledCount - baseFilledCount, 0),
|
|
17674
|
+
answerSlotCount
|
|
17675
|
+
);
|
|
17676
|
+
const [selectedSlots, setSelectedSlots] = useState(
|
|
17677
|
+
() => buildSelectedAnswerSlots(totalSlots, baseFilledCount, selectedAnswerCount)
|
|
17678
|
+
);
|
|
17679
|
+
const lastInternalValueRef = useRef(null);
|
|
17680
|
+
const [hasInteracted, setHasInteracted] = useState(false);
|
|
17681
|
+
const { jiggling: hinting, onAnimationEnd: onHintEnd } = useJiggle(hasInteracted);
|
|
17682
|
+
const playSound = useSound();
|
|
17683
|
+
const exerciseLocked = useExerciseLocked();
|
|
17684
|
+
useEffect(() => {
|
|
17685
|
+
const nextValue = String(axisMin + baseFilledCount + selectedAnswerCount);
|
|
17686
|
+
if (nextValue === lastInternalValueRef.current) return;
|
|
17687
|
+
lastInternalValueRef.current = null;
|
|
17688
|
+
setSelectedSlots(buildSelectedAnswerSlots(totalSlots, baseFilledCount, selectedAnswerCount));
|
|
17689
|
+
}, [axisMin, baseFilledCount, selectedAnswerCount, totalSlots]);
|
|
17690
|
+
const handleCellClick = useCallback(
|
|
17691
|
+
(cellIndex) => {
|
|
17692
|
+
if (exerciseLocked || !isInteractive || cellIndex < baseFilledCount) return;
|
|
17693
|
+
if (!hasInteracted) setHasInteracted(true);
|
|
17694
|
+
const slotRelative = cellIndex - baseFilledCount;
|
|
17695
|
+
const isCurrentlyFilled = Boolean(selectedSlots[cellIndex]);
|
|
17696
|
+
const nextCount = isCurrentlyFilled ? slotRelative : slotRelative + 1;
|
|
17697
|
+
const nextSlots = buildSelectedAnswerSlots(totalSlots, baseFilledCount, nextCount);
|
|
17698
|
+
const nextValue = axisMin + baseFilledCount + nextCount;
|
|
17699
|
+
lastInternalValueRef.current = String(nextValue);
|
|
17700
|
+
setSelectedSlots(nextSlots);
|
|
17701
|
+
onChange(picto.id, String(nextValue));
|
|
17702
|
+
playSound("fraction-shade");
|
|
17703
|
+
},
|
|
17704
|
+
[exerciseLocked, isInteractive, baseFilledCount, totalSlots, selectedSlots, axisMin, onChange, picto.id, playSound, hasInteracted]
|
|
17705
|
+
);
|
|
17706
|
+
const statusClass = validationStatus === "correct" ? "picto-column--correct" : validationStatus === "incorrect" ? "picto-column--incorrect" : "";
|
|
17707
|
+
const cellCount = isInteractive ? totalSlots : currentFilledCount;
|
|
17708
|
+
const cells = Array.from({ length: cellCount }, (_, i) => {
|
|
17709
|
+
const isPrefilled = isInteractive ? i < baseFilledCount : i < currentFilledCount;
|
|
17710
|
+
const isSelectedAnswer = isInteractive && i >= baseFilledCount && Boolean(selectedSlots[i]);
|
|
17711
|
+
const canToggle = isInteractive && i >= baseFilledCount;
|
|
17712
|
+
const isActive = isPrefilled || isSelectedAnswer;
|
|
17713
|
+
const isInactiveFn = !isActive && isInteractive;
|
|
17714
|
+
const isLastInteractive = canToggle && i === cellCount - 1;
|
|
17715
|
+
const animClass = validationStatus === "correct" && isActive ? "picto-cell--pop" : validationStatus === "incorrect" && isActive ? "picto-cell--jiggle" : hinting && validationStatus === null && !exerciseLocked && canToggle && !isActive ? "picto-cell--hint" : "";
|
|
17716
|
+
const cellClass = [
|
|
17717
|
+
"picto-cell",
|
|
17718
|
+
isInactiveFn ? "picto-cell--inactive" : "",
|
|
17719
|
+
canToggle ? "picto-cell--interactive" : "",
|
|
17720
|
+
isInteractive && !canToggle ? "picto-cell--locked" : "",
|
|
17721
|
+
animClass
|
|
17722
|
+
].filter(Boolean).join(" ");
|
|
17723
|
+
const imgStyle = animClass === "picto-cell--pop" || animClass === "picto-cell--jiggle" ? { animationDelay: `${i * 50}ms` } : animClass === "picto-cell--hint" ? { animationDelay: `${(i - baseFilledCount) * 80}ms` } : void 0;
|
|
17724
|
+
return /* @__PURE__ */ jsx(
|
|
17725
|
+
"div",
|
|
17726
|
+
{
|
|
17727
|
+
className: cellClass,
|
|
17728
|
+
onClick: canToggle ? () => handleCellClick(i) : void 0,
|
|
17729
|
+
role: isInteractive ? "checkbox" : void 0,
|
|
17730
|
+
"aria-checked": isInteractive ? isActive : void 0,
|
|
17731
|
+
"aria-disabled": isInteractive && (!canToggle || exerciseLocked) ? true : void 0,
|
|
17732
|
+
"aria-label": isInteractive ? `${picto.id} icon ${i + 1}` : void 0,
|
|
17733
|
+
tabIndex: canToggle ? 0 : void 0,
|
|
17734
|
+
onKeyDown: canToggle ? (e) => {
|
|
17735
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
17736
|
+
e.preventDefault();
|
|
17737
|
+
handleCellClick(i);
|
|
17738
|
+
}
|
|
17739
|
+
} : void 0,
|
|
17740
|
+
children: /* @__PURE__ */ jsx(
|
|
17741
|
+
"img",
|
|
17742
|
+
{
|
|
17743
|
+
src: picto.image,
|
|
17744
|
+
alt: "",
|
|
17745
|
+
"aria-hidden": "true",
|
|
17746
|
+
style: imgStyle,
|
|
17747
|
+
onAnimationEnd: isLastInteractive && animClass === "picto-cell--hint" ? onHintEnd : void 0
|
|
17748
|
+
}
|
|
17749
|
+
)
|
|
17750
|
+
},
|
|
17751
|
+
i
|
|
17752
|
+
);
|
|
17753
|
+
});
|
|
17754
|
+
const columnClass = [
|
|
17755
|
+
"chart-picto-column-wrapper",
|
|
17756
|
+
orientation === "horizontal" ? "chart-picto-column-wrapper--h" : "",
|
|
17757
|
+
statusClass
|
|
17758
|
+
].filter(Boolean).join(" ");
|
|
17759
|
+
return /* @__PURE__ */ jsx("div", { className: columnClass, children: /* @__PURE__ */ jsx("div", { className: `chart-picto-cells${orientation === "horizontal" ? " chart-picto-cells--h" : ""}`, children: cells }) });
|
|
17760
|
+
};
|
|
17761
|
+
const PictographGrid = ({
|
|
17762
|
+
axis,
|
|
17763
|
+
columnCount,
|
|
17764
|
+
orientation = "vertical"
|
|
17765
|
+
}) => {
|
|
17766
|
+
const axisMin = axis.min ?? CHART_AXIS_DEFAULTS.min;
|
|
17767
|
+
const axisMax = axis.max ?? CHART_AXIS_DEFAULTS.max;
|
|
17768
|
+
const totalSlots = Math.round(axisMax - axisMin);
|
|
17769
|
+
if (orientation === "horizontal") {
|
|
17770
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17771
|
+
Array.from({ length: columnCount - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17772
|
+
"div",
|
|
17773
|
+
{
|
|
17774
|
+
className: "picto-gridline picto-gridline--row",
|
|
17775
|
+
style: {
|
|
17776
|
+
top: `calc(var(--chart-bar-gap) + ${i + 1} * (var(--chart-bar-slot-width) + var(--chart-bar-gap)))`
|
|
17777
|
+
}
|
|
17778
|
+
},
|
|
17779
|
+
`row-${i}`
|
|
17780
|
+
)),
|
|
17781
|
+
Array.from({ length: totalSlots - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17782
|
+
"div",
|
|
17783
|
+
{
|
|
17784
|
+
className: "picto-gridline picto-gridline--col",
|
|
17785
|
+
style: {
|
|
17786
|
+
left: `calc(${i + 1} * (var(--picto-cell-size) + var(--picto-cell-gap)))`
|
|
17787
|
+
}
|
|
17788
|
+
},
|
|
17789
|
+
`col-${i}`
|
|
17790
|
+
))
|
|
17791
|
+
] });
|
|
17792
|
+
}
|
|
17793
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17794
|
+
Array.from({ length: totalSlots - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17795
|
+
"div",
|
|
17796
|
+
{
|
|
17797
|
+
className: "picto-gridline picto-gridline--row",
|
|
17798
|
+
style: {
|
|
17799
|
+
bottom: `calc(${i + 1} * (var(--picto-cell-size) + var(--picto-cell-gap)))`
|
|
17800
|
+
}
|
|
17801
|
+
},
|
|
17802
|
+
`row-${i}`
|
|
17803
|
+
)),
|
|
17804
|
+
Array.from({ length: columnCount - 1 }, (_, i) => /* @__PURE__ */ jsx(
|
|
17805
|
+
"div",
|
|
17806
|
+
{
|
|
17807
|
+
className: "picto-gridline picto-gridline--col",
|
|
17808
|
+
style: {
|
|
17809
|
+
left: `calc(var(--chart-bar-gap) + ${i + 1} * (var(--chart-bar-slot-width) + var(--chart-bar-gap)))`
|
|
17810
|
+
}
|
|
17811
|
+
},
|
|
17812
|
+
`col-${i}`
|
|
17813
|
+
))
|
|
17814
|
+
] });
|
|
17815
|
+
};
|
|
17816
|
+
const EMPTY_SET = /* @__PURE__ */ new Set();
|
|
17817
|
+
const PictographChart = ({
|
|
17818
|
+
item,
|
|
17819
|
+
widgets,
|
|
17820
|
+
values,
|
|
17821
|
+
validationDetails,
|
|
17822
|
+
onChange
|
|
17823
|
+
}) => {
|
|
17824
|
+
const { pictos = [], grid } = item;
|
|
17825
|
+
const isHorizontal = item.orientation === "horizontal";
|
|
17826
|
+
const valueAxis = isHorizontal ? item.xAxis : item.yAxis;
|
|
17827
|
+
const categoryAxis = isHorizontal ? item.yAxis : item.xAxis;
|
|
17828
|
+
const chartClass = getChartClassName(item);
|
|
17829
|
+
const renderAxisTitle = (title) => {
|
|
17830
|
+
if (!title?.length) return null;
|
|
17831
|
+
return /* @__PURE__ */ jsx(Fragment, { children: title.map((subItem) => /* @__PURE__ */ jsx(
|
|
17832
|
+
LayoutRenderer,
|
|
17833
|
+
{
|
|
17834
|
+
item: subItem,
|
|
17835
|
+
widgets,
|
|
17836
|
+
values,
|
|
17837
|
+
droppedInputs: EMPTY_SET,
|
|
17838
|
+
validationDetails,
|
|
17839
|
+
onChange
|
|
17840
|
+
},
|
|
17841
|
+
subItem.id
|
|
17842
|
+
)) });
|
|
17843
|
+
};
|
|
17844
|
+
const fillAxis = valueAxis ?? CHART_AXIS_DEFAULTS;
|
|
17845
|
+
const totalSlots = Math.round(
|
|
17846
|
+
(fillAxis.max ?? CHART_AXIS_DEFAULTS.max) - (fillAxis.min ?? CHART_AXIS_DEFAULTS.min)
|
|
17847
|
+
);
|
|
17848
|
+
const renderColumn = (picto, orient = "vertical") => {
|
|
17849
|
+
const currentValue = resolveItemValue(picto.id, picto.value, values);
|
|
17850
|
+
const vStatus = picto.answer !== void 0 ? validationDetails?.[picto.id] : void 0;
|
|
17851
|
+
const validationStatus = vStatus?.isCorrect === true ? "correct" : vStatus?.isCorrect === false ? "incorrect" : null;
|
|
17852
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-bar-wrapper", children: /* @__PURE__ */ jsx(
|
|
17853
|
+
PictographColumn,
|
|
17854
|
+
{
|
|
17855
|
+
picto,
|
|
17856
|
+
currentValue,
|
|
17857
|
+
axis: fillAxis,
|
|
17858
|
+
validationStatus,
|
|
17859
|
+
orientation: orient,
|
|
17860
|
+
onChange
|
|
17861
|
+
}
|
|
17862
|
+
) }, picto.id);
|
|
17863
|
+
};
|
|
17864
|
+
if (isHorizontal) {
|
|
17865
|
+
const chartStyle2 = computePictographChartStyle(item.scale, pictos.length, totalSlots, "horizontal");
|
|
17866
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle2, children: [
|
|
17867
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17868
|
+
categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y-h", children: renderAxisTitle(categoryAxis.title) }) : null,
|
|
17869
|
+
/* @__PURE__ */ jsx("div", { className: "chart-axis-y--categories", children: pictos.map((picto) => /* @__PURE__ */ jsx("div", { className: "chart-bar-y-label", children: picto.label?.map((subItem) => /* @__PURE__ */ jsx(
|
|
17870
|
+
LayoutRenderer,
|
|
17871
|
+
{
|
|
17872
|
+
item: subItem,
|
|
17873
|
+
widgets,
|
|
17874
|
+
values,
|
|
17875
|
+
droppedInputs: EMPTY_SET,
|
|
17876
|
+
validationDetails,
|
|
17877
|
+
onChange
|
|
17878
|
+
},
|
|
17879
|
+
subItem.id
|
|
17880
|
+
)) }, picto.id)) }),
|
|
17881
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17882
|
+
grid && /* @__PURE__ */ jsx(PictographGrid, { axis: fillAxis, columnCount: pictos.length, orientation: "horizontal" }),
|
|
17883
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: pictos.map((p) => renderColumn(p, "horizontal")) })
|
|
17884
|
+
] }),
|
|
17885
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(XAxis, { axis: valueAxis, title: renderAxisTitle(valueAxis.title) })
|
|
17886
|
+
] }) }) });
|
|
17887
|
+
}
|
|
17888
|
+
const chartStyle = computePictographChartStyle(item.scale, pictos.length, totalSlots);
|
|
17889
|
+
const hasAnyLabel = pictos.some((p) => p.label && p.label.length > 0);
|
|
17890
|
+
const xAxisRow = hasAnyLabel ? /* @__PURE__ */ jsx("div", { className: "chart-axis-x", children: pictos.map((picto) => /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x", children: /* @__PURE__ */ jsx("div", { className: "chart-axis-tick-x-inner", children: picto.label?.map((subItem) => /* @__PURE__ */ jsx(
|
|
17891
|
+
LayoutRenderer,
|
|
17892
|
+
{
|
|
17893
|
+
item: subItem,
|
|
17894
|
+
widgets,
|
|
17895
|
+
values,
|
|
17896
|
+
droppedInputs: EMPTY_SET,
|
|
17897
|
+
validationDetails,
|
|
17898
|
+
onChange
|
|
17899
|
+
},
|
|
17900
|
+
subItem.id
|
|
17901
|
+
)) }) }, picto.id)) }) : null;
|
|
17902
|
+
const xAxisTitle = categoryAxis?.visible && categoryAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--x-category", children: renderAxisTitle(categoryAxis.title) }) : null;
|
|
17903
|
+
return /* @__PURE__ */ jsx("div", { className: "chart-container", children: /* @__PURE__ */ jsx("div", { className: "chart-scroll", children: /* @__PURE__ */ jsxs("div", { className: chartClass, dir: "ltr", style: chartStyle, children: [
|
|
17904
|
+
item.title && /* @__PURE__ */ jsx("div", { className: "chart-title", children: item.title }),
|
|
17905
|
+
valueAxis?.visible && valueAxis.title?.length ? /* @__PURE__ */ jsx("div", { className: "chart-axis-title chart-axis-title--y", children: renderAxisTitle(valueAxis.title) }) : null,
|
|
17906
|
+
valueAxis && valueAxis.visible && /* @__PURE__ */ jsx(YAxis, { axis: valueAxis }),
|
|
17907
|
+
/* @__PURE__ */ jsxs("div", { className: "chart-plot", children: [
|
|
17908
|
+
grid && /* @__PURE__ */ jsx(PictographGrid, { axis: fillAxis, columnCount: pictos.length }),
|
|
17909
|
+
/* @__PURE__ */ jsx("div", { className: "chart-bars", children: pictos.map((p) => renderColumn(p)) })
|
|
17910
|
+
] }),
|
|
17911
|
+
xAxisRow,
|
|
17912
|
+
xAxisTitle
|
|
17913
|
+
] }) }) });
|
|
17914
|
+
};
|
|
17915
|
+
const CHART_TYPE_COMPONENTS = {
|
|
17916
|
+
bar: BarChart,
|
|
17917
|
+
pictograph: PictographChart
|
|
17918
|
+
// line: LineChart,
|
|
17919
|
+
// pie: PieChart,
|
|
17920
|
+
};
|
|
17921
|
+
const Chart = (props) => {
|
|
17922
|
+
const ChartComponent = CHART_TYPE_COMPONENTS[props.item.chartType];
|
|
17923
|
+
if (!ChartComponent) {
|
|
17924
|
+
return /* @__PURE__ */ jsx("div", { className: "chart chart--unsupported", children: /* @__PURE__ */ jsx("span", { style: { color: "var(--exercise-incorrect, #ef4444)", fontSize: "0.85rem" }, children: `Chart type "${props.item.chartType}" is not yet supported.` }) });
|
|
17925
|
+
}
|
|
17926
|
+
return /* @__PURE__ */ jsx(ChartComponent, { ...props });
|
|
17927
|
+
};
|
|
16334
17928
|
const RadioOptions = ({
|
|
16335
17929
|
name,
|
|
16336
17930
|
options,
|
|
@@ -18457,11 +20051,14 @@ const NumberLineRenderer = ({
|
|
|
18457
20051
|
onChangeRef.current = onChange;
|
|
18458
20052
|
const valuesRef = useRef(values);
|
|
18459
20053
|
valuesRef.current = values;
|
|
20054
|
+
const prevCounterCountRef = useRef(null);
|
|
18460
20055
|
useEffect(() => {
|
|
18461
20056
|
if (counter === void 0) return;
|
|
18462
20057
|
const next = String(counterCount);
|
|
20058
|
+
const isSeed = prevCounterCountRef.current === null || prevCounterCountRef.current === counterCount;
|
|
20059
|
+
prevCounterCountRef.current = counterCount;
|
|
18463
20060
|
if (valuesRef.current[counter.id] !== next) {
|
|
18464
|
-
onChangeRef.current(counter.id, next);
|
|
20061
|
+
onChangeRef.current(counter.id, next, false, void 0, isSeed);
|
|
18465
20062
|
}
|
|
18466
20063
|
}, [counterCount, counter?.id]);
|
|
18467
20064
|
const cursorKeys = useMemo(
|
|
@@ -18475,7 +20072,7 @@ const NumberLineRenderer = ({
|
|
|
18475
20072
|
for (const cursor of cursors ?? []) {
|
|
18476
20073
|
const existing = valuesRef.current[cursor.id];
|
|
18477
20074
|
if (!isDefinitionChange && existing !== void 0 && existing !== "") continue;
|
|
18478
|
-
onChangeRef.current(cursor.id, String(cursor.value));
|
|
20075
|
+
onChangeRef.current(cursor.id, String(cursor.value), false, void 0, true);
|
|
18479
20076
|
}
|
|
18480
20077
|
}, [cursorKeys]);
|
|
18481
20078
|
const effectiveStep = useMemo(() => {
|
|
@@ -19038,7 +20635,10 @@ const LayoutRenderer = ({
|
|
|
19038
20635
|
)) });
|
|
19039
20636
|
case JSON_TYPES.TABLE: {
|
|
19040
20637
|
const totalRows = item.rows.length;
|
|
19041
|
-
|
|
20638
|
+
const tableStyle = {
|
|
20639
|
+
"--exercise-table-border-color": item.borderColor ?? DEFAULT_TABLE_BORDER_COLOR
|
|
20640
|
+
};
|
|
20641
|
+
return /* @__PURE__ */ jsx("div", { className: "exercise-table-container", style: tableStyle, children: /* @__PURE__ */ jsx("table", { className: "exercise-table", children: /* @__PURE__ */ jsx("tbody", { children: item.rows.map((row, rowIndex) => /* @__PURE__ */ jsx("tr", { className: "exercise-table-row", style: { backgroundColor: row.backgroundColor }, children: row.cells.map((cell) => {
|
|
19042
20642
|
const rowspan = cell.rowspan || 1;
|
|
19043
20643
|
const isLastRow = rowIndex + rowspan - 1 >= totalRows - 1;
|
|
19044
20644
|
return /* @__PURE__ */ jsx(
|
|
@@ -19217,6 +20817,20 @@ const LayoutRenderer = ({
|
|
|
19217
20817
|
}
|
|
19218
20818
|
);
|
|
19219
20819
|
}
|
|
20820
|
+
case JSON_TYPES.CHART: {
|
|
20821
|
+
const chartItem = item;
|
|
20822
|
+
return /* @__PURE__ */ jsx(
|
|
20823
|
+
Chart,
|
|
20824
|
+
{
|
|
20825
|
+
item: chartItem,
|
|
20826
|
+
widgets,
|
|
20827
|
+
values,
|
|
20828
|
+
validationDetails,
|
|
20829
|
+
onChange: (barId, val) => onChange(barId, val)
|
|
20830
|
+
},
|
|
20831
|
+
resetKey
|
|
20832
|
+
);
|
|
20833
|
+
}
|
|
19220
20834
|
default:
|
|
19221
20835
|
return null;
|
|
19222
20836
|
}
|
|
@@ -19230,18 +20844,30 @@ const FLIP_BTN_EXTRA = 44;
|
|
|
19230
20844
|
function buildInitialValues(json) {
|
|
19231
20845
|
const initial = {};
|
|
19232
20846
|
for (const [id, widget] of Object.entries(json.widgets)) {
|
|
19233
|
-
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER)
|
|
19234
|
-
|
|
20847
|
+
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) {
|
|
20848
|
+
initial[id] = [];
|
|
20849
|
+
} else if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
19235
20850
|
initial[id] = widget.cells.filter((cell) => isCellInInteractiveArea(
|
|
19236
20851
|
cell,
|
|
19237
20852
|
widget.rows,
|
|
19238
20853
|
widget.cols,
|
|
19239
20854
|
widget.interactiveArea
|
|
19240
20855
|
));
|
|
20856
|
+
} else if (widget.type === JSON_TYPES.CHART_BAR || widget.type === JSON_TYPES.CHART_PICTO) {
|
|
20857
|
+
initial[id] = String(widget.value);
|
|
19241
20858
|
}
|
|
19242
20859
|
}
|
|
19243
20860
|
return initial;
|
|
19244
20861
|
}
|
|
20862
|
+
function toBaseId(widgetId) {
|
|
20863
|
+
return parseFractionKey(widgetId)?.widgetId ?? widgetId;
|
|
20864
|
+
}
|
|
20865
|
+
function buildInitialValuesSignature(json) {
|
|
20866
|
+
return JSON.stringify({
|
|
20867
|
+
widgets: json.widgets,
|
|
20868
|
+
answers: json.validation?.answers ?? null
|
|
20869
|
+
});
|
|
20870
|
+
}
|
|
19245
20871
|
function buildRevealedValues(json) {
|
|
19246
20872
|
const revealed = {};
|
|
19247
20873
|
const validation = json.validation;
|
|
@@ -19289,9 +20915,12 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19289
20915
|
soundEnabled = true,
|
|
19290
20916
|
sounds
|
|
19291
20917
|
}, ref) => {
|
|
20918
|
+
const initialValuesSignature = useMemo(() => buildInitialValuesSignature(json), [json]);
|
|
19292
20919
|
const [values, setValues] = useState(() => buildInitialValues(json));
|
|
19293
20920
|
const valuesRef = useRef(values);
|
|
19294
20921
|
valuesRef.current = values;
|
|
20922
|
+
const touchedRef = useRef(/* @__PURE__ */ new Set());
|
|
20923
|
+
const [loadedValuesSignature, setLoadedValuesSignature] = useState(initialValuesSignature);
|
|
19295
20924
|
const [droppedSources, setDroppedSources] = useState(/* @__PURE__ */ new Map());
|
|
19296
20925
|
const [focusedInputId, setFocusedInputId] = useState(null);
|
|
19297
20926
|
const [validationDetails, setValidationDetails] = useState(null);
|
|
@@ -19300,6 +20929,16 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19300
20929
|
const [revealed, setRevealed] = useState(false);
|
|
19301
20930
|
const playSound = useSoundPlayer(soundEnabled, sounds);
|
|
19302
20931
|
const externallyLocked = useExerciseLocked();
|
|
20932
|
+
useLayoutEffect(() => {
|
|
20933
|
+
if (loadedValuesSignature === initialValuesSignature) return;
|
|
20934
|
+
setValues(buildInitialValues(json));
|
|
20935
|
+
setDroppedSources(/* @__PURE__ */ new Map());
|
|
20936
|
+
setFocusedInputId(null);
|
|
20937
|
+
setValidationDetails(null);
|
|
20938
|
+
setResetKey((k) => k + 1);
|
|
20939
|
+
setLoadedValuesSignature(initialValuesSignature);
|
|
20940
|
+
touchedRef.current = /* @__PURE__ */ new Set();
|
|
20941
|
+
}, [json, initialValuesSignature, loadedValuesSignature]);
|
|
19303
20942
|
const locked = validationDetails !== null || externallyLocked;
|
|
19304
20943
|
const lastResultRef = useRef(null);
|
|
19305
20944
|
const runSubmit = () => {
|
|
@@ -19337,8 +20976,11 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19337
20976
|
getValues: () => values,
|
|
19338
20977
|
getCompletion: () => {
|
|
19339
20978
|
const answerable = getAnswerableInputs(json);
|
|
19340
|
-
const
|
|
19341
|
-
const
|
|
20979
|
+
const missingSet = new Set(getMissingInputs(json, valuesRef.current));
|
|
20980
|
+
for (const id of answerable) {
|
|
20981
|
+
if (!touchedRef.current.has(id)) missingSet.add(id);
|
|
20982
|
+
}
|
|
20983
|
+
const missing = [...missingSet];
|
|
19342
20984
|
const filledIds = answerable.filter((id) => !missingSet.has(id));
|
|
19343
20985
|
return {
|
|
19344
20986
|
total: answerable.length,
|
|
@@ -19358,6 +21000,8 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19358
21000
|
setValidationDetails(null);
|
|
19359
21001
|
setResetKey((k) => k + 1);
|
|
19360
21002
|
setRevealed(false);
|
|
21003
|
+
setLoadedValuesSignature(initialValuesSignature);
|
|
21004
|
+
touchedRef.current = /* @__PURE__ */ new Set();
|
|
19361
21005
|
},
|
|
19362
21006
|
revealAnswers: () => {
|
|
19363
21007
|
const revealedValues = buildRevealedValues(json);
|
|
@@ -19366,6 +21010,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19366
21010
|
const details = {};
|
|
19367
21011
|
for (const [widgetId, userValue] of Object.entries(revealedValues)) {
|
|
19368
21012
|
details[widgetId] = { widgetId, userValue, isCorrect: true };
|
|
21013
|
+
touchedRef.current.add(toBaseId(widgetId));
|
|
19369
21014
|
}
|
|
19370
21015
|
const next = { ...valuesRef.current, ...revealedValues };
|
|
19371
21016
|
valuesRef.current = next;
|
|
@@ -19385,10 +21030,13 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19385
21030
|
const target = candidates.find(canTakeInput) ?? (locked ? void 0 : candidates[0]);
|
|
19386
21031
|
target?.focus();
|
|
19387
21032
|
}
|
|
19388
|
-
}), [json, values, onSubmit, playSound, locked]);
|
|
19389
|
-
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId) => {
|
|
21033
|
+
}), [json, values, onSubmit, playSound, locked, initialValuesSignature]);
|
|
21034
|
+
const handleChange = (widgetId, value, fromDrop = false, sourceOptionId, silent = false) => {
|
|
19390
21035
|
if (locked) return;
|
|
19391
|
-
|
|
21036
|
+
if (!silent) {
|
|
21037
|
+
setRevealed(false);
|
|
21038
|
+
touchedRef.current.add(toBaseId(widgetId));
|
|
21039
|
+
}
|
|
19392
21040
|
const next = { ...valuesRef.current, [widgetId]: value };
|
|
19393
21041
|
valuesRef.current = next;
|
|
19394
21042
|
setValues(next);
|
|
@@ -19475,6 +21123,22 @@ const ExerciseRenderer = forwardRef(({
|
|
|
19475
21123
|
});
|
|
19476
21124
|
continue;
|
|
19477
21125
|
}
|
|
21126
|
+
if (item.type === JSON_TYPES.CHART) {
|
|
21127
|
+
if (currentGroup.length > 0) {
|
|
21128
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
21129
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
21130
|
+
const subGroups = groupLineItems(currentGroup);
|
|
21131
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
21132
|
+
currentGroup = [];
|
|
21133
|
+
}
|
|
21134
|
+
groups.push({
|
|
21135
|
+
items: [item],
|
|
21136
|
+
hasArabic: false,
|
|
21137
|
+
hasRTLInlineWidget: false,
|
|
21138
|
+
subGroups: []
|
|
21139
|
+
});
|
|
21140
|
+
continue;
|
|
21141
|
+
}
|
|
19478
21142
|
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
19479
21143
|
if (currentGroup.length > 0) {
|
|
19480
21144
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
@@ -19658,15 +21322,30 @@ export {
|
|
|
19658
21322
|
AST_TYPES,
|
|
19659
21323
|
BoldTextInlineSchema,
|
|
19660
21324
|
BoldTextSchema,
|
|
21325
|
+
CHART_AXIS_DEFAULTS,
|
|
21326
|
+
CHART_MAX_AXIS_TICKS,
|
|
21327
|
+
CHART_MAX_PICTO_SLOTS,
|
|
21328
|
+
CHART_TYPES,
|
|
19661
21329
|
CLOCK_FORMATS,
|
|
19662
21330
|
CLOCK_MODES,
|
|
19663
21331
|
CLOCK_TIME_REGEX,
|
|
19664
21332
|
CLOCK_TYPES,
|
|
21333
|
+
ChartAxisJSONSchema,
|
|
21334
|
+
ChartAxisSchema,
|
|
21335
|
+
ChartBarJSONSchema,
|
|
21336
|
+
ChartBarSchema,
|
|
21337
|
+
ChartBarWidgetJSONSchema,
|
|
21338
|
+
ChartBlockSchema,
|
|
21339
|
+
ChartItemSchema,
|
|
21340
|
+
ChartPictoJSONSchema,
|
|
21341
|
+
ChartPictoSchema,
|
|
21342
|
+
ChartPictoWidgetJSONSchema,
|
|
19665
21343
|
ClockInlineSchema,
|
|
19666
21344
|
ClockItemSchema,
|
|
19667
21345
|
ColorTextInlineSchema,
|
|
19668
21346
|
ColorTextSchema,
|
|
19669
21347
|
ContentBlockSchema,
|
|
21348
|
+
DEFAULT_TABLE_BORDER_COLOR,
|
|
19670
21349
|
DisplayInlineNodeSchema,
|
|
19671
21350
|
DragOptionsJSONSchema,
|
|
19672
21351
|
DragOptionsToolSchema,
|
|
@@ -19778,6 +21457,10 @@ export {
|
|
|
19778
21457
|
computeGridSymmetryCellsToFill,
|
|
19779
21458
|
computeGridSymmetryExpectedCells,
|
|
19780
21459
|
createBoldText,
|
|
21460
|
+
createChartAxis,
|
|
21461
|
+
createChartBar,
|
|
21462
|
+
createChartBlock,
|
|
21463
|
+
createChartPicto,
|
|
19781
21464
|
createColorText,
|
|
19782
21465
|
createDisplayClock,
|
|
19783
21466
|
createDragOptions,
|
|
@@ -19825,6 +21508,13 @@ export {
|
|
|
19825
21508
|
getMissingInputs,
|
|
19826
21509
|
isBoldText,
|
|
19827
21510
|
isCellInInteractiveArea,
|
|
21511
|
+
isChartAxis,
|
|
21512
|
+
isChartBar,
|
|
21513
|
+
isChartBarWidgetJSON,
|
|
21514
|
+
isChartBlock,
|
|
21515
|
+
isChartItem,
|
|
21516
|
+
isChartPicto,
|
|
21517
|
+
isChartPictoWidgetJSON,
|
|
19828
21518
|
isClock,
|
|
19829
21519
|
isClockItem,
|
|
19830
21520
|
isColorText,
|
|
@@ -19892,6 +21582,7 @@ export {
|
|
|
19892
21582
|
parseSafe,
|
|
19893
21583
|
reflectGridSymmetryCell,
|
|
19894
21584
|
renderExercise,
|
|
21585
|
+
resolveChartAxes,
|
|
19895
21586
|
toAST,
|
|
19896
21587
|
toJSON,
|
|
19897
21588
|
tokenize,
|