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