@mathscan/math-exercise-engine 1.1.16 → 1.1.17
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 +596 -194
- package/dist/index.js +494 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +494 -11
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +95 -0
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createToken, Lexer, CstParser } from "chevrotain";
|
|
2
1
|
import { z } from "zod";
|
|
2
|
+
import { createToken, Lexer, CstParser } from "chevrotain";
|
|
3
3
|
import { create, all } from "mathjs";
|
|
4
4
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
5
5
|
import React, { useLayoutEffect, useEffect, useRef, useMemo, useCallback, useState, createContext, memo, useReducer, useContext, forwardRef, cloneElement, useId, useImperativeHandle } from "react";
|
|
@@ -54,6 +54,8 @@ const AST_TYPES = {
|
|
|
54
54
|
VOP_CARRY_ROW: "VopCarryRow",
|
|
55
55
|
/** Number line layout block */
|
|
56
56
|
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
57
|
+
/** Symmetry exercise on a square grid */
|
|
58
|
+
GRID_SYMMETRY_BLOCK: "GridSymmetryBlock",
|
|
57
59
|
/** A position marker on the number line */
|
|
58
60
|
NUMBER_LINE_POSITION: "NumberLinePosition",
|
|
59
61
|
/** A draggable cursor on the number line */
|
|
@@ -185,6 +187,8 @@ const JSON_TYPES = {
|
|
|
185
187
|
FRACTION: "fraction",
|
|
186
188
|
/** Visual fraction widget */
|
|
187
189
|
VISUAL_FRACTION: "visual-fraction",
|
|
190
|
+
/** Interactive symmetry exercise on a grid */
|
|
191
|
+
GRID_SYMMETRY: "grid-symmetry",
|
|
188
192
|
/** Ruler geometry tool widget */
|
|
189
193
|
RULER: "ruler",
|
|
190
194
|
/** Protractor geometry tool widget */
|
|
@@ -199,9 +203,11 @@ const SET_SQUARE_WIDGET_ID = "_set_square";
|
|
|
199
203
|
const STATEFUL_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
200
204
|
JSON_TYPES.MATCHER,
|
|
201
205
|
JSON_TYPES.PAIR_MATCHER,
|
|
202
|
-
JSON_TYPES.VISUAL_FRACTION
|
|
206
|
+
JSON_TYPES.VISUAL_FRACTION,
|
|
207
|
+
JSON_TYPES.GRID_SYMMETRY
|
|
203
208
|
]);
|
|
204
209
|
const INLINE_DRAG_OPT_PREFIX = "_inline_drag_opt_";
|
|
210
|
+
const GRID_SYMMETRY_WIDGET_PREFIX = "_grid_symmetry_";
|
|
205
211
|
const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
206
212
|
JSON_TYPES.INLINE_INPUT,
|
|
207
213
|
JSON_TYPES.INLINE_DRAG_OPTION,
|
|
@@ -244,6 +250,11 @@ function parseFractionKey(key2) {
|
|
|
244
250
|
if (!match) return null;
|
|
245
251
|
return { widgetId: match[1], part: match[2] };
|
|
246
252
|
}
|
|
253
|
+
const GridCellSchema = z.tuple([
|
|
254
|
+
z.number().int().positive(),
|
|
255
|
+
z.number().int().positive()
|
|
256
|
+
]);
|
|
257
|
+
const gridCellKey = ([row, col]) => `${row},${col}`;
|
|
247
258
|
const Newline = createToken({
|
|
248
259
|
name: "Newline",
|
|
249
260
|
pattern: /\r?\n/,
|
|
@@ -325,6 +336,10 @@ const NUMBER_LINE = createToken({
|
|
|
325
336
|
name: "NUMBER_LINE",
|
|
326
337
|
pattern: /NUMBER_LINE/
|
|
327
338
|
});
|
|
339
|
+
const GRID_SYMMETRY = createToken({
|
|
340
|
+
name: "GRID_SYMMETRY",
|
|
341
|
+
pattern: /GRID_SYMMETRY/
|
|
342
|
+
});
|
|
328
343
|
const POSITION = createToken({
|
|
329
344
|
name: "POSITION",
|
|
330
345
|
pattern: /POSITION/
|
|
@@ -533,6 +548,7 @@ const allTokens = [
|
|
|
533
548
|
W_N_I,
|
|
534
549
|
NUMERIC_INPUT,
|
|
535
550
|
NUMBER_LINE,
|
|
551
|
+
GRID_SYMMETRY,
|
|
536
552
|
POSITION,
|
|
537
553
|
CURSOR,
|
|
538
554
|
ARROW,
|
|
@@ -708,6 +724,10 @@ class MathExerciseParser extends CstParser {
|
|
|
708
724
|
GATE: this.BACKTRACK(this.numberLine),
|
|
709
725
|
ALT: () => this.SUBRULE(this.numberLine)
|
|
710
726
|
},
|
|
727
|
+
{
|
|
728
|
+
GATE: this.BACKTRACK(this.gridSymmetry),
|
|
729
|
+
ALT: () => this.SUBRULE(this.gridSymmetry)
|
|
730
|
+
},
|
|
711
731
|
{
|
|
712
732
|
GATE: this.BACKTRACK(this.pairMatcher),
|
|
713
733
|
ALT: () => this.SUBRULE(this.pairMatcher)
|
|
@@ -723,6 +743,18 @@ class MathExerciseParser extends CstParser {
|
|
|
723
743
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
724
744
|
]);
|
|
725
745
|
});
|
|
746
|
+
this.gridSymmetry = this.RULE("gridSymmetry", () => {
|
|
747
|
+
this.MANY(() => {
|
|
748
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
749
|
+
});
|
|
750
|
+
this.CONSUME(GRID_SYMMETRY);
|
|
751
|
+
this.CONSUME(LBracket);
|
|
752
|
+
this.SUBRULE(this.parameterList);
|
|
753
|
+
this.CONSUME(RBracket);
|
|
754
|
+
this.MANY2(() => {
|
|
755
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
726
758
|
this.table = this.RULE("table", () => {
|
|
727
759
|
this.MANY(() => {
|
|
728
760
|
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
@@ -1436,27 +1468,51 @@ class MathExerciseParser extends CstParser {
|
|
|
1436
1468
|
});
|
|
1437
1469
|
this.parameterList = this.RULE("parameterList", () => {
|
|
1438
1470
|
this.MANY(() => {
|
|
1439
|
-
this.
|
|
1471
|
+
this.OR([
|
|
1472
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
1473
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
1474
|
+
]);
|
|
1440
1475
|
});
|
|
1441
1476
|
this.SUBRULE(this.parameter);
|
|
1442
1477
|
this.MANY2(() => {
|
|
1443
1478
|
this.MANY3(() => {
|
|
1444
|
-
this.
|
|
1479
|
+
this.OR2([
|
|
1480
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
1481
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1482
|
+
]);
|
|
1445
1483
|
});
|
|
1446
1484
|
this.CONSUME(Comma);
|
|
1447
1485
|
this.MANY4(() => {
|
|
1448
|
-
this.
|
|
1486
|
+
this.OR3([
|
|
1487
|
+
{ ALT: () => this.CONSUME3(Space) },
|
|
1488
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1489
|
+
]);
|
|
1449
1490
|
});
|
|
1450
1491
|
this.SUBRULE2(this.parameter);
|
|
1451
1492
|
});
|
|
1452
1493
|
this.MANY5(() => {
|
|
1453
|
-
this.
|
|
1494
|
+
this.OR4([
|
|
1495
|
+
{ ALT: () => this.CONSUME4(Space) },
|
|
1496
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
1497
|
+
]);
|
|
1454
1498
|
});
|
|
1455
1499
|
});
|
|
1456
1500
|
this.parameter = this.RULE("parameter", () => {
|
|
1457
1501
|
this.CONSUME(Identifier, { LABEL: "key" });
|
|
1502
|
+
this.MANY(() => {
|
|
1503
|
+
this.OR([
|
|
1504
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
1505
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
1506
|
+
]);
|
|
1507
|
+
});
|
|
1458
1508
|
this.CONSUME(Equals);
|
|
1459
|
-
this.
|
|
1509
|
+
this.MANY2(() => {
|
|
1510
|
+
this.OR2([
|
|
1511
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
1512
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1513
|
+
]);
|
|
1514
|
+
});
|
|
1515
|
+
this.OR3([
|
|
1460
1516
|
// String value
|
|
1461
1517
|
{ ALT: () => this.CONSUME(StringLiteral, { LABEL: "value" }) },
|
|
1462
1518
|
// Number value
|
|
@@ -1465,6 +1521,11 @@ class MathExerciseParser extends CstParser {
|
|
|
1465
1521
|
{ ALT: () => this.CONSUME2(Identifier, { LABEL: "value" }) },
|
|
1466
1522
|
// Tuple value: (start, end)
|
|
1467
1523
|
{ ALT: () => this.SUBRULE(this.tuple, { LABEL: "value" }) },
|
|
1524
|
+
// Number-pair list enclosed in braces: {[1, 2], [3, 4]}
|
|
1525
|
+
{
|
|
1526
|
+
GATE: () => this.isBracedNumberPairListAhead(),
|
|
1527
|
+
ALT: () => this.SUBRULE(this.bracedNumberPairList, { LABEL: "value" })
|
|
1528
|
+
},
|
|
1468
1529
|
// Curly-brace string list: {"a", "b", "c"}
|
|
1469
1530
|
{ ALT: () => this.SUBRULE(this.curlyStringList, { LABEL: "value" }) },
|
|
1470
1531
|
// Square-bracket number array: [1, 2, 3] — used by VISUAL_FRACTION filled/answer
|
|
@@ -1520,8 +1581,52 @@ class MathExerciseParser extends CstParser {
|
|
|
1520
1581
|
this.SUBRULE(this.stringList);
|
|
1521
1582
|
this.CONSUME(RBrace);
|
|
1522
1583
|
});
|
|
1584
|
+
this.bracedNumberPairList = this.RULE("bracedNumberPairList", () => {
|
|
1585
|
+
this.CONSUME(LBrace);
|
|
1586
|
+
this.MANY(() => {
|
|
1587
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1588
|
+
});
|
|
1589
|
+
this.OPTION(() => {
|
|
1590
|
+
this.SUBRULE(this.numberPair);
|
|
1591
|
+
this.MANY2(() => {
|
|
1592
|
+
this.MANY3(() => {
|
|
1593
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1594
|
+
});
|
|
1595
|
+
this.CONSUME(Comma);
|
|
1596
|
+
this.MANY4(() => {
|
|
1597
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1598
|
+
});
|
|
1599
|
+
this.SUBRULE2(this.numberPair);
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
this.MANY5(() => {
|
|
1603
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1604
|
+
});
|
|
1605
|
+
this.CONSUME(RBrace);
|
|
1606
|
+
});
|
|
1607
|
+
this.numberPair = this.RULE("numberPair", () => {
|
|
1608
|
+
this.CONSUME(LBracket);
|
|
1609
|
+
this.MANY(() => this.CONSUME(Space));
|
|
1610
|
+
this.CONSUME(NumberLiteral, { LABEL: "row" });
|
|
1611
|
+
this.MANY2(() => this.CONSUME2(Space));
|
|
1612
|
+
this.CONSUME(Comma);
|
|
1613
|
+
this.MANY3(() => this.CONSUME3(Space));
|
|
1614
|
+
this.CONSUME2(NumberLiteral, { LABEL: "col" });
|
|
1615
|
+
this.MANY4(() => this.CONSUME4(Space));
|
|
1616
|
+
this.CONSUME(RBracket);
|
|
1617
|
+
});
|
|
1523
1618
|
this.performSelfAnalysis();
|
|
1524
1619
|
}
|
|
1620
|
+
/** Distinguish `{[number, number], ...}` and the valid empty form `{}` from string lists. */
|
|
1621
|
+
isBracedNumberPairListAhead() {
|
|
1622
|
+
if (this.LA(1).tokenType !== LBrace) return false;
|
|
1623
|
+
let offset = 2;
|
|
1624
|
+
while (this.LA(offset).tokenType === Space || this.LA(offset).tokenType === Newline) {
|
|
1625
|
+
offset++;
|
|
1626
|
+
}
|
|
1627
|
+
const firstContentToken = this.LA(offset).tokenType;
|
|
1628
|
+
return firstContentToken === LBracket || firstContentToken === RBrace;
|
|
1629
|
+
}
|
|
1525
1630
|
}
|
|
1526
1631
|
const parser = new MathExerciseParser();
|
|
1527
1632
|
function parseOrThrow(schema, data, label) {
|
|
@@ -2066,6 +2171,135 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style, lab
|
|
|
2066
2171
|
positions
|
|
2067
2172
|
});
|
|
2068
2173
|
}
|
|
2174
|
+
const GRID_SYMMETRY_AXES = ["vertical", "horizontal"];
|
|
2175
|
+
const GRID_SYMMETRY_INTERACTIVE_AREAS = ["left", "right", "top", "bottom"];
|
|
2176
|
+
const GRID_SYMMETRY_MAX_ROWS = 20;
|
|
2177
|
+
const GRID_SYMMETRY_MAX_COLS = 20;
|
|
2178
|
+
const GRID_SYMMETRY_MIN_CELL_SIZE = 0.25;
|
|
2179
|
+
const GRID_SYMMETRY_MAX_CELL_SIZE = 2;
|
|
2180
|
+
function isCellInInteractiveArea(cell, rows, cols, area) {
|
|
2181
|
+
const [row, col] = cell;
|
|
2182
|
+
switch (area) {
|
|
2183
|
+
case "left":
|
|
2184
|
+
return col <= Math.floor(cols / 2);
|
|
2185
|
+
case "right":
|
|
2186
|
+
return col > Math.ceil(cols / 2);
|
|
2187
|
+
case "top":
|
|
2188
|
+
return row <= Math.floor(rows / 2);
|
|
2189
|
+
case "bottom":
|
|
2190
|
+
return row > Math.ceil(rows / 2);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
function reflectGridSymmetryCell(cell, rows, cols, axis) {
|
|
2194
|
+
const [row, col] = cell;
|
|
2195
|
+
return axis === "vertical" ? [row, cols + 1 - col] : [rows + 1 - row, col];
|
|
2196
|
+
}
|
|
2197
|
+
function computeGridSymmetryExpectedCells(data) {
|
|
2198
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2199
|
+
const expected = [];
|
|
2200
|
+
for (const cell of data.cells) {
|
|
2201
|
+
if (isCellInInteractiveArea(cell, data.rows, data.cols, data.interactiveArea)) continue;
|
|
2202
|
+
const reflected = reflectGridSymmetryCell(cell, data.rows, data.cols, data.axis);
|
|
2203
|
+
if (!isCellInInteractiveArea(reflected, data.rows, data.cols, data.interactiveArea)) continue;
|
|
2204
|
+
const key2 = gridCellKey(reflected);
|
|
2205
|
+
if (!seen.has(key2)) {
|
|
2206
|
+
seen.add(key2);
|
|
2207
|
+
expected.push(reflected);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return expected.sort(([ar, ac], [br, bc]) => ar - br || ac - bc);
|
|
2211
|
+
}
|
|
2212
|
+
function computeGridSymmetryCellsToFill(data) {
|
|
2213
|
+
const initial = new Set(data.cells.map(gridCellKey));
|
|
2214
|
+
return computeGridSymmetryExpectedCells(data).filter((cell) => !initial.has(gridCellKey(cell)));
|
|
2215
|
+
}
|
|
2216
|
+
const GridSymmetryBlockSchema = z.object({
|
|
2217
|
+
type: z.literal(AST_TYPES.GRID_SYMMETRY_BLOCK),
|
|
2218
|
+
rows: z.number().int("The number of rows must be a whole number.").positive("The number of rows must be greater than 0.").max(GRID_SYMMETRY_MAX_ROWS, {
|
|
2219
|
+
message: `The number of rows cannot exceed ${GRID_SYMMETRY_MAX_ROWS}.`
|
|
2220
|
+
}),
|
|
2221
|
+
cols: z.number().int("The number of columns must be a whole number.").positive("The number of columns must be greater than 0.").max(GRID_SYMMETRY_MAX_COLS, {
|
|
2222
|
+
message: `The number of columns cannot exceed ${GRID_SYMMETRY_MAX_COLS}.`
|
|
2223
|
+
}),
|
|
2224
|
+
axis: z.enum(GRID_SYMMETRY_AXES),
|
|
2225
|
+
interactiveArea: z.enum(GRID_SYMMETRY_INTERACTIVE_AREAS),
|
|
2226
|
+
cellSize: z.number().finite().min(
|
|
2227
|
+
GRID_SYMMETRY_MIN_CELL_SIZE,
|
|
2228
|
+
`Cell size must be between ${GRID_SYMMETRY_MIN_CELL_SIZE} and ${GRID_SYMMETRY_MAX_CELL_SIZE}.`
|
|
2229
|
+
).max(
|
|
2230
|
+
GRID_SYMMETRY_MAX_CELL_SIZE,
|
|
2231
|
+
`Cell size must be between ${GRID_SYMMETRY_MIN_CELL_SIZE} and ${GRID_SYMMETRY_MAX_CELL_SIZE}.`
|
|
2232
|
+
).default(1),
|
|
2233
|
+
cells: z.array(GridCellSchema)
|
|
2234
|
+
}).superRefine((data, ctx) => {
|
|
2235
|
+
const compatible = data.axis === "vertical" ? data.interactiveArea === "left" || data.interactiveArea === "right" : data.interactiveArea === "top" || data.interactiveArea === "bottom";
|
|
2236
|
+
if (!compatible) {
|
|
2237
|
+
ctx.addIssue({
|
|
2238
|
+
code: z.ZodIssueCode.custom,
|
|
2239
|
+
path: ["interactiveArea"],
|
|
2240
|
+
message: data.axis === "vertical" ? 'For a vertical axis, the interactive area must be "left" or "right".' : 'For a horizontal axis, the interactive area must be "top" or "bottom".'
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
if (data.axis === "vertical" && data.cols % 2 !== 0) {
|
|
2244
|
+
ctx.addIssue({
|
|
2245
|
+
code: z.ZodIssueCode.custom,
|
|
2246
|
+
path: ["cols"],
|
|
2247
|
+
message: `For a vertical axis, the number of columns must be even. Received: ${data.cols}.`
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
if (data.axis === "horizontal" && data.rows % 2 !== 0) {
|
|
2251
|
+
ctx.addIssue({
|
|
2252
|
+
code: z.ZodIssueCode.custom,
|
|
2253
|
+
path: ["rows"],
|
|
2254
|
+
message: `For a horizontal axis, the number of rows must be even. Received: ${data.rows}.`
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2258
|
+
data.cells.forEach((cell, index) => {
|
|
2259
|
+
const [row, col] = cell;
|
|
2260
|
+
if (row > data.rows || col > data.cols) {
|
|
2261
|
+
ctx.addIssue({
|
|
2262
|
+
code: z.ZodIssueCode.custom,
|
|
2263
|
+
path: ["cells", index],
|
|
2264
|
+
message: `Cell [${row},${col}] is outside the ${data.rows} × ${data.cols} grid.`
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
const key2 = gridCellKey(cell);
|
|
2268
|
+
if (seen.has(key2)) {
|
|
2269
|
+
ctx.addIssue({
|
|
2270
|
+
code: z.ZodIssueCode.custom,
|
|
2271
|
+
path: ["cells", index],
|
|
2272
|
+
message: `Cell [${row},${col}] is listed more than once.`
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
seen.add(key2);
|
|
2276
|
+
});
|
|
2277
|
+
if (!compatible) return;
|
|
2278
|
+
const expected = new Set(computeGridSymmetryExpectedCells(data).map(gridCellKey));
|
|
2279
|
+
data.cells.forEach((cell, index) => {
|
|
2280
|
+
if (isCellInInteractiveArea(cell, data.rows, data.cols, data.interactiveArea) && !expected.has(gridCellKey(cell))) {
|
|
2281
|
+
ctx.addIssue({
|
|
2282
|
+
code: z.ZodIssueCode.custom,
|
|
2283
|
+
path: ["cells", index],
|
|
2284
|
+
message: `Prefilled cell [${cell[0]},${cell[1]}] does not match the expected symmetry.`
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
});
|
|
2289
|
+
function createGridSymmetryBlock(rows, cols, axis, interactiveArea, cells, cellSize = 1) {
|
|
2290
|
+
return GridSymmetryBlockSchema.parse({
|
|
2291
|
+
type: AST_TYPES.GRID_SYMMETRY_BLOCK,
|
|
2292
|
+
rows,
|
|
2293
|
+
cols,
|
|
2294
|
+
axis,
|
|
2295
|
+
interactiveArea,
|
|
2296
|
+
cellSize,
|
|
2297
|
+
cells
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
function isGridSymmetryBlock(value) {
|
|
2301
|
+
return GridSymmetryBlockSchema.safeParse(value).success;
|
|
2302
|
+
}
|
|
2069
2303
|
const TableCellSchema = z.object({
|
|
2070
2304
|
type: z.literal(AST_TYPES.TABLE_CELL),
|
|
2071
2305
|
colspan: z.number().int().positive().optional(),
|
|
@@ -2565,7 +2799,7 @@ function isMixedValidation(value) {
|
|
|
2565
2799
|
function isValidationNode(value) {
|
|
2566
2800
|
return ValidationNodeSchema.safeParse(value).success;
|
|
2567
2801
|
}
|
|
2568
|
-
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema]);
|
|
2802
|
+
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema, GridSymmetryBlockSchema]);
|
|
2569
2803
|
const ExerciseDocumentSchema = z.object({
|
|
2570
2804
|
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
2571
2805
|
question: TextBlockSchema.optional(),
|
|
@@ -2667,12 +2901,24 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2667
2901
|
if (ctx.table) return this.visit(ctx.table);
|
|
2668
2902
|
if (ctx.radioButtons) return this.visit(ctx.radioButtons);
|
|
2669
2903
|
if (ctx.numberLine) return this.visit(ctx.numberLine);
|
|
2904
|
+
if (ctx.gridSymmetry) return this.visit(ctx.gridSymmetry);
|
|
2670
2905
|
if (ctx.pairMatcher) return this.visit(ctx.pairMatcher);
|
|
2671
2906
|
if (ctx.matcher) return this.visit(ctx.matcher);
|
|
2672
2907
|
if (ctx.verticalOperation) return this.visit(ctx.verticalOperation);
|
|
2673
2908
|
if (ctx.line) return this.visit(ctx.line);
|
|
2674
2909
|
throw new Error("Unknown content block");
|
|
2675
2910
|
}
|
|
2911
|
+
gridSymmetry(ctx) {
|
|
2912
|
+
const params = this.visit(ctx.parameterList);
|
|
2913
|
+
return createGridSymmetryBlock(
|
|
2914
|
+
Number(params.rows),
|
|
2915
|
+
Number(params.cols),
|
|
2916
|
+
params.axis,
|
|
2917
|
+
params.interactiveArea,
|
|
2918
|
+
params.cells,
|
|
2919
|
+
params.cellSize === void 0 ? 1 : Number(params.cellSize)
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2676
2922
|
radioButtons(ctx) {
|
|
2677
2923
|
const params = this.visit(ctx.parameterList);
|
|
2678
2924
|
const name = params.name;
|
|
@@ -3261,6 +3507,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3261
3507
|
curlyStringList(ctx) {
|
|
3262
3508
|
return this.visit(ctx.stringList);
|
|
3263
3509
|
}
|
|
3510
|
+
bracedNumberPairList(ctx) {
|
|
3511
|
+
return ctx.numberPair ? ctx.numberPair.map((pair) => this.visit(pair)) : [];
|
|
3512
|
+
}
|
|
3513
|
+
numberPair(ctx) {
|
|
3514
|
+
return [
|
|
3515
|
+
parseFloat(ctx.row[0].image),
|
|
3516
|
+
parseFloat(ctx.col[0].image)
|
|
3517
|
+
];
|
|
3518
|
+
}
|
|
3264
3519
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3265
3520
|
// Helpers
|
|
3266
3521
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -3637,6 +3892,34 @@ const VisualFractionJSONSchema = z.object({
|
|
|
3637
3892
|
*/
|
|
3638
3893
|
size: z.number().positive().optional()
|
|
3639
3894
|
}).superRefine(validateVisualFractionCapacity);
|
|
3895
|
+
const GridSymmetryJSONSchema = z.object({
|
|
3896
|
+
type: z.literal(JSON_TYPES.GRID_SYMMETRY),
|
|
3897
|
+
rows: z.number().int().positive(),
|
|
3898
|
+
cols: z.number().int().positive(),
|
|
3899
|
+
axis: z.enum(["vertical", "horizontal"]),
|
|
3900
|
+
interactiveArea: z.enum(["left", "right", "top", "bottom"]),
|
|
3901
|
+
cellSize: z.number().finite().positive().default(1),
|
|
3902
|
+
cells: z.array(GridCellSchema)
|
|
3903
|
+
}).superRefine((data, ctx) => {
|
|
3904
|
+
const result = GridSymmetryBlockSchema.safeParse({
|
|
3905
|
+
type: AST_TYPES.GRID_SYMMETRY_BLOCK,
|
|
3906
|
+
rows: data.rows,
|
|
3907
|
+
cols: data.cols,
|
|
3908
|
+
axis: data.axis,
|
|
3909
|
+
interactiveArea: data.interactiveArea,
|
|
3910
|
+
cellSize: data.cellSize,
|
|
3911
|
+
cells: data.cells
|
|
3912
|
+
});
|
|
3913
|
+
if (!result.success) {
|
|
3914
|
+
for (const issue of result.error.issues) {
|
|
3915
|
+
ctx.addIssue({
|
|
3916
|
+
code: z.ZodIssueCode.custom,
|
|
3917
|
+
path: issue.path,
|
|
3918
|
+
message: issue.message
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3640
3923
|
const RulerWidgetSchema = z.object({
|
|
3641
3924
|
type: z.literal(JSON_TYPES.RULER),
|
|
3642
3925
|
size: z.number().optional()
|
|
@@ -3659,6 +3942,7 @@ const WidgetJSONSchema = z.union([
|
|
|
3659
3942
|
MatcherWidgetJSONSchema,
|
|
3660
3943
|
PairMatcherWidgetJSONSchema,
|
|
3661
3944
|
VisualFractionJSONSchema,
|
|
3945
|
+
GridSymmetryJSONSchema,
|
|
3662
3946
|
RulerWidgetSchema,
|
|
3663
3947
|
ProtractorWidgetSchema,
|
|
3664
3948
|
SetSquareWidgetSchema
|
|
@@ -3674,7 +3958,8 @@ const ValidationJSONSchema = z.object({
|
|
|
3674
3958
|
z.string(),
|
|
3675
3959
|
z.number(),
|
|
3676
3960
|
z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
3677
|
-
z.array(z.number().int().nonnegative())
|
|
3961
|
+
z.array(z.number().int().nonnegative()),
|
|
3962
|
+
z.array(GridCellSchema)
|
|
3678
3963
|
])
|
|
3679
3964
|
).optional(),
|
|
3680
3965
|
expression: z.string().optional(),
|
|
@@ -3772,6 +4057,9 @@ function isNumericKeypadJSON(widget) {
|
|
|
3772
4057
|
function isFractionJSON(widget) {
|
|
3773
4058
|
return widget.type === JSON_TYPES.FRACTION;
|
|
3774
4059
|
}
|
|
4060
|
+
function isGridSymmetryJSON(widget) {
|
|
4061
|
+
return widget.type === JSON_TYPES.GRID_SYMMETRY;
|
|
4062
|
+
}
|
|
3775
4063
|
function isRulerWidget(widget) {
|
|
3776
4064
|
return widget.type === JSON_TYPES.RULER;
|
|
3777
4065
|
}
|
|
@@ -9279,6 +9567,7 @@ function toJSON(ast) {
|
|
|
9279
9567
|
const idGenerator = new LayoutIdGenerator();
|
|
9280
9568
|
const widgets = {};
|
|
9281
9569
|
const answers = {};
|
|
9570
|
+
let gridSymmetryCounter = 0;
|
|
9282
9571
|
const allInlines = collectAllInlines(ast.lines);
|
|
9283
9572
|
const inlineDragIds = /* @__PURE__ */ new Map();
|
|
9284
9573
|
{
|
|
@@ -9333,6 +9622,34 @@ function toJSON(ast) {
|
|
|
9333
9622
|
});
|
|
9334
9623
|
} else if (block.type === AST_TYPES.VERTICAL_OPERATION_BLOCK) {
|
|
9335
9624
|
layout.push(transformVerticalOperation(block, idGenerator));
|
|
9625
|
+
} else if (block.type === AST_TYPES.GRID_SYMMETRY_BLOCK) {
|
|
9626
|
+
const grid = block;
|
|
9627
|
+
const widgetId = `${GRID_SYMMETRY_WIDGET_PREFIX}${gridSymmetryCounter++}`;
|
|
9628
|
+
widgets[widgetId] = {
|
|
9629
|
+
type: JSON_TYPES.GRID_SYMMETRY,
|
|
9630
|
+
rows: grid.rows,
|
|
9631
|
+
cols: grid.cols,
|
|
9632
|
+
axis: grid.axis,
|
|
9633
|
+
interactiveArea: grid.interactiveArea,
|
|
9634
|
+
cellSize: grid.cellSize,
|
|
9635
|
+
cells: grid.cells
|
|
9636
|
+
};
|
|
9637
|
+
const prefilled = grid.cells.filter((cell) => isCellInInteractiveArea(
|
|
9638
|
+
cell,
|
|
9639
|
+
grid.rows,
|
|
9640
|
+
grid.cols,
|
|
9641
|
+
grid.interactiveArea
|
|
9642
|
+
));
|
|
9643
|
+
answers[widgetId] = [...prefilled, ...computeGridSymmetryCellsToFill(grid)].sort(([ar, ac], [br, bc]) => ar - br || ac - bc);
|
|
9644
|
+
layout.push({
|
|
9645
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
9646
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
9647
|
+
widgetId
|
|
9648
|
+
});
|
|
9649
|
+
layout.push({
|
|
9650
|
+
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
9651
|
+
type: JSON_TYPES.LINEBREAK
|
|
9652
|
+
});
|
|
9336
9653
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
9337
9654
|
const radioBlock = block;
|
|
9338
9655
|
layout.push({
|
|
@@ -9930,6 +10247,18 @@ function areVisualFractionCountsEqual(expected, result) {
|
|
|
9930
10247
|
if (expected.length !== result.length) return false;
|
|
9931
10248
|
return lodashExports.isEqual(lodashExports.sortBy(expected), lodashExports.sortBy(result));
|
|
9932
10249
|
}
|
|
10250
|
+
function isValidGridSymmetryCell(value, rows, cols) {
|
|
10251
|
+
if (!Array.isArray(value) || value.length !== 2) return false;
|
|
10252
|
+
const [row, col] = value;
|
|
10253
|
+
return Number.isInteger(row) && Number.isInteger(col) && row >= 1 && row <= rows && col >= 1 && col <= cols;
|
|
10254
|
+
}
|
|
10255
|
+
function areGridSymmetryCellsEqual(expected, actual, rows, cols) {
|
|
10256
|
+
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
10257
|
+
if (!actual.every((cell) => isValidGridSymmetryCell(cell, rows, cols))) return false;
|
|
10258
|
+
const actualKeys = new Set(actual.map(gridCellKey));
|
|
10259
|
+
if (actualKeys.size !== actual.length) return false;
|
|
10260
|
+
return expected.every((cell) => actualKeys.has(gridCellKey(cell)));
|
|
10261
|
+
}
|
|
9933
10262
|
const math$1 = create(all);
|
|
9934
10263
|
function collectNumberLines(items) {
|
|
9935
10264
|
const result = [];
|
|
@@ -10003,6 +10332,20 @@ function validateSimple(exercise, userInputs) {
|
|
|
10003
10332
|
else allCorrect = false;
|
|
10004
10333
|
continue;
|
|
10005
10334
|
}
|
|
10335
|
+
if (widget?.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
10336
|
+
const userValue2 = userInputs[widgetId];
|
|
10337
|
+
const isCorrect2 = areGridSymmetryCellsEqual(
|
|
10338
|
+
expected,
|
|
10339
|
+
userValue2,
|
|
10340
|
+
widget.rows,
|
|
10341
|
+
widget.cols
|
|
10342
|
+
);
|
|
10343
|
+
details[widgetId] = { widgetId, userValue: userValue2, isCorrect: isCorrect2 };
|
|
10344
|
+
totalCount++;
|
|
10345
|
+
if (isCorrect2) correctCount++;
|
|
10346
|
+
else allCorrect = false;
|
|
10347
|
+
continue;
|
|
10348
|
+
}
|
|
10006
10349
|
totalCount++;
|
|
10007
10350
|
const userValue = userInputs[widgetId];
|
|
10008
10351
|
const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
|
|
@@ -10433,9 +10776,19 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
10433
10776
|
}
|
|
10434
10777
|
const inputs = preprocessFractionValues(exercise, userInputs);
|
|
10435
10778
|
const isEmpty = (v) => v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
|
|
10779
|
+
const isMissingAnswer = (widgetId) => {
|
|
10780
|
+
const value = inputs[widgetId];
|
|
10781
|
+
const widget = exercise.widgets[widgetId];
|
|
10782
|
+
if (widget?.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
10783
|
+
const expected = exercise.validation?.answers?.[widgetId];
|
|
10784
|
+
const expectedCount = Array.isArray(expected) ? expected.length : 0;
|
|
10785
|
+
return !Array.isArray(value) || value.length < expectedCount;
|
|
10786
|
+
}
|
|
10787
|
+
return !(widgetId in inputs) || isEmpty(value);
|
|
10788
|
+
};
|
|
10436
10789
|
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
10437
10790
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
10438
|
-
if (
|
|
10791
|
+
if (isMissingAnswer(widgetId)) {
|
|
10439
10792
|
missing.push(widgetId);
|
|
10440
10793
|
}
|
|
10441
10794
|
}
|
|
@@ -10451,7 +10804,7 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
10451
10804
|
...exercise.validation.variables ?? []
|
|
10452
10805
|
]);
|
|
10453
10806
|
for (const k of keys) {
|
|
10454
|
-
if (
|
|
10807
|
+
if (isMissingAnswer(k)) missing.push(k);
|
|
10455
10808
|
}
|
|
10456
10809
|
}
|
|
10457
10810
|
return missing;
|
|
@@ -15874,6 +16227,94 @@ function getSizeStyle(size, _shape) {
|
|
|
15874
16227
|
"--visual-fraction-size": `${size * VISUAL_FRACTION_SVG_SIZE_SCALE}rem`
|
|
15875
16228
|
};
|
|
15876
16229
|
}
|
|
16230
|
+
const GridSymmetry = ({
|
|
16231
|
+
widgetId,
|
|
16232
|
+
widget,
|
|
16233
|
+
value,
|
|
16234
|
+
validationStatus = null,
|
|
16235
|
+
onChange
|
|
16236
|
+
}) => {
|
|
16237
|
+
const initialKeys = useMemo(() => new Set(widget.cells.map(gridCellKey)), [widget.cells]);
|
|
16238
|
+
const initialInteractiveCells = useMemo(
|
|
16239
|
+
() => widget.cells.filter(
|
|
16240
|
+
(cell) => isCellInInteractiveArea(cell, widget.rows, widget.cols, widget.interactiveArea)
|
|
16241
|
+
),
|
|
16242
|
+
[widget.cells, widget.rows, widget.cols, widget.interactiveArea]
|
|
16243
|
+
);
|
|
16244
|
+
const selectedCells = value ?? initialInteractiveCells;
|
|
16245
|
+
const selectedKeys = useMemo(() => new Set(selectedCells.map(gridCellKey)), [selectedCells]);
|
|
16246
|
+
const toggleCell = (cell) => {
|
|
16247
|
+
const key2 = gridCellKey(cell);
|
|
16248
|
+
if (initialKeys.has(key2)) return;
|
|
16249
|
+
const next = selectedKeys.has(key2) ? selectedCells.filter((candidate) => gridCellKey(candidate) !== key2) : [...selectedCells, cell];
|
|
16250
|
+
onChange(widgetId, next);
|
|
16251
|
+
};
|
|
16252
|
+
const statusClass = validationStatus ? ` grid-symmetry--${validationStatus}` : "";
|
|
16253
|
+
const cells = [];
|
|
16254
|
+
for (let row = 1; row <= widget.rows; row++) {
|
|
16255
|
+
for (let col = 1; col <= widget.cols; col++) {
|
|
16256
|
+
const cell = [row, col];
|
|
16257
|
+
const key2 = gridCellKey(cell);
|
|
16258
|
+
const interactive = isCellInInteractiveArea(
|
|
16259
|
+
cell,
|
|
16260
|
+
widget.rows,
|
|
16261
|
+
widget.cols,
|
|
16262
|
+
widget.interactiveArea
|
|
16263
|
+
);
|
|
16264
|
+
const initial = initialKeys.has(key2);
|
|
16265
|
+
const selected = selectedKeys.has(key2);
|
|
16266
|
+
const locked = initial || !interactive;
|
|
16267
|
+
const roleClass = initial ? interactive ? " grid-symmetry__cell--prefilled" : " grid-symmetry__cell--reference" : selected ? " grid-symmetry__cell--answer" : "";
|
|
16268
|
+
const className = `grid-symmetry__cell${interactive ? " grid-symmetry__cell--interactive" : ""}${locked ? "" : " grid-symmetry__cell--editable"}${selected || initial ? " grid-symmetry__cell--selected" : ""}${roleClass}`;
|
|
16269
|
+
const label = `Row ${row}, column ${col}`;
|
|
16270
|
+
cells.push(
|
|
16271
|
+
locked ? /* @__PURE__ */ jsx(
|
|
16272
|
+
"span",
|
|
16273
|
+
{
|
|
16274
|
+
className,
|
|
16275
|
+
role: initial ? "img" : void 0,
|
|
16276
|
+
"aria-label": initial ? `${label}, filled` : void 0,
|
|
16277
|
+
"aria-hidden": initial ? void 0 : true
|
|
16278
|
+
},
|
|
16279
|
+
key2
|
|
16280
|
+
) : /* @__PURE__ */ jsx(
|
|
16281
|
+
"button",
|
|
16282
|
+
{
|
|
16283
|
+
type: "button",
|
|
16284
|
+
className,
|
|
16285
|
+
"aria-label": label,
|
|
16286
|
+
"aria-pressed": selected,
|
|
16287
|
+
onClick: () => toggleCell(cell)
|
|
16288
|
+
},
|
|
16289
|
+
key2
|
|
16290
|
+
)
|
|
16291
|
+
);
|
|
16292
|
+
}
|
|
16293
|
+
}
|
|
16294
|
+
return /* @__PURE__ */ jsx("div", { className: `grid-symmetry${statusClass}`, "data-testid": "grid-symmetry", children: /* @__PURE__ */ jsxs(
|
|
16295
|
+
"div",
|
|
16296
|
+
{
|
|
16297
|
+
className: "grid-symmetry__grid",
|
|
16298
|
+
style: {
|
|
16299
|
+
"--grid-symmetry-cell-size": `calc(48px * ${widget.cellSize})`,
|
|
16300
|
+
gridTemplateColumns: `repeat(${widget.cols}, var(--grid-symmetry-cell-size))`,
|
|
16301
|
+
gridTemplateRows: `repeat(${widget.rows}, var(--grid-symmetry-cell-size))`
|
|
16302
|
+
},
|
|
16303
|
+
role: "group",
|
|
16304
|
+
"aria-label": `Symmetry grid, ${widget.rows} rows by ${widget.cols} columns`,
|
|
16305
|
+
children: [
|
|
16306
|
+
cells,
|
|
16307
|
+
/* @__PURE__ */ jsx(
|
|
16308
|
+
"span",
|
|
16309
|
+
{
|
|
16310
|
+
className: `grid-symmetry__axis grid-symmetry__axis--${widget.axis}`,
|
|
16311
|
+
"aria-hidden": "true"
|
|
16312
|
+
}
|
|
16313
|
+
)
|
|
16314
|
+
]
|
|
16315
|
+
}
|
|
16316
|
+
) });
|
|
16317
|
+
};
|
|
15877
16318
|
const RadioOptions = ({
|
|
15878
16319
|
name,
|
|
15879
16320
|
options,
|
|
@@ -16522,6 +16963,9 @@ function isPairMatcherWidget(widget) {
|
|
|
16522
16963
|
function isVisualFractionWidget(widget) {
|
|
16523
16964
|
return widget.type === "visual-fraction";
|
|
16524
16965
|
}
|
|
16966
|
+
function isGridSymmetryWidget(widget) {
|
|
16967
|
+
return widget.type === "grid-symmetry";
|
|
16968
|
+
}
|
|
16525
16969
|
const WidgetRenderer = ({
|
|
16526
16970
|
widget,
|
|
16527
16971
|
widgetId,
|
|
@@ -16666,6 +17110,21 @@ const WidgetRenderer = ({
|
|
|
16666
17110
|
}
|
|
16667
17111
|
);
|
|
16668
17112
|
}
|
|
17113
|
+
if (isGridSymmetryWidget(widget)) {
|
|
17114
|
+
const cells = Array.isArray(value) && value.every(
|
|
17115
|
+
(cell) => Array.isArray(cell) && cell.length === 2 && cell.every(Number.isInteger)
|
|
17116
|
+
) ? value : void 0;
|
|
17117
|
+
return /* @__PURE__ */ jsx(
|
|
17118
|
+
GridSymmetry,
|
|
17119
|
+
{
|
|
17120
|
+
widgetId,
|
|
17121
|
+
widget,
|
|
17122
|
+
value: cells,
|
|
17123
|
+
validationStatus,
|
|
17124
|
+
onChange: (id, nextValue) => onChange(id, nextValue)
|
|
17125
|
+
}
|
|
17126
|
+
);
|
|
17127
|
+
}
|
|
16669
17128
|
return null;
|
|
16670
17129
|
};
|
|
16671
17130
|
const ExpandDiamondIcon = () => {
|
|
@@ -18730,6 +19189,14 @@ function buildInitialValues(json) {
|
|
|
18730
19189
|
const initial = {};
|
|
18731
19190
|
for (const [id, widget] of Object.entries(json.widgets)) {
|
|
18732
19191
|
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) initial[id] = [];
|
|
19192
|
+
if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
19193
|
+
initial[id] = widget.cells.filter((cell) => isCellInInteractiveArea(
|
|
19194
|
+
cell,
|
|
19195
|
+
widget.rows,
|
|
19196
|
+
widget.cols,
|
|
19197
|
+
widget.interactiveArea
|
|
19198
|
+
));
|
|
19199
|
+
}
|
|
18733
19200
|
}
|
|
18734
19201
|
return initial;
|
|
18735
19202
|
}
|
|
@@ -19159,6 +19626,15 @@ export {
|
|
|
19159
19626
|
FractionInlineSchema,
|
|
19160
19627
|
FractionJSONSchema,
|
|
19161
19628
|
FreeTextInlineSchema,
|
|
19629
|
+
GRID_SYMMETRY_AXES,
|
|
19630
|
+
GRID_SYMMETRY_INTERACTIVE_AREAS,
|
|
19631
|
+
GRID_SYMMETRY_MAX_CELL_SIZE,
|
|
19632
|
+
GRID_SYMMETRY_MAX_COLS,
|
|
19633
|
+
GRID_SYMMETRY_MAX_ROWS,
|
|
19634
|
+
GRID_SYMMETRY_MIN_CELL_SIZE,
|
|
19635
|
+
GridCellSchema,
|
|
19636
|
+
GridSymmetryBlockSchema,
|
|
19637
|
+
GridSymmetryJSONSchema,
|
|
19162
19638
|
GroupItemSchema,
|
|
19163
19639
|
HighlightedTextInlineSchema,
|
|
19164
19640
|
HighlightedTextSchema,
|
|
@@ -19245,6 +19721,8 @@ export {
|
|
|
19245
19721
|
WidgetRenderer,
|
|
19246
19722
|
compile,
|
|
19247
19723
|
compileWithAST,
|
|
19724
|
+
computeGridSymmetryCellsToFill,
|
|
19725
|
+
computeGridSymmetryExpectedCells,
|
|
19248
19726
|
createBoldText,
|
|
19249
19727
|
createColorText,
|
|
19250
19728
|
createDisplayClock,
|
|
@@ -19253,6 +19731,7 @@ export {
|
|
|
19253
19731
|
createExpressionValidation,
|
|
19254
19732
|
createFraction,
|
|
19255
19733
|
createFreeText,
|
|
19734
|
+
createGridSymmetryBlock,
|
|
19256
19735
|
createHighlightedText,
|
|
19257
19736
|
createImage,
|
|
19258
19737
|
createInlineDragOption,
|
|
@@ -19291,6 +19770,7 @@ export {
|
|
|
19291
19770
|
getFractionKey,
|
|
19292
19771
|
getMissingInputs,
|
|
19293
19772
|
isBoldText,
|
|
19773
|
+
isCellInInteractiveArea,
|
|
19294
19774
|
isClock,
|
|
19295
19775
|
isClockItem,
|
|
19296
19776
|
isColorText,
|
|
@@ -19302,6 +19782,8 @@ export {
|
|
|
19302
19782
|
isFractionJSON,
|
|
19303
19783
|
isFractionKey,
|
|
19304
19784
|
isFreeText,
|
|
19785
|
+
isGridSymmetryBlock,
|
|
19786
|
+
isGridSymmetryJSON,
|
|
19305
19787
|
isGroupItem,
|
|
19306
19788
|
isHighlightedText,
|
|
19307
19789
|
isImage,
|
|
@@ -19354,6 +19836,7 @@ export {
|
|
|
19354
19836
|
parse,
|
|
19355
19837
|
parseFractionKey,
|
|
19356
19838
|
parseSafe,
|
|
19839
|
+
reflectGridSymmetryCell,
|
|
19357
19840
|
renderExercise,
|
|
19358
19841
|
toAST,
|
|
19359
19842
|
toJSON,
|