@mathscan/math-exercise-engine 1.1.1 → 1.1.3
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 +11684 -3233
- package/dist/index.js +1826 -155
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1826 -155
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +247 -8
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,8 @@ const AST_TYPES = {
|
|
|
21
21
|
BOLD_TEXT: "BoldText",
|
|
22
22
|
/** Inline input widget (string or numeric) */
|
|
23
23
|
INLINE_INPUT: "InlineInput",
|
|
24
|
+
/** Single inline draggable option (appears in-flow with text) */
|
|
25
|
+
INLINE_DRAG_OPTION: "InlineDragOption",
|
|
24
26
|
/** Inline fraction widget (numerator/denominator with optional inputs) */
|
|
25
27
|
FRACTION: "Fraction",
|
|
26
28
|
/** Inline image element */
|
|
@@ -38,6 +40,16 @@ const AST_TYPES = {
|
|
|
38
40
|
TABLE_ROW: "TableRow",
|
|
39
41
|
/** Cell within a table row */
|
|
40
42
|
TABLE_CELL: "TableCell",
|
|
43
|
+
/** Vertical operation (column arithmetic) block */
|
|
44
|
+
VERTICAL_OPERATION_BLOCK: "VerticalOperationBlock",
|
|
45
|
+
/** Row within a vertical operation */
|
|
46
|
+
VOP_ROW: "VopRow",
|
|
47
|
+
/** Cell within a vertical-operation row */
|
|
48
|
+
VOP_CELL: "VopCell",
|
|
49
|
+
/** Separator line within a vertical operation */
|
|
50
|
+
VOP_SEPARATOR: "VopSeparator",
|
|
51
|
+
/** Carry row within a vertical operation */
|
|
52
|
+
VOP_CARRY_ROW: "VopCarryRow",
|
|
41
53
|
/** Number line layout block */
|
|
42
54
|
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
43
55
|
/** A position marker on the number line */
|
|
@@ -52,6 +64,8 @@ const AST_TYPES = {
|
|
|
52
64
|
MATCHER_ROW: "MatcherRow",
|
|
53
65
|
/** Item within a matcher row */
|
|
54
66
|
MATCHER_ITEM: "MatcherItem",
|
|
67
|
+
/** Pair-matcher layout block (top-row ↔ bottom-row pairing) */
|
|
68
|
+
PAIR_MATCHER_BLOCK: "PairMatcherBlock",
|
|
55
69
|
/** Visual fraction inline widget */
|
|
56
70
|
VISUAL_FRACTION: "VisualFraction",
|
|
57
71
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -70,6 +84,8 @@ const AST_TYPES = {
|
|
|
70
84
|
SIMPLE_VALIDATION: "SimpleValidation",
|
|
71
85
|
/** Expression-based validation */
|
|
72
86
|
EXPRESSION_VALIDATION: "ExpressionValidation",
|
|
87
|
+
/** Mixed validation: per-widget simple answers + expression over free variables */
|
|
88
|
+
MIXED_VALIDATION: "MixedValidation",
|
|
73
89
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
90
|
// Document
|
|
75
91
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -108,6 +124,16 @@ const JSON_TYPES = {
|
|
|
108
124
|
TABLE_ROW: "table-row",
|
|
109
125
|
/** Cell within a table */
|
|
110
126
|
TABLE_CELL: "table-cell",
|
|
127
|
+
/** Vertical operation (column arithmetic) block */
|
|
128
|
+
VERTICAL_OPERATION: "vertical-operation",
|
|
129
|
+
/** Row within a vertical operation */
|
|
130
|
+
VOP_ROW: "vop-row",
|
|
131
|
+
/** Cell within a vertical-operation row */
|
|
132
|
+
VOP_CELL: "vop-cell",
|
|
133
|
+
/** Separator within a vertical operation */
|
|
134
|
+
VOP_SEPARATOR: "vop-separator",
|
|
135
|
+
/** Carry row within a vertical operation */
|
|
136
|
+
VOP_CARRY_ROW: "vop-carry-row",
|
|
111
137
|
/** Number line layout block */
|
|
112
138
|
NUMBER_LINE: "number-line",
|
|
113
139
|
/** Position marker within a number line */
|
|
@@ -122,6 +148,8 @@ const JSON_TYPES = {
|
|
|
122
148
|
MATCHER_ROW: "matcher-row",
|
|
123
149
|
/** Item within a matcher row */
|
|
124
150
|
MATCHER_ITEM: "matcher-item",
|
|
151
|
+
/** Pair-matcher widget (top-row ↔ bottom-row pairing) */
|
|
152
|
+
PAIR_MATCHER: "pair-matcher",
|
|
125
153
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
126
154
|
// Widget Types
|
|
127
155
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -129,6 +157,8 @@ const JSON_TYPES = {
|
|
|
129
157
|
INLINE_INPUT: "inline-input",
|
|
130
158
|
/** Drag-and-drop options widget */
|
|
131
159
|
DRAG_OPTIONS: "drag-options",
|
|
160
|
+
/** Single inline draggable option (appears in-flow with text) */
|
|
161
|
+
INLINE_DRAG_OPTION: "inline-drag-option",
|
|
132
162
|
/** Radio button options widget */
|
|
133
163
|
RADIO_OPTIONS: "radio-options",
|
|
134
164
|
/** Numeric keypad widget */
|
|
@@ -139,11 +169,25 @@ const JSON_TYPES = {
|
|
|
139
169
|
VISUAL_FRACTION: "visual-fraction"
|
|
140
170
|
};
|
|
141
171
|
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
172
|
+
const STATEFUL_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
173
|
+
JSON_TYPES.MATCHER,
|
|
174
|
+
JSON_TYPES.PAIR_MATCHER,
|
|
175
|
+
JSON_TYPES.VISUAL_FRACTION
|
|
176
|
+
]);
|
|
177
|
+
const INLINE_DRAG_OPT_PREFIX = "_inline_drag_opt_";
|
|
178
|
+
const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
179
|
+
JSON_TYPES.INLINE_INPUT,
|
|
180
|
+
JSON_TYPES.INLINE_DRAG_OPTION,
|
|
181
|
+
JSON_TYPES.FRACTION,
|
|
182
|
+
JSON_TYPES.VISUAL_FRACTION
|
|
183
|
+
]);
|
|
142
184
|
const VALIDATION_MODES = {
|
|
143
185
|
/** Simple answer matching validation */
|
|
144
186
|
SIMPLE: "simple",
|
|
145
187
|
/** Expression-based validation using JavaScript expressions */
|
|
146
|
-
EXPRESSION: "expression"
|
|
188
|
+
EXPRESSION: "expression",
|
|
189
|
+
/** Mixed: simple per-widget answers AND a boolean expression over free variables */
|
|
190
|
+
MIXED: "mixed"
|
|
147
191
|
};
|
|
148
192
|
const INPUT_TYPES = {
|
|
149
193
|
/** String/text input */
|
|
@@ -219,6 +263,10 @@ const STRING_INPUT = createToken({
|
|
|
219
263
|
name: "STRING_INPUT",
|
|
220
264
|
pattern: /STRING_INPUT/
|
|
221
265
|
});
|
|
266
|
+
const INLINE_DRAG_OPTION = createToken({
|
|
267
|
+
name: "INLINE_DRAG_OPTION",
|
|
268
|
+
pattern: /INLINE_DRAG_OPTION/
|
|
269
|
+
});
|
|
222
270
|
const DRAGABLE_BUTTONS_OPTIONS = createToken({
|
|
223
271
|
name: "DRAGABLE_BUTTONS_OPTIONS",
|
|
224
272
|
pattern: /DRAGABLE_BUTTONS_OPTIONS/
|
|
@@ -279,10 +327,22 @@ const FRACTION = createToken({
|
|
|
279
327
|
name: "FRACTION",
|
|
280
328
|
pattern: /FRACTION/
|
|
281
329
|
});
|
|
330
|
+
const PAIR_MATCHER = createToken({
|
|
331
|
+
name: "PAIR_MATCHER",
|
|
332
|
+
pattern: /PAIR_MATCHER/
|
|
333
|
+
});
|
|
282
334
|
const MATCHER = createToken({
|
|
283
335
|
name: "MATCHER",
|
|
284
336
|
pattern: /MATCHER/
|
|
285
337
|
});
|
|
338
|
+
const TOP_ROW = createToken({
|
|
339
|
+
name: "TOP_ROW",
|
|
340
|
+
pattern: /TOP_ROW/
|
|
341
|
+
});
|
|
342
|
+
const BOTTOM_ROW = createToken({
|
|
343
|
+
name: "BOTTOM_ROW",
|
|
344
|
+
pattern: /BOTTOM_ROW/
|
|
345
|
+
});
|
|
286
346
|
const ITEM = createToken({
|
|
287
347
|
name: "ITEM",
|
|
288
348
|
pattern: /ITEM/
|
|
@@ -295,6 +355,26 @@ const IMAGE = createToken({
|
|
|
295
355
|
name: "IMAGE",
|
|
296
356
|
pattern: /IMAGE/
|
|
297
357
|
});
|
|
358
|
+
const VERTICAL_OPERATION = createToken({
|
|
359
|
+
name: "VERTICAL_OPERATION",
|
|
360
|
+
pattern: /VERTICAL_OPERATION/
|
|
361
|
+
});
|
|
362
|
+
const SEP = createToken({
|
|
363
|
+
name: "SEP",
|
|
364
|
+
pattern: /SEP(?![a-zA-Z0-9_])/
|
|
365
|
+
});
|
|
366
|
+
const CARRY = createToken({
|
|
367
|
+
name: "CARRY",
|
|
368
|
+
pattern: /CARRY(?![a-zA-Z0-9_])/
|
|
369
|
+
});
|
|
370
|
+
const INPUT = createToken({
|
|
371
|
+
name: "INPUT",
|
|
372
|
+
pattern: /INPUT(?=\s*\[)/
|
|
373
|
+
});
|
|
374
|
+
const Underscore = createToken({
|
|
375
|
+
name: "Underscore",
|
|
376
|
+
pattern: /_(?![a-zA-Z0-9_])/
|
|
377
|
+
});
|
|
298
378
|
const NumberLiteral = createToken({
|
|
299
379
|
name: "NumberLiteral",
|
|
300
380
|
pattern: /-?\d+(\.\d+)?/
|
|
@@ -391,6 +471,7 @@ const allTokens = [
|
|
|
391
471
|
HIGHLIGHT_TEXT,
|
|
392
472
|
W_S_I,
|
|
393
473
|
STRING_INPUT,
|
|
474
|
+
INLINE_DRAG_OPTION,
|
|
394
475
|
DRAGABLE_BUTTONS_OPTIONS,
|
|
395
476
|
RADIO_BUTTONS_OPTIONS,
|
|
396
477
|
NUMERIC_KEYPAD,
|
|
@@ -401,15 +482,23 @@ const allTokens = [
|
|
|
401
482
|
POSITION,
|
|
402
483
|
TABLE,
|
|
403
484
|
ROW,
|
|
485
|
+
TOP_ROW,
|
|
486
|
+
BOTTOM_ROW,
|
|
404
487
|
CELL,
|
|
405
488
|
COUNTER,
|
|
406
489
|
HIGHLIGHT,
|
|
407
490
|
VISUAL_FRACTION,
|
|
408
491
|
FRACTION,
|
|
492
|
+
PAIR_MATCHER,
|
|
409
493
|
MATCHER,
|
|
410
494
|
ITEM,
|
|
411
495
|
ANSWERS,
|
|
412
496
|
IMAGE,
|
|
497
|
+
VERTICAL_OPERATION,
|
|
498
|
+
SEP,
|
|
499
|
+
CARRY,
|
|
500
|
+
INPUT,
|
|
501
|
+
Underscore,
|
|
413
502
|
// Literals
|
|
414
503
|
NumberLiteral,
|
|
415
504
|
StringLiteral,
|
|
@@ -559,10 +648,18 @@ class MathExerciseParser extends CstParser {
|
|
|
559
648
|
GATE: this.BACKTRACK(this.numberLine),
|
|
560
649
|
ALT: () => this.SUBRULE(this.numberLine)
|
|
561
650
|
},
|
|
651
|
+
{
|
|
652
|
+
GATE: this.BACKTRACK(this.pairMatcher),
|
|
653
|
+
ALT: () => this.SUBRULE(this.pairMatcher)
|
|
654
|
+
},
|
|
562
655
|
{
|
|
563
656
|
GATE: this.BACKTRACK(this.matcher),
|
|
564
657
|
ALT: () => this.SUBRULE(this.matcher)
|
|
565
658
|
},
|
|
659
|
+
{
|
|
660
|
+
GATE: this.BACKTRACK(this.verticalOperation),
|
|
661
|
+
ALT: () => this.SUBRULE(this.verticalOperation)
|
|
662
|
+
},
|
|
566
663
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
567
664
|
]);
|
|
568
665
|
});
|
|
@@ -820,6 +917,193 @@ class MathExerciseParser extends CstParser {
|
|
|
820
917
|
});
|
|
821
918
|
this.CONSUME(RBracket);
|
|
822
919
|
});
|
|
920
|
+
this.pairMatcher = this.RULE("pairMatcher", () => {
|
|
921
|
+
this.MANY(() => {
|
|
922
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
923
|
+
});
|
|
924
|
+
this.CONSUME(PAIR_MATCHER);
|
|
925
|
+
this.CONSUME(LBracket);
|
|
926
|
+
this.SUBRULE(this.parameterList);
|
|
927
|
+
this.CONSUME(RBracket);
|
|
928
|
+
this.MANY2(() => {
|
|
929
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
930
|
+
});
|
|
931
|
+
this.CONSUME(LBrace);
|
|
932
|
+
this.MANY3(() => {
|
|
933
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
934
|
+
});
|
|
935
|
+
this.SUBRULE(this.pairMatcherTopSection);
|
|
936
|
+
this.SUBRULE(this.pairMatcherBottomSection);
|
|
937
|
+
this.SUBRULE(this.matcherAnswers);
|
|
938
|
+
this.CONSUME(RBrace);
|
|
939
|
+
this.MANY4(() => {
|
|
940
|
+
this.OR4([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
this.pairMatcherTopSection = this.RULE("pairMatcherTopSection", () => {
|
|
944
|
+
this.MANY(() => {
|
|
945
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
946
|
+
});
|
|
947
|
+
this.CONSUME(TOP_ROW);
|
|
948
|
+
this.MANY2(() => {
|
|
949
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
950
|
+
});
|
|
951
|
+
this.CONSUME(LBrace);
|
|
952
|
+
this.MANY3(() => {
|
|
953
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
954
|
+
});
|
|
955
|
+
this.AT_LEAST_ONE(() => {
|
|
956
|
+
this.SUBRULE(this.matcherItem);
|
|
957
|
+
});
|
|
958
|
+
this.CONSUME(RBrace);
|
|
959
|
+
this.MANY4(() => {
|
|
960
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
this.pairMatcherBottomSection = this.RULE("pairMatcherBottomSection", () => {
|
|
964
|
+
this.MANY(() => {
|
|
965
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
966
|
+
});
|
|
967
|
+
this.CONSUME(BOTTOM_ROW);
|
|
968
|
+
this.MANY2(() => {
|
|
969
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
970
|
+
});
|
|
971
|
+
this.CONSUME(LBrace);
|
|
972
|
+
this.MANY3(() => {
|
|
973
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
974
|
+
});
|
|
975
|
+
this.AT_LEAST_ONE(() => {
|
|
976
|
+
this.SUBRULE(this.matcherItem);
|
|
977
|
+
});
|
|
978
|
+
this.CONSUME(RBrace);
|
|
979
|
+
this.MANY4(() => {
|
|
980
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
this.verticalOperation = this.RULE("verticalOperation", () => {
|
|
984
|
+
this.MANY(() => {
|
|
985
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
986
|
+
});
|
|
987
|
+
this.CONSUME(VERTICAL_OPERATION);
|
|
988
|
+
this.OPTION(() => {
|
|
989
|
+
this.CONSUME(LBracket);
|
|
990
|
+
this.SUBRULE(this.parameterList);
|
|
991
|
+
this.CONSUME(RBracket);
|
|
992
|
+
});
|
|
993
|
+
this.MANY2(() => {
|
|
994
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
995
|
+
});
|
|
996
|
+
this.CONSUME(LBrace);
|
|
997
|
+
this.MANY3(() => {
|
|
998
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
999
|
+
});
|
|
1000
|
+
this.AT_LEAST_ONE(() => {
|
|
1001
|
+
this.OR4([
|
|
1002
|
+
{ ALT: () => this.SUBRULE(this.vopRow) },
|
|
1003
|
+
{ ALT: () => this.SUBRULE(this.vopCarry) },
|
|
1004
|
+
{ ALT: () => this.SUBRULE(this.vopSep) }
|
|
1005
|
+
]);
|
|
1006
|
+
});
|
|
1007
|
+
this.CONSUME(RBrace);
|
|
1008
|
+
this.MANY4(() => {
|
|
1009
|
+
this.OR5([{ ALT: () => this.CONSUME4(Newline) }, { ALT: () => this.CONSUME4(Space) }]);
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
this.vopRow = this.RULE("vopRow", () => {
|
|
1013
|
+
this.CONSUME(ROW);
|
|
1014
|
+
this.OPTION(() => {
|
|
1015
|
+
this.CONSUME(LBracket);
|
|
1016
|
+
this.SUBRULE(this.parameterList);
|
|
1017
|
+
this.CONSUME(RBracket);
|
|
1018
|
+
});
|
|
1019
|
+
this.MANY(() => {
|
|
1020
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1021
|
+
});
|
|
1022
|
+
this.CONSUME(LBrace);
|
|
1023
|
+
this.MANY2(() => {
|
|
1024
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1025
|
+
});
|
|
1026
|
+
this.SUBRULE(this.vopCell);
|
|
1027
|
+
this.MANY3(() => {
|
|
1028
|
+
this.MANY4(() => {
|
|
1029
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1030
|
+
});
|
|
1031
|
+
this.CONSUME(Comma);
|
|
1032
|
+
this.MANY5(() => {
|
|
1033
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1034
|
+
});
|
|
1035
|
+
this.SUBRULE2(this.vopCell);
|
|
1036
|
+
});
|
|
1037
|
+
this.MANY6(() => {
|
|
1038
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
1039
|
+
});
|
|
1040
|
+
this.CONSUME(RBrace);
|
|
1041
|
+
this.MANY7(() => {
|
|
1042
|
+
this.OR6([{ ALT: () => this.CONSUME6(Space) }, { ALT: () => this.CONSUME6(Newline) }]);
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
this.vopCell = this.RULE("vopCell", () => {
|
|
1046
|
+
this.OR([
|
|
1047
|
+
{ ALT: () => this.CONSUME(NumberLiteral) },
|
|
1048
|
+
{ ALT: () => this.CONSUME(StringLiteral) },
|
|
1049
|
+
{ ALT: () => this.CONSUME(Underscore) },
|
|
1050
|
+
{
|
|
1051
|
+
ALT: () => {
|
|
1052
|
+
this.CONSUME(INPUT);
|
|
1053
|
+
this.CONSUME(LBracket);
|
|
1054
|
+
this.SUBRULE(this.parameterList);
|
|
1055
|
+
this.CONSUME(RBracket);
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
{ ALT: () => this.SUBRULE(this.colorText) },
|
|
1059
|
+
{ ALT: () => this.SUBRULE(this.boldText) },
|
|
1060
|
+
{ ALT: () => this.SUBRULE(this.textHighlighter) }
|
|
1061
|
+
]);
|
|
1062
|
+
});
|
|
1063
|
+
this.vopCarry = this.RULE("vopCarry", () => {
|
|
1064
|
+
this.CONSUME(CARRY);
|
|
1065
|
+
this.MANY(() => {
|
|
1066
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1067
|
+
});
|
|
1068
|
+
this.CONSUME(LBrace);
|
|
1069
|
+
this.MANY2(() => {
|
|
1070
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1071
|
+
});
|
|
1072
|
+
this.SUBRULE(this.vopCell);
|
|
1073
|
+
this.MANY3(() => {
|
|
1074
|
+
this.MANY4(() => {
|
|
1075
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1076
|
+
});
|
|
1077
|
+
this.CONSUME(Comma);
|
|
1078
|
+
this.MANY5(() => {
|
|
1079
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1080
|
+
});
|
|
1081
|
+
this.SUBRULE2(this.vopCell);
|
|
1082
|
+
});
|
|
1083
|
+
this.MANY6(() => {
|
|
1084
|
+
this.OR5([{ ALT: () => this.CONSUME5(Space) }, { ALT: () => this.CONSUME5(Newline) }]);
|
|
1085
|
+
});
|
|
1086
|
+
this.CONSUME(RBrace);
|
|
1087
|
+
this.MANY7(() => {
|
|
1088
|
+
this.OR6([{ ALT: () => this.CONSUME6(Newline) }, { ALT: () => this.CONSUME6(Space) }]);
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
this.vopSep = this.RULE("vopSep", () => {
|
|
1092
|
+
this.CONSUME(SEP);
|
|
1093
|
+
this.OPTION(() => {
|
|
1094
|
+
this.MANY(() => {
|
|
1095
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1096
|
+
});
|
|
1097
|
+
this.CONSUME(LBrace);
|
|
1098
|
+
this.MANY2(() => {
|
|
1099
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1100
|
+
});
|
|
1101
|
+
this.CONSUME(RBrace);
|
|
1102
|
+
});
|
|
1103
|
+
this.MANY3(() => {
|
|
1104
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
823
1107
|
this.line = this.RULE("line", () => {
|
|
824
1108
|
this.MANY(() => {
|
|
825
1109
|
this.CONSUME(Space);
|
|
@@ -873,7 +1157,9 @@ class MathExerciseParser extends CstParser {
|
|
|
873
1157
|
// VISUAL_FRACTION — always allowed; display-only when answer=[] or absence of answer parameter, interactive when answer=[...] with values.
|
|
874
1158
|
{ ALT: () => this.SUBRULE(this.visualFraction) },
|
|
875
1159
|
// W_S_I (inline input widget) — excluded in display-only contexts (e.g. MATCHER ITEM)
|
|
876
|
-
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) }
|
|
1160
|
+
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineInput) },
|
|
1161
|
+
// INLINE_DRAG_OPTION (single draggable option inline with text) — excluded in display-only
|
|
1162
|
+
{ GATE: () => !displayOnly, ALT: () => this.SUBRULE(this.inlineDragOption) }
|
|
877
1163
|
]);
|
|
878
1164
|
});
|
|
879
1165
|
this.textHighlighter = this.RULE("textHighlighter", () => {
|
|
@@ -955,6 +1241,12 @@ class MathExerciseParser extends CstParser {
|
|
|
955
1241
|
this.SUBRULE(this.parameterList);
|
|
956
1242
|
this.CONSUME(RBracket);
|
|
957
1243
|
});
|
|
1244
|
+
this.inlineDragOption = this.RULE("inlineDragOption", () => {
|
|
1245
|
+
this.CONSUME(INLINE_DRAG_OPTION);
|
|
1246
|
+
this.CONSUME(LBracket);
|
|
1247
|
+
this.CONSUME(StringLiteral);
|
|
1248
|
+
this.CONSUME(RBracket);
|
|
1249
|
+
});
|
|
958
1250
|
this.tool = this.RULE("tool", () => {
|
|
959
1251
|
this.OR([
|
|
960
1252
|
{ ALT: () => this.SUBRULE(this.dragableButtons) },
|
|
@@ -1223,6 +1515,10 @@ function isPerfectSquare(value) {
|
|
|
1223
1515
|
const root = Math.sqrt(value);
|
|
1224
1516
|
return Number.isInteger(root);
|
|
1225
1517
|
}
|
|
1518
|
+
const InlineDragOptionSchema = z.object({
|
|
1519
|
+
type: z.literal(AST_TYPES.INLINE_DRAG_OPTION),
|
|
1520
|
+
value: z.string().min(1)
|
|
1521
|
+
});
|
|
1226
1522
|
const InlineNodeSchema = z.union([
|
|
1227
1523
|
FreeTextInlineSchema,
|
|
1228
1524
|
SpaceInlineSchema,
|
|
@@ -1232,7 +1528,8 @@ const InlineNodeSchema = z.union([
|
|
|
1232
1528
|
InlineInputWidgetSchema,
|
|
1233
1529
|
FractionInlineSchema,
|
|
1234
1530
|
ImageInlineSchema,
|
|
1235
|
-
VisualFractionInlineSchema
|
|
1531
|
+
VisualFractionInlineSchema,
|
|
1532
|
+
InlineDragOptionSchema
|
|
1236
1533
|
]);
|
|
1237
1534
|
function createFreeText(value) {
|
|
1238
1535
|
return FreeTextInlineSchema.parse({
|
|
@@ -1268,24 +1565,32 @@ function createBoldText(value, highlight) {
|
|
|
1268
1565
|
});
|
|
1269
1566
|
}
|
|
1270
1567
|
function createStringInput(name, length, answer, placeholder) {
|
|
1271
|
-
return parseOrThrow(
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1568
|
+
return parseOrThrow(
|
|
1569
|
+
InlineInputWidgetSchema,
|
|
1570
|
+
{
|
|
1571
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
1572
|
+
name,
|
|
1573
|
+
inputType: INPUT_TYPES.STRING,
|
|
1574
|
+
length,
|
|
1575
|
+
placeholder,
|
|
1576
|
+
answer
|
|
1577
|
+
},
|
|
1578
|
+
"STRING_INPUT"
|
|
1579
|
+
);
|
|
1279
1580
|
}
|
|
1280
1581
|
function createNumericInput(name, digits, answer, placeholder) {
|
|
1281
|
-
return parseOrThrow(
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1582
|
+
return parseOrThrow(
|
|
1583
|
+
InlineInputWidgetSchema,
|
|
1584
|
+
{
|
|
1585
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
1586
|
+
name,
|
|
1587
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
1588
|
+
digits,
|
|
1589
|
+
placeholder,
|
|
1590
|
+
answer
|
|
1591
|
+
},
|
|
1592
|
+
"NUMERIC_INPUT"
|
|
1593
|
+
);
|
|
1289
1594
|
}
|
|
1290
1595
|
function createImage(name, src, alt, options = {}) {
|
|
1291
1596
|
return ImageInlineSchema.parse({
|
|
@@ -1311,6 +1616,12 @@ function createVisualFraction(name, divisions, options = {}) {
|
|
|
1311
1616
|
...options
|
|
1312
1617
|
});
|
|
1313
1618
|
}
|
|
1619
|
+
function createInlineDragOption(value) {
|
|
1620
|
+
return InlineDragOptionSchema.parse({
|
|
1621
|
+
type: AST_TYPES.INLINE_DRAG_OPTION,
|
|
1622
|
+
value
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1314
1625
|
function isFreeText(value) {
|
|
1315
1626
|
return FreeTextInlineSchema.safeParse(value).success;
|
|
1316
1627
|
}
|
|
@@ -1332,6 +1643,9 @@ function isInlineNode(value) {
|
|
|
1332
1643
|
function isVisualFractionInline(value) {
|
|
1333
1644
|
return VisualFractionInlineSchema.safeParse(value).success;
|
|
1334
1645
|
}
|
|
1646
|
+
function isInlineDragOption(value) {
|
|
1647
|
+
return InlineDragOptionSchema.safeParse(value).success;
|
|
1648
|
+
}
|
|
1335
1649
|
const TextBlockSchema = z.object({
|
|
1336
1650
|
type: z.literal(AST_TYPES.TEXT_BLOCK),
|
|
1337
1651
|
value: z.string()
|
|
@@ -1385,17 +1699,19 @@ const NumberLineCounterSchema = z.object({
|
|
|
1385
1699
|
min: z.number().int().min(1, { message: "COUNTER: min must be >= 1" }),
|
|
1386
1700
|
max: z.number().int().positive({ message: "COUNTER: max must be > 0" }),
|
|
1387
1701
|
default: z.number().int().nonnegative().optional(),
|
|
1388
|
-
name: z.string().optional()
|
|
1702
|
+
name: z.string().optional(),
|
|
1703
|
+
answer: z.number().int().positive({ message: "COUNTER: answer must be a positive integer" }).optional()
|
|
1389
1704
|
}).refine((d) => d.min <= d.max, {
|
|
1390
1705
|
message: "COUNTER: min must be <= max"
|
|
1391
1706
|
});
|
|
1392
|
-
function createNumberLineCounter(max, min, defaultValue2, name) {
|
|
1707
|
+
function createNumberLineCounter(max, min, defaultValue2, name, answer) {
|
|
1393
1708
|
return NumberLineCounterSchema.parse({
|
|
1394
1709
|
type: AST_TYPES.NUMBER_LINE_COUNTER,
|
|
1395
1710
|
min: min ?? 1,
|
|
1396
1711
|
max,
|
|
1397
1712
|
...defaultValue2 !== void 0 && { default: defaultValue2 },
|
|
1398
|
-
...name !== void 0 && { name }
|
|
1713
|
+
...name !== void 0 && { name },
|
|
1714
|
+
...answer !== void 0 && { answer }
|
|
1399
1715
|
});
|
|
1400
1716
|
}
|
|
1401
1717
|
const NumberLineHighlightSchema = z.object({
|
|
@@ -1529,6 +1845,146 @@ function createTableBlock(cols, rows) {
|
|
|
1529
1845
|
rows
|
|
1530
1846
|
});
|
|
1531
1847
|
}
|
|
1848
|
+
const VOP_OPERATORS = ["+", "-", "×", "÷", "="];
|
|
1849
|
+
function normalizeVopOperator(raw) {
|
|
1850
|
+
const map = {
|
|
1851
|
+
"+": "+",
|
|
1852
|
+
"-": "-",
|
|
1853
|
+
"−": "-",
|
|
1854
|
+
"*": "×",
|
|
1855
|
+
"x": "×",
|
|
1856
|
+
"X": "×",
|
|
1857
|
+
"×": "×",
|
|
1858
|
+
"/": "÷",
|
|
1859
|
+
"÷": "÷",
|
|
1860
|
+
"=": "="
|
|
1861
|
+
};
|
|
1862
|
+
const op = map[raw];
|
|
1863
|
+
if (!op) throw new Error(`VERTICAL_OPERATION: unsupported op "${raw}"`);
|
|
1864
|
+
return op;
|
|
1865
|
+
}
|
|
1866
|
+
const VopDigitCellSchema = z.object({
|
|
1867
|
+
type: z.literal(AST_TYPES.VOP_CELL),
|
|
1868
|
+
kind: z.literal("digit"),
|
|
1869
|
+
value: z.string()
|
|
1870
|
+
});
|
|
1871
|
+
const VopLabelCellSchema = z.object({
|
|
1872
|
+
type: z.literal(AST_TYPES.VOP_CELL),
|
|
1873
|
+
kind: z.literal("label"),
|
|
1874
|
+
value: z.string()
|
|
1875
|
+
});
|
|
1876
|
+
const VopInputCellSchema = z.object({
|
|
1877
|
+
type: z.literal(AST_TYPES.VOP_CELL),
|
|
1878
|
+
kind: z.literal("input"),
|
|
1879
|
+
name: z.string().min(1),
|
|
1880
|
+
digits: z.number().int().positive().default(1),
|
|
1881
|
+
answer: z.union([z.string(), z.number()]).optional()
|
|
1882
|
+
});
|
|
1883
|
+
const VopEmptyCellSchema = z.object({
|
|
1884
|
+
type: z.literal(AST_TYPES.VOP_CELL),
|
|
1885
|
+
kind: z.literal("empty")
|
|
1886
|
+
});
|
|
1887
|
+
const VopColorCellSchema = z.object({
|
|
1888
|
+
type: z.literal(AST_TYPES.VOP_CELL),
|
|
1889
|
+
kind: z.literal("color"),
|
|
1890
|
+
value: z.string(),
|
|
1891
|
+
color: z.string()
|
|
1892
|
+
});
|
|
1893
|
+
const VopBoldCellSchema = z.object({
|
|
1894
|
+
type: z.literal(AST_TYPES.VOP_CELL),
|
|
1895
|
+
kind: z.literal("bold"),
|
|
1896
|
+
value: z.string()
|
|
1897
|
+
});
|
|
1898
|
+
const VopHighlightCellSchema = z.object({
|
|
1899
|
+
type: z.literal(AST_TYPES.VOP_CELL),
|
|
1900
|
+
kind: z.literal("highlight"),
|
|
1901
|
+
value: z.string()
|
|
1902
|
+
});
|
|
1903
|
+
const VopCellSchema = z.discriminatedUnion("kind", [
|
|
1904
|
+
VopDigitCellSchema,
|
|
1905
|
+
VopLabelCellSchema,
|
|
1906
|
+
VopColorCellSchema,
|
|
1907
|
+
VopBoldCellSchema,
|
|
1908
|
+
VopHighlightCellSchema,
|
|
1909
|
+
VopInputCellSchema,
|
|
1910
|
+
VopEmptyCellSchema
|
|
1911
|
+
]);
|
|
1912
|
+
function createVopDigitCell(value) {
|
|
1913
|
+
return VopDigitCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "digit", value });
|
|
1914
|
+
}
|
|
1915
|
+
function createVopInputCell(name, digits = 1, answer) {
|
|
1916
|
+
return VopInputCellSchema.parse({
|
|
1917
|
+
type: AST_TYPES.VOP_CELL,
|
|
1918
|
+
kind: "input",
|
|
1919
|
+
name,
|
|
1920
|
+
digits,
|
|
1921
|
+
answer
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
function createVopLabelCell(value) {
|
|
1925
|
+
return VopLabelCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "label", value });
|
|
1926
|
+
}
|
|
1927
|
+
function createVopEmptyCell() {
|
|
1928
|
+
return VopEmptyCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "empty" });
|
|
1929
|
+
}
|
|
1930
|
+
function createVopColorCell(value, color) {
|
|
1931
|
+
return VopColorCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "color", value, color });
|
|
1932
|
+
}
|
|
1933
|
+
function createVopBoldCell(value) {
|
|
1934
|
+
return VopBoldCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "bold", value });
|
|
1935
|
+
}
|
|
1936
|
+
function createVopHighlightCell(value) {
|
|
1937
|
+
return VopHighlightCellSchema.parse({ type: AST_TYPES.VOP_CELL, kind: "highlight", value });
|
|
1938
|
+
}
|
|
1939
|
+
const VopRowSchema = z.object({
|
|
1940
|
+
type: z.literal(AST_TYPES.VOP_ROW),
|
|
1941
|
+
cells: z.array(VopCellSchema).min(1),
|
|
1942
|
+
op: z.enum(VOP_OPERATORS).optional()
|
|
1943
|
+
});
|
|
1944
|
+
function createVopRow(cells, op) {
|
|
1945
|
+
return VopRowSchema.parse({ type: AST_TYPES.VOP_ROW, cells, op });
|
|
1946
|
+
}
|
|
1947
|
+
const VopCarryRowSchema = z.object({
|
|
1948
|
+
type: z.literal(AST_TYPES.VOP_CARRY_ROW),
|
|
1949
|
+
cells: z.array(VopCellSchema).min(1)
|
|
1950
|
+
});
|
|
1951
|
+
function createVopCarryRow(cells) {
|
|
1952
|
+
return VopCarryRowSchema.parse({ type: AST_TYPES.VOP_CARRY_ROW, cells });
|
|
1953
|
+
}
|
|
1954
|
+
const VopSeparatorSchema = z.object({
|
|
1955
|
+
type: z.literal(AST_TYPES.VOP_SEPARATOR)
|
|
1956
|
+
});
|
|
1957
|
+
function createVopSeparator() {
|
|
1958
|
+
return VopSeparatorSchema.parse({ type: AST_TYPES.VOP_SEPARATOR });
|
|
1959
|
+
}
|
|
1960
|
+
const VopBlockItemSchema = z.union([VopRowSchema, VopCarryRowSchema, VopSeparatorSchema]);
|
|
1961
|
+
const VerticalOperationBlockSchema = z.object({
|
|
1962
|
+
type: z.literal(AST_TYPES.VERTICAL_OPERATION_BLOCK),
|
|
1963
|
+
op: z.enum(VOP_OPERATORS).optional(),
|
|
1964
|
+
items: z.array(VopBlockItemSchema).min(1)
|
|
1965
|
+
});
|
|
1966
|
+
function createVerticalOperationBlock(op, items) {
|
|
1967
|
+
return VerticalOperationBlockSchema.parse({
|
|
1968
|
+
type: AST_TYPES.VERTICAL_OPERATION_BLOCK,
|
|
1969
|
+
op,
|
|
1970
|
+
items
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
function isVopCell(value) {
|
|
1974
|
+
return VopCellSchema.safeParse(value).success;
|
|
1975
|
+
}
|
|
1976
|
+
function isVopRow(value) {
|
|
1977
|
+
return VopRowSchema.safeParse(value).success;
|
|
1978
|
+
}
|
|
1979
|
+
function isVopCarryRow(value) {
|
|
1980
|
+
return VopCarryRowSchema.safeParse(value).success;
|
|
1981
|
+
}
|
|
1982
|
+
function isVopSeparator(value) {
|
|
1983
|
+
return VopSeparatorSchema.safeParse(value).success;
|
|
1984
|
+
}
|
|
1985
|
+
function isVerticalOperationBlock(value) {
|
|
1986
|
+
return VerticalOperationBlockSchema.safeParse(value).success;
|
|
1987
|
+
}
|
|
1532
1988
|
const DisplayFractionInlineSchema = FractionInlineSchema.refine(
|
|
1533
1989
|
(f) => f.numerator !== void 0 && f.denominator !== void 0,
|
|
1534
1990
|
{ message: "FRACTION inside MATCHER ITEM must be in display mode (both numerator and denominator required)" }
|
|
@@ -1634,6 +2090,91 @@ function createMatcherBlock(name, rows, answers, colors) {
|
|
|
1634
2090
|
function isMatcherBlock(value) {
|
|
1635
2091
|
return MatcherBlockSchema.safeParse(value).success;
|
|
1636
2092
|
}
|
|
2093
|
+
const PairMatcherBlockSchema = z.object({
|
|
2094
|
+
type: z.literal(AST_TYPES.PAIR_MATCHER_BLOCK),
|
|
2095
|
+
name: z.string().min(1),
|
|
2096
|
+
/** Items in the top row — at least one required */
|
|
2097
|
+
topItems: z.array(MatcherItemSchema).min(1),
|
|
2098
|
+
/** Items in the bottom row — at least one required */
|
|
2099
|
+
bottomItems: z.array(MatcherItemSchema).min(1),
|
|
2100
|
+
/**
|
|
2101
|
+
* Each answer pair is [topId, bottomId].
|
|
2102
|
+
* At least one pair is required.
|
|
2103
|
+
*/
|
|
2104
|
+
answers: z.array(z.tuple([z.string().min(1), z.string().min(1)])).min(1)
|
|
2105
|
+
}).superRefine((data, ctx) => {
|
|
2106
|
+
const idRegistry = /* @__PURE__ */ new Map();
|
|
2107
|
+
const sections = [
|
|
2108
|
+
{ key: "topItems", items: data.topItems },
|
|
2109
|
+
{ key: "bottomItems", items: data.bottomItems }
|
|
2110
|
+
];
|
|
2111
|
+
for (const { key: key2, items } of sections) {
|
|
2112
|
+
for (let i = 0; i < items.length; i++) {
|
|
2113
|
+
const { itemId } = items[i];
|
|
2114
|
+
const existing = idRegistry.get(itemId);
|
|
2115
|
+
if (existing) {
|
|
2116
|
+
const isCrossSection = existing.section !== key2;
|
|
2117
|
+
ctx.addIssue({
|
|
2118
|
+
code: z.ZodIssueCode.custom,
|
|
2119
|
+
message: isCrossSection ? `itemId "${itemId}" appears in both TOP and BOTTOM — ids must be globally unique` : `Duplicate itemId "${itemId}" in ${key2 === "topItems" ? "TOP" : "BOTTOM"} section`,
|
|
2120
|
+
path: [key2, i, "itemId"]
|
|
2121
|
+
});
|
|
2122
|
+
} else {
|
|
2123
|
+
idRegistry.set(itemId, { section: key2, index: i });
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
const topIds = new Set(data.topItems.map((it) => it.itemId));
|
|
2128
|
+
const bottomIds = new Set(data.bottomItems.map((it) => it.itemId));
|
|
2129
|
+
const usedTopIds = /* @__PURE__ */ new Set();
|
|
2130
|
+
const usedBottomIds = /* @__PURE__ */ new Set();
|
|
2131
|
+
data.answers.forEach(([topId, bottomId], i) => {
|
|
2132
|
+
if (!topIds.has(topId)) {
|
|
2133
|
+
ctx.addIssue({
|
|
2134
|
+
code: z.ZodIssueCode.custom,
|
|
2135
|
+
message: `Answer pair [${topId}, ${bottomId}]: "${topId}" is not declared in TOP section`,
|
|
2136
|
+
path: ["answers", i, 0]
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
if (!bottomIds.has(bottomId)) {
|
|
2140
|
+
ctx.addIssue({
|
|
2141
|
+
code: z.ZodIssueCode.custom,
|
|
2142
|
+
message: `Answer pair [${topId}, ${bottomId}]: "${bottomId}" is not declared in BOTTOM section`,
|
|
2143
|
+
path: ["answers", i, 1]
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
if (usedTopIds.has(topId)) {
|
|
2147
|
+
ctx.addIssue({
|
|
2148
|
+
code: z.ZodIssueCode.custom,
|
|
2149
|
+
message: `topId "${topId}" appears in more than one answer pair`,
|
|
2150
|
+
path: ["answers", i, 0]
|
|
2151
|
+
});
|
|
2152
|
+
} else {
|
|
2153
|
+
usedTopIds.add(topId);
|
|
2154
|
+
}
|
|
2155
|
+
if (usedBottomIds.has(bottomId)) {
|
|
2156
|
+
ctx.addIssue({
|
|
2157
|
+
code: z.ZodIssueCode.custom,
|
|
2158
|
+
message: `bottomId "${bottomId}" appears in more than one answer pair`,
|
|
2159
|
+
path: ["answers", i, 1]
|
|
2160
|
+
});
|
|
2161
|
+
} else {
|
|
2162
|
+
usedBottomIds.add(bottomId);
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
});
|
|
2166
|
+
function createPairMatcherBlock(name, topItems, bottomItems, answers) {
|
|
2167
|
+
return PairMatcherBlockSchema.parse({
|
|
2168
|
+
type: AST_TYPES.PAIR_MATCHER_BLOCK,
|
|
2169
|
+
name,
|
|
2170
|
+
topItems,
|
|
2171
|
+
bottomItems,
|
|
2172
|
+
answers
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
function isPairMatcherBlock(value) {
|
|
2176
|
+
return PairMatcherBlockSchema.safeParse(value).success;
|
|
2177
|
+
}
|
|
1637
2178
|
const DragOptionsToolSchema = z.object({
|
|
1638
2179
|
type: z.literal(AST_TYPES.DRAG_OPTIONS),
|
|
1639
2180
|
options: z.array(z.string()).min(1)
|
|
@@ -1692,9 +2233,14 @@ const ExpressionValidationSchema = z.object({
|
|
|
1692
2233
|
type: z.literal(AST_TYPES.EXPRESSION_VALIDATION),
|
|
1693
2234
|
expression: z.string()
|
|
1694
2235
|
});
|
|
2236
|
+
const MixedValidationSchema = z.object({
|
|
2237
|
+
type: z.literal(AST_TYPES.MIXED_VALIDATION),
|
|
2238
|
+
expression: z.string().min(1)
|
|
2239
|
+
});
|
|
1695
2240
|
const ValidationNodeSchema = z.discriminatedUnion("type", [
|
|
1696
2241
|
SimpleValidationSchema,
|
|
1697
|
-
ExpressionValidationSchema
|
|
2242
|
+
ExpressionValidationSchema,
|
|
2243
|
+
MixedValidationSchema
|
|
1698
2244
|
]);
|
|
1699
2245
|
function createSimpleValidation() {
|
|
1700
2246
|
return SimpleValidationSchema.parse({
|
|
@@ -1707,16 +2253,25 @@ function createExpressionValidation(expression) {
|
|
|
1707
2253
|
expression
|
|
1708
2254
|
});
|
|
1709
2255
|
}
|
|
2256
|
+
function createMixedValidation(expression) {
|
|
2257
|
+
return MixedValidationSchema.parse({
|
|
2258
|
+
type: AST_TYPES.MIXED_VALIDATION,
|
|
2259
|
+
expression
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
1710
2262
|
function isSimpleValidation(value) {
|
|
1711
2263
|
return SimpleValidationSchema.safeParse(value).success;
|
|
1712
2264
|
}
|
|
1713
2265
|
function isExpressionValidation(value) {
|
|
1714
2266
|
return ExpressionValidationSchema.safeParse(value).success;
|
|
1715
2267
|
}
|
|
2268
|
+
function isMixedValidation(value) {
|
|
2269
|
+
return MixedValidationSchema.safeParse(value).success;
|
|
2270
|
+
}
|
|
1716
2271
|
function isValidationNode(value) {
|
|
1717
2272
|
return ValidationNodeSchema.safeParse(value).success;
|
|
1718
2273
|
}
|
|
1719
|
-
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema]);
|
|
2274
|
+
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema]);
|
|
1720
2275
|
const ExerciseDocumentSchema = z.object({
|
|
1721
2276
|
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
1722
2277
|
question: TextBlockSchema.optional(),
|
|
@@ -1764,6 +2319,12 @@ const ExerciseDocumentSchema = z.object({
|
|
|
1764
2319
|
visitInlines(item.inlines, ["lines", bi, "rows", ri, "items", ii, "inlines"]);
|
|
1765
2320
|
});
|
|
1766
2321
|
});
|
|
2322
|
+
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
2323
|
+
const pm = block;
|
|
2324
|
+
record(pm.name, ["lines", bi, "name"]);
|
|
2325
|
+
[...pm.topItems, ...pm.bottomItems].forEach((item, ii) => {
|
|
2326
|
+
visitInlines(item.inlines, ["lines", bi, "items", ii, "inlines"]);
|
|
2327
|
+
});
|
|
1767
2328
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
1768
2329
|
record(block.name, ["lines", bi, "name"]);
|
|
1769
2330
|
}
|
|
@@ -1812,7 +2373,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1812
2373
|
if (ctx.table) return this.visit(ctx.table);
|
|
1813
2374
|
if (ctx.radioButtons) return this.visit(ctx.radioButtons);
|
|
1814
2375
|
if (ctx.numberLine) return this.visit(ctx.numberLine);
|
|
2376
|
+
if (ctx.pairMatcher) return this.visit(ctx.pairMatcher);
|
|
1815
2377
|
if (ctx.matcher) return this.visit(ctx.matcher);
|
|
2378
|
+
if (ctx.verticalOperation) return this.visit(ctx.verticalOperation);
|
|
1816
2379
|
if (ctx.line) return this.visit(ctx.line);
|
|
1817
2380
|
throw new Error("Unknown content block");
|
|
1818
2381
|
}
|
|
@@ -1884,7 +2447,8 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1884
2447
|
const min = params.min !== void 0 ? Number(params.min) : void 0;
|
|
1885
2448
|
const defaultValue2 = params.default !== void 0 ? Number(params.default) : void 0;
|
|
1886
2449
|
const name = params.name;
|
|
1887
|
-
|
|
2450
|
+
const answer = params.answer !== void 0 ? Number(params.answer) : void 0;
|
|
2451
|
+
return createNumberLineCounter(max, min, defaultValue2, name, answer);
|
|
1888
2452
|
}
|
|
1889
2453
|
highlight(ctx) {
|
|
1890
2454
|
const params = this.visit(ctx.parameterList);
|
|
@@ -1925,6 +2489,102 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1925
2489
|
return ctx.ids ? ctx.ids.map((t) => t.image) : [];
|
|
1926
2490
|
}
|
|
1927
2491
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2492
|
+
// Pair Matcher Block
|
|
2493
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2494
|
+
pairMatcher(ctx) {
|
|
2495
|
+
const params = this.visit(ctx.parameterList);
|
|
2496
|
+
const name = params.name;
|
|
2497
|
+
const topItems = this.visit(ctx.pairMatcherTopSection);
|
|
2498
|
+
const bottomItems = this.visit(ctx.pairMatcherBottomSection);
|
|
2499
|
+
const rawAnswers = this.visit(ctx.matcherAnswers);
|
|
2500
|
+
return createPairMatcherBlock(name, topItems, bottomItems, rawAnswers);
|
|
2501
|
+
}
|
|
2502
|
+
pairMatcherTopSection(ctx) {
|
|
2503
|
+
return this.visitMatcherItemList(ctx);
|
|
2504
|
+
}
|
|
2505
|
+
pairMatcherBottomSection(ctx) {
|
|
2506
|
+
return this.visitMatcherItemList(ctx);
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Maps `ctx.matcherItem` CST nodes to MatcherItem AST nodes.
|
|
2510
|
+
* Shared by pairMatcherTopSection and pairMatcherBottomSection because their
|
|
2511
|
+
* grammar rules are structurally identical — both are `matcherItem+` lists
|
|
2512
|
+
* wrapped in a labelled section keyword.
|
|
2513
|
+
*/
|
|
2514
|
+
visitMatcherItemList(ctx) {
|
|
2515
|
+
return ctx.matcherItem ? ctx.matcherItem.map((i) => this.visit(i)) : [];
|
|
2516
|
+
}
|
|
2517
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2518
|
+
// Vertical Operation Block
|
|
2519
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2520
|
+
verticalOperation(ctx) {
|
|
2521
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
2522
|
+
const op = params.op !== void 0 ? normalizeVopOperator(String(params.op)) : void 0;
|
|
2523
|
+
const tagged = [];
|
|
2524
|
+
ctx.vopRow?.forEach((cstNode, i) => {
|
|
2525
|
+
tagged.push({ offset: cstNode.location?.startOffset ?? i, node: this.visit(cstNode) });
|
|
2526
|
+
});
|
|
2527
|
+
ctx.vopCarry?.forEach((cstNode, i) => {
|
|
2528
|
+
tagged.push({ offset: cstNode.location?.startOffset ?? i, node: this.visit(cstNode) });
|
|
2529
|
+
});
|
|
2530
|
+
ctx.vopSep?.forEach((cstNode, i) => {
|
|
2531
|
+
tagged.push({ offset: cstNode.location?.startOffset ?? i, node: this.visit(cstNode) });
|
|
2532
|
+
});
|
|
2533
|
+
tagged.sort((a, b) => a.offset - b.offset);
|
|
2534
|
+
const items = tagged.map((t) => t.node);
|
|
2535
|
+
return createVerticalOperationBlock(op, items);
|
|
2536
|
+
}
|
|
2537
|
+
vopRow(ctx) {
|
|
2538
|
+
const cells = ctx.vopCell ? ctx.vopCell.map((c) => this.visit(c)) : [];
|
|
2539
|
+
const rowParams = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
2540
|
+
const op = rowParams.op !== void 0 ? normalizeVopOperator(String(rowParams.op)) : void 0;
|
|
2541
|
+
return createVopRow(cells, op);
|
|
2542
|
+
}
|
|
2543
|
+
vopCarry(ctx) {
|
|
2544
|
+
const cells = ctx.vopCell ? ctx.vopCell.map((c) => this.visit(c)) : [];
|
|
2545
|
+
return createVopCarryRow(cells);
|
|
2546
|
+
}
|
|
2547
|
+
vopSep(_ctx) {
|
|
2548
|
+
return createVopSeparator();
|
|
2549
|
+
}
|
|
2550
|
+
vopCell(ctx) {
|
|
2551
|
+
if (ctx.NumberLiteral) {
|
|
2552
|
+
const token = ctx.NumberLiteral[0];
|
|
2553
|
+
return createVopDigitCell(token.image);
|
|
2554
|
+
}
|
|
2555
|
+
if (ctx.StringLiteral) {
|
|
2556
|
+
const token = ctx.StringLiteral[0];
|
|
2557
|
+
const value = token.image.slice(1, -1);
|
|
2558
|
+
return createVopLabelCell(value);
|
|
2559
|
+
}
|
|
2560
|
+
if (ctx.Underscore) {
|
|
2561
|
+
return createVopEmptyCell();
|
|
2562
|
+
}
|
|
2563
|
+
if (ctx.INPUT) {
|
|
2564
|
+
const params = this.visit(ctx.parameterList);
|
|
2565
|
+
const name = params.name;
|
|
2566
|
+
if (!name) {
|
|
2567
|
+
throw new Error("INPUT inside VERTICAL_OPERATION requires parameter: name");
|
|
2568
|
+
}
|
|
2569
|
+
const digits = params.digits !== void 0 ? Number(params.digits) : 1;
|
|
2570
|
+
const answer = params.answer;
|
|
2571
|
+
return createVopInputCell(name, digits, answer);
|
|
2572
|
+
}
|
|
2573
|
+
if (ctx.colorText) {
|
|
2574
|
+
const inline = this.visit(ctx.colorText);
|
|
2575
|
+
return createVopColorCell(inline.value, inline.color);
|
|
2576
|
+
}
|
|
2577
|
+
if (ctx.boldText) {
|
|
2578
|
+
const inline = this.visit(ctx.boldText);
|
|
2579
|
+
return createVopBoldCell(inline.value);
|
|
2580
|
+
}
|
|
2581
|
+
if (ctx.textHighlighter) {
|
|
2582
|
+
const inline = this.visit(ctx.textHighlighter);
|
|
2583
|
+
return createVopHighlightCell(inline.value);
|
|
2584
|
+
}
|
|
2585
|
+
throw new Error("Unknown vopCell");
|
|
2586
|
+
}
|
|
2587
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1928
2588
|
// Line Block
|
|
1929
2589
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1930
2590
|
line(ctx) {
|
|
@@ -2020,6 +2680,9 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2020
2680
|
if (ctx.inlineInput) {
|
|
2021
2681
|
return this.visit(ctx.inlineInput);
|
|
2022
2682
|
}
|
|
2683
|
+
if (ctx.inlineDragOption) {
|
|
2684
|
+
return this.visit(ctx.inlineDragOption);
|
|
2685
|
+
}
|
|
2023
2686
|
throw new Error("Unknown inline element");
|
|
2024
2687
|
}
|
|
2025
2688
|
textHighlighter(ctx) {
|
|
@@ -2096,6 +2759,11 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2096
2759
|
const digits = params.digits;
|
|
2097
2760
|
return createNumericInput(name, digits, answer, placeholder);
|
|
2098
2761
|
}
|
|
2762
|
+
inlineDragOption(ctx) {
|
|
2763
|
+
const valueToken = ctx.StringLiteral[0];
|
|
2764
|
+
const value = this.extractString(valueToken.image);
|
|
2765
|
+
return createInlineDragOption(value);
|
|
2766
|
+
}
|
|
2099
2767
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
2100
2768
|
// Tools
|
|
2101
2769
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -2130,6 +2798,12 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2130
2798
|
} else if (type === "expression") {
|
|
2131
2799
|
const expression = params.expression;
|
|
2132
2800
|
return createExpressionValidation(expression);
|
|
2801
|
+
} else if (type === "mixed") {
|
|
2802
|
+
const expression = params.expression;
|
|
2803
|
+
if (!expression || expression.trim() === "") {
|
|
2804
|
+
throw new Error("Mixed validation requires a non-empty expression");
|
|
2805
|
+
}
|
|
2806
|
+
return createMixedValidation(expression);
|
|
2133
2807
|
}
|
|
2134
2808
|
throw new Error(`Unknown validation type: ${type}`);
|
|
2135
2809
|
}
|
|
@@ -2284,7 +2958,8 @@ const LayoutItemSchema = z.lazy(
|
|
|
2284
2958
|
GroupItemSchema,
|
|
2285
2959
|
OperatorItemSchema,
|
|
2286
2960
|
TableItemSchema,
|
|
2287
|
-
NumberLineItemSchema
|
|
2961
|
+
NumberLineItemSchema,
|
|
2962
|
+
VerticalOperationItemSchema
|
|
2288
2963
|
])
|
|
2289
2964
|
);
|
|
2290
2965
|
const GroupItemSchema = z.object({
|
|
@@ -2321,7 +2996,8 @@ const NumberLineCounterItemSchema = z.object({
|
|
|
2321
2996
|
min: z.number().int().min(1),
|
|
2322
2997
|
max: z.number().int().positive(),
|
|
2323
2998
|
default: z.number().int().nonnegative().optional(),
|
|
2324
|
-
name: z.string().optional()
|
|
2999
|
+
name: z.string().optional(),
|
|
3000
|
+
answer: z.number().int().positive().optional()
|
|
2325
3001
|
});
|
|
2326
3002
|
const NumberLineHighlightItemSchema = z.object({
|
|
2327
3003
|
id: z.string(),
|
|
@@ -2358,6 +3034,80 @@ const MatcherRowItemSchema = z.object({
|
|
|
2358
3034
|
type: z.literal(JSON_TYPES.MATCHER_ROW),
|
|
2359
3035
|
items: z.array(MatcherItemContentSchema).min(1)
|
|
2360
3036
|
});
|
|
3037
|
+
const VopOperatorJSONSchema = z.enum(["+", "-", "×", "÷", "="]);
|
|
3038
|
+
const VopDigitCellItemSchema = z.object({
|
|
3039
|
+
id: z.string(),
|
|
3040
|
+
type: z.literal(JSON_TYPES.VOP_CELL),
|
|
3041
|
+
kind: z.literal("digit"),
|
|
3042
|
+
value: z.string()
|
|
3043
|
+
});
|
|
3044
|
+
const VopInputCellItemSchema = z.object({
|
|
3045
|
+
id: z.string(),
|
|
3046
|
+
type: z.literal(JSON_TYPES.VOP_CELL),
|
|
3047
|
+
kind: z.literal("input"),
|
|
3048
|
+
widgetId: z.string()
|
|
3049
|
+
});
|
|
3050
|
+
const VopLabelCellItemSchema = z.object({
|
|
3051
|
+
id: z.string(),
|
|
3052
|
+
type: z.literal(JSON_TYPES.VOP_CELL),
|
|
3053
|
+
kind: z.literal("label"),
|
|
3054
|
+
value: z.string()
|
|
3055
|
+
});
|
|
3056
|
+
const VopEmptyCellItemSchema = z.object({
|
|
3057
|
+
id: z.string(),
|
|
3058
|
+
type: z.literal(JSON_TYPES.VOP_CELL),
|
|
3059
|
+
kind: z.literal("empty")
|
|
3060
|
+
});
|
|
3061
|
+
const VopColorCellItemSchema = z.object({
|
|
3062
|
+
id: z.string(),
|
|
3063
|
+
type: z.literal(JSON_TYPES.VOP_CELL),
|
|
3064
|
+
kind: z.literal("color"),
|
|
3065
|
+
value: z.string(),
|
|
3066
|
+
color: z.string()
|
|
3067
|
+
});
|
|
3068
|
+
const VopBoldCellItemSchema = z.object({
|
|
3069
|
+
id: z.string(),
|
|
3070
|
+
type: z.literal(JSON_TYPES.VOP_CELL),
|
|
3071
|
+
kind: z.literal("bold"),
|
|
3072
|
+
value: z.string()
|
|
3073
|
+
});
|
|
3074
|
+
const VopHighlightCellItemSchema = z.object({
|
|
3075
|
+
id: z.string(),
|
|
3076
|
+
type: z.literal(JSON_TYPES.VOP_CELL),
|
|
3077
|
+
kind: z.literal("highlight"),
|
|
3078
|
+
value: z.string()
|
|
3079
|
+
});
|
|
3080
|
+
const VopCellItemSchema = z.discriminatedUnion("kind", [
|
|
3081
|
+
VopDigitCellItemSchema,
|
|
3082
|
+
VopLabelCellItemSchema,
|
|
3083
|
+
VopColorCellItemSchema,
|
|
3084
|
+
VopBoldCellItemSchema,
|
|
3085
|
+
VopHighlightCellItemSchema,
|
|
3086
|
+
VopInputCellItemSchema,
|
|
3087
|
+
VopEmptyCellItemSchema
|
|
3088
|
+
]);
|
|
3089
|
+
const VopRowItemSchema = z.object({
|
|
3090
|
+
id: z.string(),
|
|
3091
|
+
type: z.literal(JSON_TYPES.VOP_ROW),
|
|
3092
|
+
cells: z.array(VopCellItemSchema),
|
|
3093
|
+
op: VopOperatorJSONSchema.optional()
|
|
3094
|
+
});
|
|
3095
|
+
const VopCarryRowItemSchema = z.object({
|
|
3096
|
+
id: z.string(),
|
|
3097
|
+
type: z.literal(JSON_TYPES.VOP_CARRY_ROW),
|
|
3098
|
+
cells: z.array(VopCellItemSchema)
|
|
3099
|
+
});
|
|
3100
|
+
const VopSeparatorItemSchema = z.object({
|
|
3101
|
+
id: z.string(),
|
|
3102
|
+
type: z.literal(JSON_TYPES.VOP_SEPARATOR)
|
|
3103
|
+
});
|
|
3104
|
+
const VerticalOperationItemSchema = z.object({
|
|
3105
|
+
id: z.string(),
|
|
3106
|
+
type: z.literal(JSON_TYPES.VERTICAL_OPERATION),
|
|
3107
|
+
op: VopOperatorJSONSchema.optional(),
|
|
3108
|
+
cols: z.number().int().positive(),
|
|
3109
|
+
items: z.array(z.union([VopRowItemSchema, VopCarryRowItemSchema, VopSeparatorItemSchema]))
|
|
3110
|
+
});
|
|
2361
3111
|
const StringInputJSONSchema = z.object({
|
|
2362
3112
|
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
2363
3113
|
inputType: z.literal(INPUT_TYPES.STRING),
|
|
@@ -2378,6 +3128,10 @@ const DragOptionsJSONSchema = z.object({
|
|
|
2378
3128
|
type: z.literal(JSON_TYPES.DRAG_OPTIONS),
|
|
2379
3129
|
options: z.array(z.string())
|
|
2380
3130
|
});
|
|
3131
|
+
const InlineDragOptionJSONSchema = z.object({
|
|
3132
|
+
type: z.literal(JSON_TYPES.INLINE_DRAG_OPTION),
|
|
3133
|
+
value: z.string().min(1)
|
|
3134
|
+
});
|
|
2381
3135
|
const RadioOptionsJSONSchema = z.object({
|
|
2382
3136
|
type: z.literal(JSON_TYPES.RADIO_OPTIONS),
|
|
2383
3137
|
options: z.array(z.string())
|
|
@@ -2403,6 +3157,11 @@ const MatcherWidgetJSONSchema = z.object({
|
|
|
2403
3157
|
/** Number of answer groups — equals answers.length, stored here to avoid prop-drilling. */
|
|
2404
3158
|
groupCount: z.number().int().min(1)
|
|
2405
3159
|
});
|
|
3160
|
+
const PairMatcherWidgetJSONSchema = z.object({
|
|
3161
|
+
type: z.literal(JSON_TYPES.PAIR_MATCHER),
|
|
3162
|
+
topItems: z.array(MatcherItemContentSchema).min(1),
|
|
3163
|
+
bottomItems: z.array(MatcherItemContentSchema).min(1)
|
|
3164
|
+
});
|
|
2406
3165
|
const VisualFractionJSONSchema = z.object({
|
|
2407
3166
|
type: z.literal(JSON_TYPES.VISUAL_FRACTION),
|
|
2408
3167
|
shape: z.enum(["bar", "circle", "triangle", "grid"]).default("bar"),
|
|
@@ -2423,14 +3182,16 @@ const VisualFractionJSONSchema = z.object({
|
|
|
2423
3182
|
const WidgetJSONSchema = z.union([
|
|
2424
3183
|
InlineInputJSONSchema,
|
|
2425
3184
|
DragOptionsJSONSchema,
|
|
3185
|
+
InlineDragOptionJSONSchema,
|
|
2426
3186
|
RadioOptionsJSONSchema,
|
|
2427
3187
|
NumericKeypadJSONSchema,
|
|
2428
3188
|
FractionJSONSchema,
|
|
2429
3189
|
MatcherWidgetJSONSchema,
|
|
3190
|
+
PairMatcherWidgetJSONSchema,
|
|
2430
3191
|
VisualFractionJSONSchema
|
|
2431
3192
|
]);
|
|
2432
3193
|
const ValidationJSONSchema = z.object({
|
|
2433
|
-
mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
|
|
3194
|
+
mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION, VALIDATION_MODES.MIXED]),
|
|
2434
3195
|
// Simple answers: string|number per widget id.
|
|
2435
3196
|
// Matcher answers: string[][] keyed by matcher name. Outer/inner min(1)
|
|
2436
3197
|
// mirrors MatcherBlockSchema invariants in src/ast/matcher.ts.
|
|
@@ -2514,12 +3275,18 @@ function isTableRowItem(item) {
|
|
|
2514
3275
|
function isTableCellItem(item) {
|
|
2515
3276
|
return item.type === JSON_TYPES.TABLE_CELL;
|
|
2516
3277
|
}
|
|
3278
|
+
function isVerticalOperationItem(item) {
|
|
3279
|
+
return item.type === JSON_TYPES.VERTICAL_OPERATION;
|
|
3280
|
+
}
|
|
2517
3281
|
function isInlineInputJSON(widget) {
|
|
2518
3282
|
return widget.type === JSON_TYPES.INLINE_INPUT;
|
|
2519
3283
|
}
|
|
2520
3284
|
function isDragOptionsJSON(widget) {
|
|
2521
3285
|
return widget.type === JSON_TYPES.DRAG_OPTIONS;
|
|
2522
3286
|
}
|
|
3287
|
+
function isInlineDragOptionJSON(widget) {
|
|
3288
|
+
return widget.type === JSON_TYPES.INLINE_DRAG_OPTION;
|
|
3289
|
+
}
|
|
2523
3290
|
function isRadioOptionsJSON(widget) {
|
|
2524
3291
|
return widget.type === JSON_TYPES.RADIO_OPTIONS;
|
|
2525
3292
|
}
|
|
@@ -2542,6 +3309,18 @@ class LayoutIdGenerator {
|
|
|
2542
3309
|
}
|
|
2543
3310
|
function toJSON(ast) {
|
|
2544
3311
|
const idGenerator = new LayoutIdGenerator();
|
|
3312
|
+
const widgets = {};
|
|
3313
|
+
const answers = {};
|
|
3314
|
+
const allInlines = collectAllInlines(ast.lines);
|
|
3315
|
+
const inlineDragIds = /* @__PURE__ */ new Map();
|
|
3316
|
+
{
|
|
3317
|
+
let counter = 0;
|
|
3318
|
+
for (const inline of allInlines) {
|
|
3319
|
+
if (inline.type === AST_TYPES.INLINE_DRAG_OPTION) {
|
|
3320
|
+
inlineDragIds.set(inline, `${INLINE_DRAG_OPT_PREFIX}${counter++}`);
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
2545
3324
|
const layout = [];
|
|
2546
3325
|
if (ast.question) {
|
|
2547
3326
|
layout.push({
|
|
@@ -2552,9 +3331,13 @@ function toJSON(ast) {
|
|
|
2552
3331
|
}
|
|
2553
3332
|
for (const block of ast.lines) {
|
|
2554
3333
|
if (block.type === AST_TYPES.TABLE_BLOCK) {
|
|
2555
|
-
layout.push(transformTable(block, idGenerator));
|
|
3334
|
+
layout.push(transformTable(block, idGenerator, inlineDragIds));
|
|
2556
3335
|
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
2557
|
-
|
|
3336
|
+
const nlItem = transformNumberLine(block, idGenerator, inlineDragIds);
|
|
3337
|
+
layout.push(nlItem);
|
|
3338
|
+
if (nlItem.counter?.answer !== void 0) {
|
|
3339
|
+
answers[nlItem.counter.id] = nlItem.counter.answer;
|
|
3340
|
+
}
|
|
2558
3341
|
} else if (block.type === AST_TYPES.MATCHER_BLOCK) {
|
|
2559
3342
|
const matcherBlock = block;
|
|
2560
3343
|
layout.push({
|
|
@@ -2566,6 +3349,19 @@ function toJSON(ast) {
|
|
|
2566
3349
|
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
2567
3350
|
type: JSON_TYPES.LINEBREAK
|
|
2568
3351
|
});
|
|
3352
|
+
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
3353
|
+
const pmBlock = block;
|
|
3354
|
+
layout.push({
|
|
3355
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
3356
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
3357
|
+
widgetId: pmBlock.name
|
|
3358
|
+
});
|
|
3359
|
+
layout.push({
|
|
3360
|
+
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
3361
|
+
type: JSON_TYPES.LINEBREAK
|
|
3362
|
+
});
|
|
3363
|
+
} else if (block.type === AST_TYPES.VERTICAL_OPERATION_BLOCK) {
|
|
3364
|
+
layout.push(transformVerticalOperation(block, idGenerator));
|
|
2569
3365
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
2570
3366
|
const radioBlock = block;
|
|
2571
3367
|
layout.push({
|
|
@@ -2578,13 +3374,10 @@ function toJSON(ast) {
|
|
|
2578
3374
|
type: JSON_TYPES.LINEBREAK
|
|
2579
3375
|
});
|
|
2580
3376
|
} else {
|
|
2581
|
-
const lineItems = transformLine(block, idGenerator);
|
|
3377
|
+
const lineItems = transformLine(block, idGenerator, inlineDragIds);
|
|
2582
3378
|
layout.push(...lineItems);
|
|
2583
3379
|
}
|
|
2584
3380
|
}
|
|
2585
|
-
const widgets = {};
|
|
2586
|
-
const answers = {};
|
|
2587
|
-
const allInlines = collectAllInlines(ast.lines);
|
|
2588
3381
|
for (const inline of allInlines) {
|
|
2589
3382
|
if (inline.type === AST_TYPES.INLINE_INPUT) {
|
|
2590
3383
|
if (inline.inputType === INPUT_TYPES.STRING) {
|
|
@@ -2618,6 +3411,13 @@ function toJSON(ast) {
|
|
|
2618
3411
|
answers[inline.name] = inline.answer;
|
|
2619
3412
|
}
|
|
2620
3413
|
}
|
|
3414
|
+
if (inline.type === AST_TYPES.INLINE_DRAG_OPTION) {
|
|
3415
|
+
const widgetId = inlineDragIds.get(inline);
|
|
3416
|
+
widgets[widgetId] = {
|
|
3417
|
+
type: JSON_TYPES.INLINE_DRAG_OPTION,
|
|
3418
|
+
value: inline.value
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
2621
3421
|
if (inline.type === AST_TYPES.VISUAL_FRACTION) {
|
|
2622
3422
|
const answerArr = inline.answer ?? [];
|
|
2623
3423
|
widgets[inline.name] = {
|
|
@@ -2635,6 +3435,27 @@ function toJSON(ast) {
|
|
|
2635
3435
|
}
|
|
2636
3436
|
}
|
|
2637
3437
|
}
|
|
3438
|
+
for (const block of ast.lines) {
|
|
3439
|
+
if (block.type !== AST_TYPES.VERTICAL_OPERATION_BLOCK) continue;
|
|
3440
|
+
const vop = block;
|
|
3441
|
+
for (const item of vop.items) {
|
|
3442
|
+
if (item.type !== AST_TYPES.VOP_ROW && item.type !== AST_TYPES.VOP_CARRY_ROW) continue;
|
|
3443
|
+
for (const cell of item.cells) {
|
|
3444
|
+
if (cell.kind !== "input") continue;
|
|
3445
|
+
if (widgets[cell.name]) {
|
|
3446
|
+
throw new Error(`VERTICAL_OPERATION: duplicate INPUT name "${cell.name}"`);
|
|
3447
|
+
}
|
|
3448
|
+
widgets[cell.name] = {
|
|
3449
|
+
type: JSON_TYPES.INLINE_INPUT,
|
|
3450
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
3451
|
+
digits: cell.digits
|
|
3452
|
+
};
|
|
3453
|
+
if (cell.answer !== void 0) {
|
|
3454
|
+
answers[cell.name] = cell.answer;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
2638
3459
|
for (const block of ast.lines) {
|
|
2639
3460
|
if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
2640
3461
|
const radioBlock = block;
|
|
@@ -2645,8 +3466,12 @@ function toJSON(ast) {
|
|
|
2645
3466
|
answers[radioBlock.name] = radioBlock.answer;
|
|
2646
3467
|
} else if (block.type === AST_TYPES.MATCHER_BLOCK) {
|
|
2647
3468
|
const matcherBlock = block;
|
|
2648
|
-
widgets[matcherBlock.name] = buildMatcherWidget(matcherBlock, idGenerator);
|
|
3469
|
+
widgets[matcherBlock.name] = buildMatcherWidget(matcherBlock, idGenerator, inlineDragIds);
|
|
2649
3470
|
answers[matcherBlock.name] = matcherBlock.answers;
|
|
3471
|
+
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
3472
|
+
const pmBlock = block;
|
|
3473
|
+
widgets[pmBlock.name] = buildPairMatcherWidget(pmBlock, idGenerator, inlineDragIds);
|
|
3474
|
+
answers[pmBlock.name] = pmBlock.answers;
|
|
2650
3475
|
}
|
|
2651
3476
|
}
|
|
2652
3477
|
if (ast.tools) {
|
|
@@ -2695,13 +3520,18 @@ function collectAllInlines(blocks) {
|
|
|
2695
3520
|
result.push(...item.inlines);
|
|
2696
3521
|
}
|
|
2697
3522
|
}
|
|
3523
|
+
} else if (block.type === AST_TYPES.PAIR_MATCHER_BLOCK) {
|
|
3524
|
+
const pm = block;
|
|
3525
|
+
for (const item of [...pm.topItems, ...pm.bottomItems]) {
|
|
3526
|
+
result.push(...item.inlines);
|
|
3527
|
+
}
|
|
2698
3528
|
}
|
|
2699
3529
|
}
|
|
2700
3530
|
return result;
|
|
2701
3531
|
}
|
|
2702
|
-
function transformNumberLine(numberLine, idGenerator) {
|
|
3532
|
+
function transformNumberLine(numberLine, idGenerator, inlineDragIds) {
|
|
2703
3533
|
const positions = numberLine.positions.map((pos) => {
|
|
2704
|
-
const content = pos.inlines.map((inline) => transformInline(inline, idGenerator));
|
|
3534
|
+
const content = pos.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds));
|
|
2705
3535
|
return {
|
|
2706
3536
|
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
2707
3537
|
type: JSON_TYPES.NUMBER_LINE_POSITION,
|
|
@@ -2718,7 +3548,8 @@ function transformNumberLine(numberLine, idGenerator) {
|
|
|
2718
3548
|
min: c.min,
|
|
2719
3549
|
max: c.max,
|
|
2720
3550
|
...c.default !== void 0 && { default: c.default },
|
|
2721
|
-
...c.name !== void 0 && { name: c.name }
|
|
3551
|
+
...c.name !== void 0 && { name: c.name },
|
|
3552
|
+
...c.answer !== void 0 && { answer: c.answer }
|
|
2722
3553
|
};
|
|
2723
3554
|
}
|
|
2724
3555
|
let highlights;
|
|
@@ -2748,20 +3579,20 @@ function transformNumberLine(numberLine, idGenerator) {
|
|
|
2748
3579
|
};
|
|
2749
3580
|
return item;
|
|
2750
3581
|
}
|
|
2751
|
-
function
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
});
|
|
3582
|
+
function mapMatcherItemList(items, idGenerator, inlineDragIds) {
|
|
3583
|
+
return items.map((item) => ({
|
|
3584
|
+
id: idGenerator.generate(JSON_TYPES.MATCHER_ITEM),
|
|
3585
|
+
type: JSON_TYPES.MATCHER_ITEM,
|
|
3586
|
+
itemId: item.itemId,
|
|
3587
|
+
content: item.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds))
|
|
3588
|
+
}));
|
|
3589
|
+
}
|
|
3590
|
+
function buildMatcherWidget(matcher, idGenerator, inlineDragIds) {
|
|
3591
|
+
const rows = matcher.rows.map((row) => ({
|
|
3592
|
+
id: idGenerator.generate(JSON_TYPES.MATCHER_ROW),
|
|
3593
|
+
type: JSON_TYPES.MATCHER_ROW,
|
|
3594
|
+
items: mapMatcherItemList(row.items, idGenerator, inlineDragIds)
|
|
3595
|
+
}));
|
|
2765
3596
|
const result = {
|
|
2766
3597
|
type: JSON_TYPES.MATCHER,
|
|
2767
3598
|
rows,
|
|
@@ -2772,10 +3603,72 @@ function buildMatcherWidget(matcher, idGenerator) {
|
|
|
2772
3603
|
}
|
|
2773
3604
|
return result;
|
|
2774
3605
|
}
|
|
2775
|
-
function
|
|
3606
|
+
function buildPairMatcherWidget(pm, idGenerator, inlineDragIds) {
|
|
3607
|
+
return {
|
|
3608
|
+
type: JSON_TYPES.PAIR_MATCHER,
|
|
3609
|
+
topItems: mapMatcherItemList(pm.topItems, idGenerator, inlineDragIds),
|
|
3610
|
+
bottomItems: mapMatcherItemList(pm.bottomItems, idGenerator, inlineDragIds)
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
function transformVerticalOperation(vop, idGenerator) {
|
|
3614
|
+
const anyRowHasOp = vop.items.some(
|
|
3615
|
+
(item) => item.type === AST_TYPES.VOP_ROW && item.op !== void 0
|
|
3616
|
+
);
|
|
3617
|
+
if (!anyRowHasOp && vop.op === void 0) {
|
|
3618
|
+
throw new Error("VERTICAL_OPERATION: at least one op is required (set op on the block or on individual ROWs)");
|
|
3619
|
+
}
|
|
3620
|
+
let cols = 1;
|
|
3621
|
+
for (const item of vop.items) {
|
|
3622
|
+
if (item.type === AST_TYPES.VOP_ROW || item.type === AST_TYPES.VOP_CARRY_ROW) {
|
|
3623
|
+
cols = Math.max(cols, item.cells.length);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
function buildCells(row) {
|
|
3627
|
+
return row.cells.map((cell) => {
|
|
3628
|
+
const id = idGenerator.generate(JSON_TYPES.VOP_CELL);
|
|
3629
|
+
if (cell.kind === "digit") return { id, type: JSON_TYPES.VOP_CELL, kind: "digit", value: cell.value };
|
|
3630
|
+
if (cell.kind === "label") return { id, type: JSON_TYPES.VOP_CELL, kind: "label", value: cell.value };
|
|
3631
|
+
if (cell.kind === "color") return { id, type: JSON_TYPES.VOP_CELL, kind: "color", value: cell.value, color: cell.color };
|
|
3632
|
+
if (cell.kind === "bold") return { id, type: JSON_TYPES.VOP_CELL, kind: "bold", value: cell.value };
|
|
3633
|
+
if (cell.kind === "highlight") return { id, type: JSON_TYPES.VOP_CELL, kind: "highlight", value: cell.value };
|
|
3634
|
+
if (cell.kind === "input") return { id, type: JSON_TYPES.VOP_CELL, kind: "input", widgetId: cell.name };
|
|
3635
|
+
return { id, type: JSON_TYPES.VOP_CELL, kind: "empty" };
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
const items = vop.items.map((item) => {
|
|
3639
|
+
if (item.type === AST_TYPES.VOP_SEPARATOR) {
|
|
3640
|
+
return {
|
|
3641
|
+
id: idGenerator.generate(JSON_TYPES.VOP_SEPARATOR),
|
|
3642
|
+
type: JSON_TYPES.VOP_SEPARATOR
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
if (item.type === AST_TYPES.VOP_CARRY_ROW) {
|
|
3646
|
+
return {
|
|
3647
|
+
id: idGenerator.generate(JSON_TYPES.VOP_CARRY_ROW),
|
|
3648
|
+
type: JSON_TYPES.VOP_CARRY_ROW,
|
|
3649
|
+
cells: buildCells(item)
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
const row = item;
|
|
3653
|
+
return {
|
|
3654
|
+
id: idGenerator.generate(JSON_TYPES.VOP_ROW),
|
|
3655
|
+
type: JSON_TYPES.VOP_ROW,
|
|
3656
|
+
cells: buildCells(row),
|
|
3657
|
+
...row.op !== void 0 && { op: row.op }
|
|
3658
|
+
};
|
|
3659
|
+
});
|
|
3660
|
+
return {
|
|
3661
|
+
id: idGenerator.generate(JSON_TYPES.VERTICAL_OPERATION),
|
|
3662
|
+
type: JSON_TYPES.VERTICAL_OPERATION,
|
|
3663
|
+
op: vop.op,
|
|
3664
|
+
cols,
|
|
3665
|
+
items
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
function transformTable(table, idGenerator, inlineDragIds) {
|
|
2776
3669
|
const rows = table.rows.map((row) => {
|
|
2777
3670
|
const cells = row.cells.map((cell) => {
|
|
2778
|
-
const items = cell.inlines.map((inline) => transformInline(inline, idGenerator));
|
|
3671
|
+
const items = cell.inlines.map((inline) => transformInline(inline, idGenerator, inlineDragIds));
|
|
2779
3672
|
const cellItem = {
|
|
2780
3673
|
id: idGenerator.generate(JSON_TYPES.TABLE_CELL),
|
|
2781
3674
|
type: JSON_TYPES.TABLE_CELL,
|
|
@@ -2799,10 +3692,10 @@ function transformTable(table, idGenerator) {
|
|
|
2799
3692
|
rows
|
|
2800
3693
|
};
|
|
2801
3694
|
}
|
|
2802
|
-
function transformLine(line, idGenerator) {
|
|
3695
|
+
function transformLine(line, idGenerator, inlineDragIds) {
|
|
2803
3696
|
const items = [];
|
|
2804
3697
|
for (const inline of line.inlines) {
|
|
2805
|
-
items.push(transformInline(inline, idGenerator));
|
|
3698
|
+
items.push(transformInline(inline, idGenerator, inlineDragIds));
|
|
2806
3699
|
}
|
|
2807
3700
|
items.push({
|
|
2808
3701
|
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
@@ -2810,7 +3703,7 @@ function transformLine(line, idGenerator) {
|
|
|
2810
3703
|
});
|
|
2811
3704
|
return items;
|
|
2812
3705
|
}
|
|
2813
|
-
function transformInline(inline, idGenerator) {
|
|
3706
|
+
function transformInline(inline, idGenerator, inlineDragIds) {
|
|
2814
3707
|
switch (inline.type) {
|
|
2815
3708
|
case AST_TYPES.FREE_TEXT:
|
|
2816
3709
|
const trimmed = inline.value.trim();
|
|
@@ -2882,6 +3775,12 @@ function transformInline(inline, idGenerator) {
|
|
|
2882
3775
|
type: JSON_TYPES.WIDGET_REF,
|
|
2883
3776
|
widgetId: inline.name
|
|
2884
3777
|
};
|
|
3778
|
+
case AST_TYPES.INLINE_DRAG_OPTION:
|
|
3779
|
+
return {
|
|
3780
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
3781
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
3782
|
+
widgetId: inlineDragIds.get(inline)
|
|
3783
|
+
};
|
|
2885
3784
|
default:
|
|
2886
3785
|
throw new Error(`Unknown inline type: ${inline.type}`);
|
|
2887
3786
|
}
|
|
@@ -2900,6 +3799,32 @@ function transformValidation(validation, answers, widgets) {
|
|
|
2900
3799
|
expression: validation.expression,
|
|
2901
3800
|
variables
|
|
2902
3801
|
};
|
|
3802
|
+
} else if (validation.type === AST_TYPES.MIXED_VALIDATION) {
|
|
3803
|
+
const validVariableNames = Object.keys(widgets);
|
|
3804
|
+
const variables = extractVariables$1(validation.expression, validVariableNames);
|
|
3805
|
+
const inExpression = new Set(variables);
|
|
3806
|
+
for (const [name, widget] of Object.entries(widgets)) {
|
|
3807
|
+
if (name.startsWith("_")) continue;
|
|
3808
|
+
if (name in answers) continue;
|
|
3809
|
+
if (inExpression.has(name)) continue;
|
|
3810
|
+
if (widget.type === JSON_TYPES.FRACTION) {
|
|
3811
|
+
const w = widget;
|
|
3812
|
+
if (w.numerator !== null && w.denominator !== null) continue;
|
|
3813
|
+
}
|
|
3814
|
+
if (widget.type === JSON_TYPES.VISUAL_FRACTION) {
|
|
3815
|
+
const w = widget;
|
|
3816
|
+
if (Array.isArray(w.filled) && w.filled.length > 0) continue;
|
|
3817
|
+
}
|
|
3818
|
+
throw new Error(
|
|
3819
|
+
`Mixed validation: widget "${name}" has no \`answer\` and is not referenced in expression "${validation.expression}"`
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
return {
|
|
3823
|
+
mode: VALIDATION_MODES.MIXED,
|
|
3824
|
+
expression: validation.expression,
|
|
3825
|
+
answers,
|
|
3826
|
+
variables
|
|
3827
|
+
};
|
|
2903
3828
|
}
|
|
2904
3829
|
throw new Error(`Unknown validation type: ${validation.type}`);
|
|
2905
3830
|
}
|
|
@@ -8424,6 +9349,19 @@ function areMatcherEqual(expected, result) {
|
|
|
8424
9349
|
const normalize2 = (groups) => lodashExports.sortBy(groups.map((g) => lodashExports.sortBy(g)), (g) => g.join("|"));
|
|
8425
9350
|
return lodashExports.isEqual(normalize2(expected), normalize2(result));
|
|
8426
9351
|
}
|
|
9352
|
+
function getPairMatcherGroupResults(expected, result) {
|
|
9353
|
+
if (!Array.isArray(result)) return [];
|
|
9354
|
+
const normalizeGroup = (g) => lodashExports.sortBy(g).join("|");
|
|
9355
|
+
const expectedKeys = new Set(expected.map(normalizeGroup));
|
|
9356
|
+
return result.map((group) => {
|
|
9357
|
+
const key2 = normalizeGroup(group);
|
|
9358
|
+
if (expectedKeys.has(key2)) {
|
|
9359
|
+
expectedKeys.delete(key2);
|
|
9360
|
+
return { group, isCorrect: true };
|
|
9361
|
+
}
|
|
9362
|
+
return { group, isCorrect: false };
|
|
9363
|
+
});
|
|
9364
|
+
}
|
|
8427
9365
|
function areVisualFractionCountsEqual(expected, result) {
|
|
8428
9366
|
if (!Array.isArray(result)) return false;
|
|
8429
9367
|
if (expected.length !== result.length) return false;
|
|
@@ -8462,7 +9400,8 @@ function validateSimple(exercise, userInputs) {
|
|
|
8462
9400
|
return {
|
|
8463
9401
|
isCorrect: false,
|
|
8464
9402
|
details: {},
|
|
8465
|
-
score: 0
|
|
9403
|
+
score: 0,
|
|
9404
|
+
error: "No answers defined for simple validation"
|
|
8466
9405
|
};
|
|
8467
9406
|
}
|
|
8468
9407
|
const positionMap = buildPositionMap(exercise);
|
|
@@ -8477,6 +9416,21 @@ function validateSimple(exercise, userInputs) {
|
|
|
8477
9416
|
else allCorrect = false;
|
|
8478
9417
|
continue;
|
|
8479
9418
|
}
|
|
9419
|
+
if (widget?.type === JSON_TYPES.PAIR_MATCHER) {
|
|
9420
|
+
const userValue2 = userInputs[widgetId];
|
|
9421
|
+
const groupResults = getPairMatcherGroupResults(expected, userValue2);
|
|
9422
|
+
const isCorrect2 = groupResults.length === expected.length && groupResults.every((r) => r.isCorrect);
|
|
9423
|
+
details[widgetId] = {
|
|
9424
|
+
widgetId,
|
|
9425
|
+
userValue: userValue2,
|
|
9426
|
+
isCorrect: isCorrect2,
|
|
9427
|
+
groupResults
|
|
9428
|
+
};
|
|
9429
|
+
totalCount++;
|
|
9430
|
+
if (isCorrect2) correctCount++;
|
|
9431
|
+
else allCorrect = false;
|
|
9432
|
+
continue;
|
|
9433
|
+
}
|
|
8480
9434
|
if (widget?.type === JSON_TYPES.VISUAL_FRACTION) {
|
|
8481
9435
|
const userValue2 = userInputs[widgetId];
|
|
8482
9436
|
const isCorrect2 = areVisualFractionCountsEqual(expected, userValue2);
|
|
@@ -8791,9 +9745,58 @@ function parseFractionOrNumber(s) {
|
|
|
8791
9745
|
if (Number.isFinite(num) && Number.isFinite(den) && den !== 0) {
|
|
8792
9746
|
return num / den;
|
|
8793
9747
|
}
|
|
8794
|
-
return NaN;
|
|
9748
|
+
return NaN;
|
|
9749
|
+
}
|
|
9750
|
+
return parseFloat(s);
|
|
9751
|
+
}
|
|
9752
|
+
function validateMixed(exercise, userInputs) {
|
|
9753
|
+
const answers = exercise.validation.answers ?? {};
|
|
9754
|
+
const simpleResult = validateSimple(exercise, userInputs);
|
|
9755
|
+
const scalarInputs = {};
|
|
9756
|
+
for (const [k, v] of Object.entries(userInputs)) {
|
|
9757
|
+
if (typeof v === "string" || typeof v === "number") scalarInputs[k] = v;
|
|
9758
|
+
}
|
|
9759
|
+
const exprResult = validateExpression(exercise, scalarInputs);
|
|
9760
|
+
const details = {};
|
|
9761
|
+
const answerKeys = new Set(Object.keys(answers));
|
|
9762
|
+
for (const k of answerKeys) {
|
|
9763
|
+
const simpleDetail = simpleResult.details[k];
|
|
9764
|
+
if (!simpleDetail) continue;
|
|
9765
|
+
const inExpr = k in exprResult.details;
|
|
9766
|
+
details[k] = {
|
|
9767
|
+
...simpleDetail,
|
|
9768
|
+
simpleCorrect: simpleDetail.isCorrect,
|
|
9769
|
+
...inExpr && { expressionCorrect: exprResult.isCorrect },
|
|
9770
|
+
isCorrect: inExpr ? simpleDetail.isCorrect && exprResult.isCorrect : simpleDetail.isCorrect
|
|
9771
|
+
};
|
|
9772
|
+
}
|
|
9773
|
+
for (const [k, detail] of Object.entries(exprResult.details)) {
|
|
9774
|
+
if (!(k in details)) {
|
|
9775
|
+
details[k] = {
|
|
9776
|
+
...detail,
|
|
9777
|
+
expressionCorrect: exprResult.isCorrect
|
|
9778
|
+
};
|
|
9779
|
+
}
|
|
8795
9780
|
}
|
|
8796
|
-
|
|
9781
|
+
const simpleCount = Object.keys(answers).length;
|
|
9782
|
+
let simpleCorrect = 0;
|
|
9783
|
+
for (const k of Object.keys(answers)) {
|
|
9784
|
+
if (simpleResult.details[k]?.isCorrect) simpleCorrect++;
|
|
9785
|
+
}
|
|
9786
|
+
const score = (simpleCorrect + (exprResult.isCorrect ? 1 : 0)) / (simpleCount + 1);
|
|
9787
|
+
const isCorrect = simpleResult.isCorrect && exprResult.isCorrect;
|
|
9788
|
+
const result = {
|
|
9789
|
+
isCorrect,
|
|
9790
|
+
details,
|
|
9791
|
+
score,
|
|
9792
|
+
expression: exercise.validation.expression,
|
|
9793
|
+
scope: exprResult.scope
|
|
9794
|
+
};
|
|
9795
|
+
const propagatedError = exprResult.error ?? simpleResult.error;
|
|
9796
|
+
if (!isCorrect && propagatedError) {
|
|
9797
|
+
result.error = propagatedError;
|
|
9798
|
+
}
|
|
9799
|
+
return result;
|
|
8797
9800
|
}
|
|
8798
9801
|
function preprocessFractionValues(exercise, userInputs) {
|
|
8799
9802
|
const result = { ...userInputs };
|
|
@@ -8837,6 +9840,8 @@ function validate(exercise, userInputs) {
|
|
|
8837
9840
|
}
|
|
8838
9841
|
return validateExpression(validatedExercise, scalar);
|
|
8839
9842
|
}
|
|
9843
|
+
case VALIDATION_MODES.MIXED:
|
|
9844
|
+
return validateMixed(validatedExercise, processedInputs);
|
|
8840
9845
|
default:
|
|
8841
9846
|
return {
|
|
8842
9847
|
isCorrect: false,
|
|
@@ -8850,18 +9855,27 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
8850
9855
|
if (!exercise.validation) {
|
|
8851
9856
|
return missing;
|
|
8852
9857
|
}
|
|
9858
|
+
const isEmpty = (v) => v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
|
|
8853
9859
|
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
8854
9860
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
8855
|
-
if (!(widgetId in userInputs) || userInputs[widgetId]
|
|
9861
|
+
if (!(widgetId in userInputs) || isEmpty(userInputs[widgetId])) {
|
|
8856
9862
|
missing.push(widgetId);
|
|
8857
9863
|
}
|
|
8858
9864
|
}
|
|
8859
9865
|
} else if (exercise.validation.mode === VALIDATION_MODES.EXPRESSION && exercise.validation.variables) {
|
|
8860
9866
|
for (const varName of exercise.validation.variables) {
|
|
8861
|
-
if (!(varName in userInputs) || userInputs[varName]
|
|
9867
|
+
if (!(varName in userInputs) || isEmpty(userInputs[varName])) {
|
|
8862
9868
|
missing.push(varName);
|
|
8863
9869
|
}
|
|
8864
9870
|
}
|
|
9871
|
+
} else if (exercise.validation.mode === VALIDATION_MODES.MIXED) {
|
|
9872
|
+
const keys = /* @__PURE__ */ new Set([
|
|
9873
|
+
...Object.keys(exercise.validation.answers ?? {}),
|
|
9874
|
+
...exercise.validation.variables ?? []
|
|
9875
|
+
]);
|
|
9876
|
+
for (const k of keys) {
|
|
9877
|
+
if (!(k in userInputs) || isEmpty(userInputs[k])) missing.push(k);
|
|
9878
|
+
}
|
|
8865
9879
|
}
|
|
8866
9880
|
return missing;
|
|
8867
9881
|
}
|
|
@@ -12931,7 +13945,7 @@ const InlineInput = ({
|
|
|
12931
13945
|
id: widgetId,
|
|
12932
13946
|
accepts: ["drag-option"],
|
|
12933
13947
|
onDrop: handleDrop,
|
|
12934
|
-
className: "inline-input-droppable",
|
|
13948
|
+
className: "inline-widget inline-input-droppable",
|
|
12935
13949
|
children: isDropped && value !== "" ? (
|
|
12936
13950
|
// Show as chip only when value came from drag-drop
|
|
12937
13951
|
/* @__PURE__ */ jsx(
|
|
@@ -12963,6 +13977,32 @@ const InlineInput = ({
|
|
|
12963
13977
|
}
|
|
12964
13978
|
);
|
|
12965
13979
|
};
|
|
13980
|
+
const SingleDragOption = ({
|
|
13981
|
+
optionId,
|
|
13982
|
+
value,
|
|
13983
|
+
onOptionClick,
|
|
13984
|
+
className = ""
|
|
13985
|
+
}) => {
|
|
13986
|
+
const { usedOptionIds } = useExerciseDnd();
|
|
13987
|
+
const isUsed = usedOptionIds.has(optionId);
|
|
13988
|
+
return /* @__PURE__ */ jsx("div", { className: `drag-option-slot ${className}`.trim(), children: isUsed ? /* @__PURE__ */ jsx("div", { className: "drag-option-empty", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(
|
|
13989
|
+
Draggable,
|
|
13990
|
+
{
|
|
13991
|
+
id: optionId,
|
|
13992
|
+
type: "drag-option",
|
|
13993
|
+
data: { value, label: value },
|
|
13994
|
+
className: "drag-option-wrapper",
|
|
13995
|
+
children: /* @__PURE__ */ jsx(
|
|
13996
|
+
"button",
|
|
13997
|
+
{
|
|
13998
|
+
className: "drag-option",
|
|
13999
|
+
onClick: () => onOptionClick?.(value),
|
|
14000
|
+
children: value
|
|
14001
|
+
}
|
|
14002
|
+
)
|
|
14003
|
+
}
|
|
14004
|
+
) });
|
|
14005
|
+
};
|
|
12966
14006
|
const PillBackground = ({
|
|
12967
14007
|
width = "100%",
|
|
12968
14008
|
minHeight = "72px",
|
|
@@ -13035,36 +14075,32 @@ const DragOptions = ({
|
|
|
13035
14075
|
options,
|
|
13036
14076
|
onOptionClick
|
|
13037
14077
|
}) => {
|
|
13038
|
-
const { usedOptionIds } = useExerciseDnd();
|
|
13039
14078
|
return /* @__PURE__ */ jsx(PillBackground, { children: /* @__PURE__ */ jsx("div", { className: "drag-options", children: options.map((option, idx) => {
|
|
13040
14079
|
const optionId = `drag-option-${idx}`;
|
|
13041
14080
|
const optionValue = String(option);
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
id: optionId,
|
|
13052
|
-
type: "drag-option",
|
|
13053
|
-
data: { value: optionValue, label: optionValue },
|
|
13054
|
-
className: "drag-option-wrapper",
|
|
13055
|
-
children: /* @__PURE__ */ jsx(
|
|
13056
|
-
"button",
|
|
13057
|
-
{
|
|
13058
|
-
className: "drag-option",
|
|
13059
|
-
onClick: () => onOptionClick?.(optionValue),
|
|
13060
|
-
children: optionValue
|
|
13061
|
-
}
|
|
13062
|
-
)
|
|
13063
|
-
}
|
|
13064
|
-
)
|
|
13065
|
-
) }, optionId);
|
|
14081
|
+
return /* @__PURE__ */ jsx(
|
|
14082
|
+
SingleDragOption,
|
|
14083
|
+
{
|
|
14084
|
+
optionId,
|
|
14085
|
+
value: optionValue,
|
|
14086
|
+
onOptionClick
|
|
14087
|
+
},
|
|
14088
|
+
optionId
|
|
14089
|
+
);
|
|
13066
14090
|
}) }) });
|
|
13067
14091
|
};
|
|
14092
|
+
const InlineDragOption = ({
|
|
14093
|
+
widgetId,
|
|
14094
|
+
widget
|
|
14095
|
+
}) => {
|
|
14096
|
+
return /* @__PURE__ */ jsx("div", { className: "inline-widget", children: /* @__PURE__ */ jsx(
|
|
14097
|
+
SingleDragOption,
|
|
14098
|
+
{
|
|
14099
|
+
optionId: widgetId,
|
|
14100
|
+
value: widget.value
|
|
14101
|
+
}
|
|
14102
|
+
) });
|
|
14103
|
+
};
|
|
13068
14104
|
const NumericKeypad = ({
|
|
13069
14105
|
value,
|
|
13070
14106
|
includeOperators = false,
|
|
@@ -13159,7 +14195,7 @@ const FractionInput = ({
|
|
|
13159
14195
|
}
|
|
13160
14196
|
);
|
|
13161
14197
|
};
|
|
13162
|
-
return /* @__PURE__ */ jsxs("span", { className: "fraction", role: "math", "aria-label": `fraction ${widgetId}`, children: [
|
|
14198
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-widget fraction", role: "math", "aria-label": `fraction ${widgetId}`, children: [
|
|
13163
14199
|
/* @__PURE__ */ jsx("span", { className: "fraction-numerator", children: renderSlot(numKey, numerator, numValue, "numerator", numValidationStatus) }),
|
|
13164
14200
|
/* @__PURE__ */ jsx("span", { className: "fraction-bar", "aria-hidden": "true" }),
|
|
13165
14201
|
/* @__PURE__ */ jsx("span", { className: "fraction-denominator", children: renderSlot(denKey, denominator, denValue, "denominator", denValidationStatus) })
|
|
@@ -13591,7 +14627,7 @@ const VisualFraction = ({
|
|
|
13591
14627
|
return /* @__PURE__ */ jsxs(
|
|
13592
14628
|
"div",
|
|
13593
14629
|
{
|
|
13594
|
-
className: `visual-fraction-widget visual-fraction-widget--${shape}${statusClass}`,
|
|
14630
|
+
className: `inline-widget visual-fraction-widget visual-fraction-widget--${shape}${statusClass}`,
|
|
13595
14631
|
style: rootStyle,
|
|
13596
14632
|
role: "group",
|
|
13597
14633
|
"aria-label": `${divisions} part ${shape} fraction`,
|
|
@@ -13695,10 +14731,10 @@ function buildGroups(assignments) {
|
|
|
13695
14731
|
}
|
|
13696
14732
|
return Array.from(map.values());
|
|
13697
14733
|
}
|
|
13698
|
-
const EMPTY_WIDGETS = {};
|
|
13699
|
-
const EMPTY_VALUES = {};
|
|
13700
|
-
const EMPTY_DROPPED = /* @__PURE__ */ new Set();
|
|
13701
|
-
const NOOP = () => {
|
|
14734
|
+
const EMPTY_WIDGETS$1 = {};
|
|
14735
|
+
const EMPTY_VALUES$1 = {};
|
|
14736
|
+
const EMPTY_DROPPED$1 = /* @__PURE__ */ new Set();
|
|
14737
|
+
const NOOP$1 = () => {
|
|
13702
14738
|
};
|
|
13703
14739
|
const MatcherWidget = ({
|
|
13704
14740
|
widgetId,
|
|
@@ -13709,7 +14745,7 @@ const MatcherWidget = ({
|
|
|
13709
14745
|
widgets
|
|
13710
14746
|
}) => {
|
|
13711
14747
|
const { rows, colors: userColors, groupCount } = widget;
|
|
13712
|
-
const widgetsMap = widgets ?? EMPTY_WIDGETS;
|
|
14748
|
+
const widgetsMap = widgets ?? EMPTY_WIDGETS$1;
|
|
13713
14749
|
const palette = useMemo(
|
|
13714
14750
|
() => resolveGroupPalette(userColors, groupCount),
|
|
13715
14751
|
[userColors, groupCount]
|
|
@@ -13774,9 +14810,9 @@ const MatcherWidget = ({
|
|
|
13774
14810
|
{
|
|
13775
14811
|
item: layoutItem,
|
|
13776
14812
|
widgets: widgetsMap,
|
|
13777
|
-
values: EMPTY_VALUES,
|
|
13778
|
-
droppedInputs: EMPTY_DROPPED,
|
|
13779
|
-
onChange: NOOP
|
|
14813
|
+
values: EMPTY_VALUES$1,
|
|
14814
|
+
droppedInputs: EMPTY_DROPPED$1,
|
|
14815
|
+
onChange: NOOP$1
|
|
13780
14816
|
},
|
|
13781
14817
|
layoutItem.id
|
|
13782
14818
|
))
|
|
@@ -13788,12 +14824,433 @@ const MatcherWidget = ({
|
|
|
13788
14824
|
);
|
|
13789
14825
|
};
|
|
13790
14826
|
MatcherWidget.displayName = "MatcherWidget";
|
|
14827
|
+
function isArabic(text) {
|
|
14828
|
+
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
14829
|
+
}
|
|
14830
|
+
function hasLatinChars(text) {
|
|
14831
|
+
return /[A-Za-z]/.test(text);
|
|
14832
|
+
}
|
|
14833
|
+
function itemContainsArabic(item) {
|
|
14834
|
+
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
14835
|
+
return isArabic(item.value);
|
|
14836
|
+
}
|
|
14837
|
+
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
14838
|
+
return isArabic(item.value);
|
|
14839
|
+
}
|
|
14840
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
14841
|
+
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
14842
|
+
}
|
|
14843
|
+
return false;
|
|
14844
|
+
}
|
|
14845
|
+
function itemContainsLatin(item) {
|
|
14846
|
+
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
14847
|
+
return hasLatinChars(item.value);
|
|
14848
|
+
}
|
|
14849
|
+
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
14850
|
+
return hasLatinChars(item.value);
|
|
14851
|
+
}
|
|
14852
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
14853
|
+
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
14854
|
+
}
|
|
14855
|
+
return false;
|
|
14856
|
+
}
|
|
14857
|
+
function shouldItemsUseRTL(items) {
|
|
14858
|
+
const hasArabic = items.some(itemContainsArabic);
|
|
14859
|
+
const hasLatin = items.some(itemContainsLatin);
|
|
14860
|
+
return hasArabic && !hasLatin;
|
|
14861
|
+
}
|
|
14862
|
+
function usePairLines({
|
|
14863
|
+
containerRef,
|
|
14864
|
+
pairs,
|
|
14865
|
+
lineDirections,
|
|
14866
|
+
validationStatus,
|
|
14867
|
+
pairStatusByTopId
|
|
14868
|
+
}) {
|
|
14869
|
+
const dotRefs = useRef(/* @__PURE__ */ new Map());
|
|
14870
|
+
const [lines, setLines] = useState([]);
|
|
14871
|
+
const recalcLines = useCallback(() => {
|
|
14872
|
+
const container = containerRef.current;
|
|
14873
|
+
if (!container) return;
|
|
14874
|
+
const cr = container.getBoundingClientRect();
|
|
14875
|
+
const newLines = [];
|
|
14876
|
+
for (const [topId, bottomId] of Object.entries(pairs)) {
|
|
14877
|
+
const topDot = dotRefs.current.get(`top:${topId}`);
|
|
14878
|
+
const bottomDot = dotRefs.current.get(`bottom:${bottomId}`);
|
|
14879
|
+
if (!topDot || !bottomDot) continue;
|
|
14880
|
+
const tr = topDot.getBoundingClientRect();
|
|
14881
|
+
const br = bottomDot.getBoundingClientRect();
|
|
14882
|
+
const status = validationStatus ? pairStatusByTopId[topId] ?? (validationStatus === "correct" ? "correct" : "incorrect") : "idle";
|
|
14883
|
+
const direction = lineDirections[topId] ?? "top-to-bottom";
|
|
14884
|
+
const startRect = direction === "top-to-bottom" ? tr : br;
|
|
14885
|
+
const endRect = direction === "top-to-bottom" ? br : tr;
|
|
14886
|
+
newLines.push({
|
|
14887
|
+
topId,
|
|
14888
|
+
x1: startRect.left - cr.left + startRect.width / 2,
|
|
14889
|
+
y1: startRect.top - cr.top + startRect.height / 2,
|
|
14890
|
+
x2: endRect.left - cr.left + endRect.width / 2,
|
|
14891
|
+
y2: endRect.top - cr.top + endRect.height / 2,
|
|
14892
|
+
status
|
|
14893
|
+
});
|
|
14894
|
+
}
|
|
14895
|
+
setLines(newLines);
|
|
14896
|
+
}, [pairs, lineDirections, validationStatus, pairStatusByTopId]);
|
|
14897
|
+
useLayoutEffect(() => {
|
|
14898
|
+
recalcLines();
|
|
14899
|
+
}, [recalcLines]);
|
|
14900
|
+
useEffect(() => {
|
|
14901
|
+
const el = containerRef.current;
|
|
14902
|
+
if (!el) return;
|
|
14903
|
+
const ro = new ResizeObserver(recalcLines);
|
|
14904
|
+
ro.observe(el);
|
|
14905
|
+
return () => ro.disconnect();
|
|
14906
|
+
}, [containerRef, recalcLines]);
|
|
14907
|
+
return { dotRefs, lines };
|
|
14908
|
+
}
|
|
14909
|
+
const EMPTY_WIDGETS = {};
|
|
14910
|
+
const EMPTY_VALUES = {};
|
|
14911
|
+
const EMPTY_DROPPED = /* @__PURE__ */ new Set();
|
|
14912
|
+
const NOOP = () => {
|
|
14913
|
+
};
|
|
14914
|
+
function buildInitialState(defaultValue2, topIds, bottomIds) {
|
|
14915
|
+
if (!defaultValue2?.length) return { pairs: {}, lineDirections: {} };
|
|
14916
|
+
const pairs = {};
|
|
14917
|
+
const lineDirections = {};
|
|
14918
|
+
for (const pair of defaultValue2) {
|
|
14919
|
+
if (!Array.isArray(pair) || pair.length !== 2 || pair[0] == null || pair[1] == null) continue;
|
|
14920
|
+
const [first, second] = pair;
|
|
14921
|
+
if (topIds.has(first) && bottomIds.has(second)) {
|
|
14922
|
+
pairs[first] = second;
|
|
14923
|
+
lineDirections[first] = "top-to-bottom";
|
|
14924
|
+
} else if (bottomIds.has(first) && topIds.has(second)) {
|
|
14925
|
+
pairs[second] = first;
|
|
14926
|
+
lineDirections[second] = "bottom-to-top";
|
|
14927
|
+
}
|
|
14928
|
+
}
|
|
14929
|
+
return { pairs, lineDirections };
|
|
14930
|
+
}
|
|
14931
|
+
function findTopForBottom(pairs, bottomId) {
|
|
14932
|
+
return Object.keys(pairs).find((topId) => pairs[topId] === bottomId);
|
|
14933
|
+
}
|
|
14934
|
+
function withLineDirection(lineDirections, pairs, topId, bottomId, direction) {
|
|
14935
|
+
const next = { ...lineDirections };
|
|
14936
|
+
delete next[topId];
|
|
14937
|
+
const existingTop = findTopForBottom(pairs, bottomId);
|
|
14938
|
+
if (existingTop !== void 0) delete next[existingTop];
|
|
14939
|
+
next[topId] = direction;
|
|
14940
|
+
return next;
|
|
14941
|
+
}
|
|
14942
|
+
function getPairIds(pair, topIds, bottomIds) {
|
|
14943
|
+
if (pair.length !== 2) return null;
|
|
14944
|
+
const first = pair[0];
|
|
14945
|
+
const second = pair[1];
|
|
14946
|
+
if (first === void 0 || second === void 0) return null;
|
|
14947
|
+
if (topIds.has(first) && bottomIds.has(second)) return { topId: first, bottomId: second };
|
|
14948
|
+
if (bottomIds.has(first) && topIds.has(second)) return { topId: second, bottomId: first };
|
|
14949
|
+
return null;
|
|
14950
|
+
}
|
|
14951
|
+
function withPair(pairs, topId, bottomId) {
|
|
14952
|
+
const next = { ...pairs };
|
|
14953
|
+
delete next[topId];
|
|
14954
|
+
const existingTop = findTopForBottom(next, bottomId);
|
|
14955
|
+
if (existingTop !== void 0) delete next[existingTop];
|
|
14956
|
+
next[topId] = bottomId;
|
|
14957
|
+
return next;
|
|
14958
|
+
}
|
|
14959
|
+
function resolveCardStatus(ownId, isTop, pairs, pending, validationStatus, pairStatus) {
|
|
14960
|
+
if (validationStatus) {
|
|
14961
|
+
const topId2 = isTop ? ownId : findTopForBottom(pairs, ownId);
|
|
14962
|
+
if (topId2 === void 0 || pairs[topId2] === void 0) return "idle";
|
|
14963
|
+
return pairStatus ?? (validationStatus === "correct" ? "correct" : "incorrect");
|
|
14964
|
+
}
|
|
14965
|
+
if (pending?.side === (isTop ? "top" : "bottom") && pending.id === ownId) return "pending";
|
|
14966
|
+
const topId = isTop ? ownId : findTopForBottom(pairs, ownId);
|
|
14967
|
+
if (topId !== void 0 && pairs[topId] !== void 0) return "paired";
|
|
14968
|
+
return "idle";
|
|
14969
|
+
}
|
|
14970
|
+
function buildDotClass(status, position) {
|
|
14971
|
+
const cls = ["pair-matcher-dot", `pair-matcher-dot--${position}`];
|
|
14972
|
+
if (status !== "idle") {
|
|
14973
|
+
cls.push("pair-matcher-dot--active");
|
|
14974
|
+
if (status !== "paired") cls.push(`pair-matcher-dot--${status}`);
|
|
14975
|
+
}
|
|
14976
|
+
return cls.join(" ");
|
|
14977
|
+
}
|
|
14978
|
+
function buildCardClass(status, isFlashing) {
|
|
14979
|
+
const cls = ["pair-matcher-card"];
|
|
14980
|
+
if (status !== "idle") cls.push(`pair-matcher-card--${status}`);
|
|
14981
|
+
if (isFlashing) cls.push("pair-matcher-card--flash");
|
|
14982
|
+
return cls.join(" ");
|
|
14983
|
+
}
|
|
14984
|
+
function lineStrokeVar(status) {
|
|
14985
|
+
if (status === "correct") return "var(--pm-line-color-correct)";
|
|
14986
|
+
if (status === "incorrect") return "var(--pm-line-color-incorrect)";
|
|
14987
|
+
return "var(--pm-line-color)";
|
|
14988
|
+
}
|
|
14989
|
+
function renderPairMatcherItemContent(item, widgets) {
|
|
14990
|
+
if (item.type === JSON_TYPES.SPACE) {
|
|
14991
|
+
return /* @__PURE__ */ jsx("span", { className: "space", children: " " }, item.id);
|
|
14992
|
+
}
|
|
14993
|
+
if (item.type === JSON_TYPES.GROUP) {
|
|
14994
|
+
return /* @__PURE__ */ jsx("span", { className: "group", children: item.items.map((subItem) => renderPairMatcherItemContent(subItem, widgets)) }, item.id);
|
|
14995
|
+
}
|
|
14996
|
+
return /* @__PURE__ */ jsx(
|
|
14997
|
+
LayoutRenderer,
|
|
14998
|
+
{
|
|
14999
|
+
item,
|
|
15000
|
+
widgets,
|
|
15001
|
+
values: EMPTY_VALUES,
|
|
15002
|
+
droppedInputs: EMPTY_DROPPED,
|
|
15003
|
+
onChange: NOOP
|
|
15004
|
+
},
|
|
15005
|
+
item.id
|
|
15006
|
+
);
|
|
15007
|
+
}
|
|
15008
|
+
const PairMatcherWidget = ({
|
|
15009
|
+
widgetId,
|
|
15010
|
+
widget,
|
|
15011
|
+
onChange,
|
|
15012
|
+
validationStatus = null,
|
|
15013
|
+
validationDetail,
|
|
15014
|
+
defaultValue: defaultValue2,
|
|
15015
|
+
widgets,
|
|
15016
|
+
shuffleItems = true
|
|
15017
|
+
}) => {
|
|
15018
|
+
const { topItems, bottomItems } = widget;
|
|
15019
|
+
const [shuffledTop] = useState(
|
|
15020
|
+
() => shuffleItems ? lodashExports.shuffle([...topItems]) : [...topItems]
|
|
15021
|
+
);
|
|
15022
|
+
const [shuffledBottom] = useState(
|
|
15023
|
+
() => shuffleItems ? lodashExports.shuffle([...bottomItems]) : [...bottomItems]
|
|
15024
|
+
);
|
|
15025
|
+
const widgetsMap = widgets ?? EMPTY_WIDGETS;
|
|
15026
|
+
const topIds = useMemo(() => new Set(topItems.map((item) => item.itemId)), [topItems]);
|
|
15027
|
+
const bottomIds = useMemo(() => new Set(bottomItems.map((item) => item.itemId)), [bottomItems]);
|
|
15028
|
+
const pairStatusByTopId = useMemo(() => {
|
|
15029
|
+
const statuses = {};
|
|
15030
|
+
for (const result of validationDetail?.groupResults ?? []) {
|
|
15031
|
+
const ids2 = getPairIds(result.group, topIds, bottomIds);
|
|
15032
|
+
if (ids2) statuses[ids2.topId] = result.isCorrect ? "correct" : "incorrect";
|
|
15033
|
+
}
|
|
15034
|
+
return statuses;
|
|
15035
|
+
}, [validationDetail?.groupResults, topIds, bottomIds]);
|
|
15036
|
+
const [state, setState] = useState(() => ({
|
|
15037
|
+
...buildInitialState(defaultValue2, topIds, bottomIds),
|
|
15038
|
+
pending: null,
|
|
15039
|
+
flashTopId: null,
|
|
15040
|
+
flashBottomId: null
|
|
15041
|
+
}));
|
|
15042
|
+
const flashTimerRef = useRef(null);
|
|
15043
|
+
const handleTopClick = useCallback((topId) => {
|
|
15044
|
+
if (validationStatus) return;
|
|
15045
|
+
if (flashTimerRef.current) {
|
|
15046
|
+
clearTimeout(flashTimerRef.current);
|
|
15047
|
+
flashTimerRef.current = null;
|
|
15048
|
+
}
|
|
15049
|
+
setState((prev) => {
|
|
15050
|
+
if (prev.pairs[topId] !== void 0) {
|
|
15051
|
+
const { [topId]: _, ...rest } = prev.pairs;
|
|
15052
|
+
const { [topId]: __, ...restDirections } = prev.lineDirections;
|
|
15053
|
+
if (prev.pending?.side === "bottom") {
|
|
15054
|
+
return {
|
|
15055
|
+
pairs: withPair(rest, topId, prev.pending.id),
|
|
15056
|
+
lineDirections: withLineDirection(restDirections, rest, topId, prev.pending.id, "bottom-to-top"),
|
|
15057
|
+
pending: null,
|
|
15058
|
+
flashTopId: topId,
|
|
15059
|
+
flashBottomId: prev.pending.id
|
|
15060
|
+
};
|
|
15061
|
+
}
|
|
15062
|
+
return {
|
|
15063
|
+
pairs: rest,
|
|
15064
|
+
lineDirections: restDirections,
|
|
15065
|
+
pending: { side: "top", id: topId },
|
|
15066
|
+
flashTopId: null,
|
|
15067
|
+
flashBottomId: null
|
|
15068
|
+
};
|
|
15069
|
+
}
|
|
15070
|
+
if (prev.pending?.side === "bottom") {
|
|
15071
|
+
return {
|
|
15072
|
+
pairs: withPair(prev.pairs, topId, prev.pending.id),
|
|
15073
|
+
lineDirections: withLineDirection(prev.lineDirections, prev.pairs, topId, prev.pending.id, "bottom-to-top"),
|
|
15074
|
+
pending: null,
|
|
15075
|
+
flashTopId: topId,
|
|
15076
|
+
flashBottomId: prev.pending.id
|
|
15077
|
+
};
|
|
15078
|
+
}
|
|
15079
|
+
return {
|
|
15080
|
+
...prev,
|
|
15081
|
+
pending: prev.pending?.side === "top" && prev.pending.id === topId ? null : { side: "top", id: topId },
|
|
15082
|
+
flashTopId: null,
|
|
15083
|
+
flashBottomId: null
|
|
15084
|
+
};
|
|
15085
|
+
});
|
|
15086
|
+
}, [validationStatus]);
|
|
15087
|
+
const handleBottomClick = useCallback((bottomId) => {
|
|
15088
|
+
if (validationStatus) return;
|
|
15089
|
+
if (flashTimerRef.current) {
|
|
15090
|
+
clearTimeout(flashTimerRef.current);
|
|
15091
|
+
flashTimerRef.current = null;
|
|
15092
|
+
}
|
|
15093
|
+
setState((prev) => {
|
|
15094
|
+
const existingTop = findTopForBottom(prev.pairs, bottomId);
|
|
15095
|
+
if (existingTop !== void 0) {
|
|
15096
|
+
const { [existingTop]: _, ...rest } = prev.pairs;
|
|
15097
|
+
const { [existingTop]: __, ...restDirections } = prev.lineDirections;
|
|
15098
|
+
if (prev.pending?.side === "top" && prev.pending.id !== existingTop) {
|
|
15099
|
+
return {
|
|
15100
|
+
pairs: withPair(rest, prev.pending.id, bottomId),
|
|
15101
|
+
lineDirections: withLineDirection(restDirections, rest, prev.pending.id, bottomId, "top-to-bottom"),
|
|
15102
|
+
pending: null,
|
|
15103
|
+
flashTopId: prev.pending.id,
|
|
15104
|
+
flashBottomId: bottomId
|
|
15105
|
+
};
|
|
15106
|
+
}
|
|
15107
|
+
return {
|
|
15108
|
+
pairs: rest,
|
|
15109
|
+
lineDirections: restDirections,
|
|
15110
|
+
pending: { side: "bottom", id: bottomId },
|
|
15111
|
+
flashTopId: null,
|
|
15112
|
+
flashBottomId: null
|
|
15113
|
+
};
|
|
15114
|
+
}
|
|
15115
|
+
if (prev.pending?.side === "top") {
|
|
15116
|
+
return {
|
|
15117
|
+
pairs: withPair(prev.pairs, prev.pending.id, bottomId),
|
|
15118
|
+
lineDirections: withLineDirection(prev.lineDirections, prev.pairs, prev.pending.id, bottomId, "top-to-bottom"),
|
|
15119
|
+
pending: null,
|
|
15120
|
+
flashTopId: prev.pending.id,
|
|
15121
|
+
flashBottomId: bottomId
|
|
15122
|
+
};
|
|
15123
|
+
}
|
|
15124
|
+
return {
|
|
15125
|
+
...prev,
|
|
15126
|
+
pending: prev.pending?.side === "bottom" && prev.pending.id === bottomId ? null : { side: "bottom", id: bottomId },
|
|
15127
|
+
flashTopId: null,
|
|
15128
|
+
flashBottomId: null
|
|
15129
|
+
};
|
|
15130
|
+
});
|
|
15131
|
+
}, [validationStatus]);
|
|
15132
|
+
useEffect(() => {
|
|
15133
|
+
if (validationStatus || state.flashTopId === null || state.flashBottomId === null) return;
|
|
15134
|
+
flashTimerRef.current = setTimeout(() => {
|
|
15135
|
+
flashTimerRef.current = null;
|
|
15136
|
+
setState((current) => {
|
|
15137
|
+
if (current.flashTopId === null && current.flashBottomId === null) return current;
|
|
15138
|
+
return { ...current, flashTopId: null, flashBottomId: null };
|
|
15139
|
+
});
|
|
15140
|
+
}, 650);
|
|
15141
|
+
return () => {
|
|
15142
|
+
if (flashTimerRef.current) {
|
|
15143
|
+
clearTimeout(flashTimerRef.current);
|
|
15144
|
+
flashTimerRef.current = null;
|
|
15145
|
+
}
|
|
15146
|
+
};
|
|
15147
|
+
}, [state.flashTopId, state.flashBottomId, validationStatus]);
|
|
15148
|
+
const onChangeRef = useRef(onChange);
|
|
15149
|
+
onChangeRef.current = onChange;
|
|
15150
|
+
const isMounted = useRef(false);
|
|
15151
|
+
useEffect(() => {
|
|
15152
|
+
if (!isMounted.current) {
|
|
15153
|
+
isMounted.current = true;
|
|
15154
|
+
return;
|
|
15155
|
+
}
|
|
15156
|
+
const value = Object.entries(state.pairs).map(
|
|
15157
|
+
([topId, bottomId]) => state.lineDirections[topId] === "bottom-to-top" ? [bottomId, topId] : [topId, bottomId]
|
|
15158
|
+
);
|
|
15159
|
+
onChangeRef.current(widgetId, value);
|
|
15160
|
+
}, [state.pairs, state.lineDirections, widgetId]);
|
|
15161
|
+
const containerRef = useRef(null);
|
|
15162
|
+
const { dotRefs, lines } = usePairLines({
|
|
15163
|
+
containerRef,
|
|
15164
|
+
pairs: state.pairs,
|
|
15165
|
+
lineDirections: state.lineDirections,
|
|
15166
|
+
validationStatus,
|
|
15167
|
+
pairStatusByTopId
|
|
15168
|
+
});
|
|
15169
|
+
const cols = useMemo(
|
|
15170
|
+
() => Math.max(topItems.length, bottomItems.length, 1),
|
|
15171
|
+
[topItems.length, bottomItems.length]
|
|
15172
|
+
);
|
|
15173
|
+
const renderCard = (item, isTop) => {
|
|
15174
|
+
const isRtl = shouldItemsUseRTL(item.content);
|
|
15175
|
+
const pairedTopId = isTop ? item.itemId : findTopForBottom(state.pairs, item.itemId);
|
|
15176
|
+
const pairStatus = pairedTopId ? pairStatusByTopId[pairedTopId] ?? null : null;
|
|
15177
|
+
const status = resolveCardStatus(
|
|
15178
|
+
item.itemId,
|
|
15179
|
+
isTop,
|
|
15180
|
+
state.pairs,
|
|
15181
|
+
state.pending,
|
|
15182
|
+
validationStatus,
|
|
15183
|
+
pairStatus
|
|
15184
|
+
);
|
|
15185
|
+
const isFlashing = isTop ? state.flashTopId === item.itemId : state.flashBottomId === item.itemId;
|
|
15186
|
+
const dotKey = isTop ? `top:${item.itemId}` : `bottom:${item.itemId}`;
|
|
15187
|
+
const dot = /* @__PURE__ */ jsx(
|
|
15188
|
+
"span",
|
|
15189
|
+
{
|
|
15190
|
+
className: buildDotClass(status, isTop ? "south" : "north"),
|
|
15191
|
+
ref: (el) => {
|
|
15192
|
+
if (el) dotRefs.current.set(dotKey, el);
|
|
15193
|
+
else dotRefs.current.delete(dotKey);
|
|
15194
|
+
}
|
|
15195
|
+
}
|
|
15196
|
+
);
|
|
15197
|
+
return /* @__PURE__ */ jsxs("div", { className: "pair-matcher-cell", children: [
|
|
15198
|
+
!isTop && dot,
|
|
15199
|
+
/* @__PURE__ */ jsx(
|
|
15200
|
+
"button",
|
|
15201
|
+
{
|
|
15202
|
+
type: "button",
|
|
15203
|
+
className: `${buildCardClass(status, isFlashing)}${isRtl ? " pair-matcher-card--rtl" : ""}`,
|
|
15204
|
+
dir: isRtl ? "rtl" : void 0,
|
|
15205
|
+
onClick: () => isTop ? handleTopClick(item.itemId) : handleBottomClick(item.itemId),
|
|
15206
|
+
"aria-pressed": status !== "idle",
|
|
15207
|
+
"aria-disabled": validationStatus ? "true" : void 0,
|
|
15208
|
+
children: /* @__PURE__ */ jsx("span", { className: "pair-matcher-card-content", children: item.content.map((li) => renderPairMatcherItemContent(li, widgetsMap)) })
|
|
15209
|
+
}
|
|
15210
|
+
),
|
|
15211
|
+
isTop && dot
|
|
15212
|
+
] }, item.id);
|
|
15213
|
+
};
|
|
15214
|
+
return /* @__PURE__ */ jsxs(
|
|
15215
|
+
"div",
|
|
15216
|
+
{
|
|
15217
|
+
className: `pair-matcher-widget${validationStatus ? ` pair-matcher-widget--${validationStatus}` : ""}`,
|
|
15218
|
+
style: { "--pm-cols": cols },
|
|
15219
|
+
ref: containerRef,
|
|
15220
|
+
role: "group",
|
|
15221
|
+
"aria-label": "Pair matcher exercise",
|
|
15222
|
+
children: [
|
|
15223
|
+
/* @__PURE__ */ jsx("div", { className: "pair-matcher-row pair-matcher-row--top", children: shuffledTop.map((item) => renderCard(item, true)) }),
|
|
15224
|
+
/* @__PURE__ */ jsx("svg", { className: "pair-matcher-lines", "aria-hidden": "true", children: lines.map((line) => /* @__PURE__ */ jsx(
|
|
15225
|
+
"line",
|
|
15226
|
+
{
|
|
15227
|
+
x1: line.x1,
|
|
15228
|
+
y1: line.y1,
|
|
15229
|
+
x2: line.x2,
|
|
15230
|
+
y2: line.y2,
|
|
15231
|
+
stroke: lineStrokeVar(line.status),
|
|
15232
|
+
strokeWidth: "var(--pm-line-width, 4)",
|
|
15233
|
+
strokeLinecap: "round",
|
|
15234
|
+
pathLength: 1,
|
|
15235
|
+
className: "pair-matcher-line"
|
|
15236
|
+
},
|
|
15237
|
+
line.topId
|
|
15238
|
+
)) }),
|
|
15239
|
+
/* @__PURE__ */ jsx("div", { className: "pair-matcher-row pair-matcher-row--bottom", children: shuffledBottom.map((item) => renderCard(item, false)) })
|
|
15240
|
+
]
|
|
15241
|
+
}
|
|
15242
|
+
);
|
|
15243
|
+
};
|
|
15244
|
+
PairMatcherWidget.displayName = "PairMatcherWidget";
|
|
13791
15245
|
function isInlineInputWidget(widget) {
|
|
13792
15246
|
return widget.type === "inline-input";
|
|
13793
15247
|
}
|
|
13794
15248
|
function isDragOptionsWidget(widget) {
|
|
13795
15249
|
return widget.type === "drag-options";
|
|
13796
15250
|
}
|
|
15251
|
+
function isInlineDragOptionWidget(widget) {
|
|
15252
|
+
return widget.type === "inline-drag-option";
|
|
15253
|
+
}
|
|
13797
15254
|
function isRadioOptionsWidget(widget) {
|
|
13798
15255
|
return widget.type === "radio-options";
|
|
13799
15256
|
}
|
|
@@ -13806,6 +15263,9 @@ function isFractionWidget(widget) {
|
|
|
13806
15263
|
function isMatcherWidget(widget) {
|
|
13807
15264
|
return widget.type === "matcher";
|
|
13808
15265
|
}
|
|
15266
|
+
function isPairMatcherWidget(widget) {
|
|
15267
|
+
return widget.type === "pair-matcher";
|
|
15268
|
+
}
|
|
13809
15269
|
function isVisualFractionWidget(widget) {
|
|
13810
15270
|
return widget.type === "visual-fraction";
|
|
13811
15271
|
}
|
|
@@ -13818,6 +15278,7 @@ const WidgetRenderer = ({
|
|
|
13818
15278
|
validationStatus = null,
|
|
13819
15279
|
numValidationStatus = null,
|
|
13820
15280
|
denValidationStatus = null,
|
|
15281
|
+
validationDetail,
|
|
13821
15282
|
droppedInputs = /* @__PURE__ */ new Set(),
|
|
13822
15283
|
widgets,
|
|
13823
15284
|
onChange,
|
|
@@ -13852,6 +15313,15 @@ const WidgetRenderer = ({
|
|
|
13852
15313
|
}
|
|
13853
15314
|
);
|
|
13854
15315
|
}
|
|
15316
|
+
if (isInlineDragOptionWidget(widget)) {
|
|
15317
|
+
return /* @__PURE__ */ jsx(
|
|
15318
|
+
InlineDragOption,
|
|
15319
|
+
{
|
|
15320
|
+
widgetId,
|
|
15321
|
+
widget
|
|
15322
|
+
}
|
|
15323
|
+
);
|
|
15324
|
+
}
|
|
13855
15325
|
if (isRadioOptionsWidget(widget)) {
|
|
13856
15326
|
return /* @__PURE__ */ jsx(
|
|
13857
15327
|
RadioOptions,
|
|
@@ -13917,6 +15387,20 @@ const WidgetRenderer = ({
|
|
|
13917
15387
|
}
|
|
13918
15388
|
);
|
|
13919
15389
|
}
|
|
15390
|
+
if (isPairMatcherWidget(widget)) {
|
|
15391
|
+
return /* @__PURE__ */ jsx(
|
|
15392
|
+
PairMatcherWidget,
|
|
15393
|
+
{
|
|
15394
|
+
widgetId,
|
|
15395
|
+
widget,
|
|
15396
|
+
onChange,
|
|
15397
|
+
validationStatus,
|
|
15398
|
+
validationDetail,
|
|
15399
|
+
defaultValue: Array.isArray(value) ? value : void 0,
|
|
15400
|
+
widgets
|
|
15401
|
+
}
|
|
15402
|
+
);
|
|
15403
|
+
}
|
|
13920
15404
|
if (isVisualFractionWidget(widget)) {
|
|
13921
15405
|
return /* @__PURE__ */ jsx(
|
|
13922
15406
|
VisualFraction,
|
|
@@ -14554,6 +16038,13 @@ const NumberLineRenderer = ({
|
|
|
14554
16038
|
useEffect(() => {
|
|
14555
16039
|
setCounterCount(defaultCounterCount);
|
|
14556
16040
|
}, [counter]);
|
|
16041
|
+
const onChangeRef = useRef(onChange);
|
|
16042
|
+
onChangeRef.current = onChange;
|
|
16043
|
+
useEffect(() => {
|
|
16044
|
+
if (counter !== void 0) {
|
|
16045
|
+
onChangeRef.current(counter.id, String(counterCount));
|
|
16046
|
+
}
|
|
16047
|
+
}, [counterCount, counter?.id]);
|
|
14557
16048
|
const effectiveStep = useMemo(() => {
|
|
14558
16049
|
if (counter !== void 0 && counterCount > 0) {
|
|
14559
16050
|
return cleanFloat((max - min) / counterCount);
|
|
@@ -14587,10 +16078,17 @@ const NumberLineRenderer = ({
|
|
|
14587
16078
|
[visiblePositions]
|
|
14588
16079
|
);
|
|
14589
16080
|
const { positionContentHeight, getPositionContentRef } = useMeasuredPositionContentHeight(visiblePositionIds);
|
|
16081
|
+
const nlValidationStatus = useMemo(() => {
|
|
16082
|
+
if (!validationDetails || counter === void 0) return null;
|
|
16083
|
+
if (counter.answer === void 0) return null;
|
|
16084
|
+
const counterValidation = validationDetails[counter.id];
|
|
16085
|
+
if (counterValidation === void 0) return null;
|
|
16086
|
+
return counterValidation.isCorrect ? "correct" : "incorrect";
|
|
16087
|
+
}, [validationDetails, counter]);
|
|
14590
16088
|
return /* @__PURE__ */ jsxs(
|
|
14591
16089
|
"div",
|
|
14592
16090
|
{
|
|
14593
|
-
className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}`,
|
|
16091
|
+
className: `number-line${isAxisDragMode ? " number-line--drag-mode" : ""}${lineStyle === "line" ? " number-line--style-line" : ""}${nlValidationStatus ? ` number-line--${nlValidationStatus}` : ""}`,
|
|
14594
16092
|
style: {
|
|
14595
16093
|
[CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%`,
|
|
14596
16094
|
...positionContentHeight && { "--nl-position-content-height": positionContentHeight }
|
|
@@ -14673,36 +16171,6 @@ const NumberLineRenderer = ({
|
|
|
14673
16171
|
}
|
|
14674
16172
|
);
|
|
14675
16173
|
};
|
|
14676
|
-
function isArabic(text) {
|
|
14677
|
-
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
14678
|
-
}
|
|
14679
|
-
function hasLatinChars(text) {
|
|
14680
|
-
return /[A-Za-z]/.test(text);
|
|
14681
|
-
}
|
|
14682
|
-
function itemContainsArabic(item) {
|
|
14683
|
-
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
14684
|
-
return isArabic(item.value);
|
|
14685
|
-
}
|
|
14686
|
-
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
14687
|
-
return isArabic(item.value);
|
|
14688
|
-
}
|
|
14689
|
-
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
14690
|
-
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
14691
|
-
}
|
|
14692
|
-
return false;
|
|
14693
|
-
}
|
|
14694
|
-
function itemContainsLatin(item) {
|
|
14695
|
-
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
14696
|
-
return hasLatinChars(item.value);
|
|
14697
|
-
}
|
|
14698
|
-
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
14699
|
-
return hasLatinChars(item.value);
|
|
14700
|
-
}
|
|
14701
|
-
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
14702
|
-
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
14703
|
-
}
|
|
14704
|
-
return false;
|
|
14705
|
-
}
|
|
14706
16174
|
function isTextItem(item) {
|
|
14707
16175
|
return item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.BOLD_TEXT || item.type === JSON_TYPES.COLOR_TEXT || item.type === JSON_TYPES.OPERATOR || item.type === JSON_TYPES.SPACE || item.type === JSON_TYPES.GROUP;
|
|
14708
16176
|
}
|
|
@@ -14805,9 +16273,10 @@ const LayoutRenderer = ({
|
|
|
14805
16273
|
case JSON_TYPES.WIDGET_REF: {
|
|
14806
16274
|
const widget = widgets[item.widgetId];
|
|
14807
16275
|
if (!widget) return null;
|
|
14808
|
-
const
|
|
16276
|
+
const validationDetail = validationDetails?.[item.widgetId];
|
|
16277
|
+
const isCorrect = validationDetail?.isCorrect;
|
|
14809
16278
|
const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
|
|
14810
|
-
const widgetKey =
|
|
16279
|
+
const widgetKey = STATEFUL_WIDGET_TYPES.has(widget.type) ? `${item.widgetId}-${resetKey}` : item.widgetId;
|
|
14811
16280
|
return /* @__PURE__ */ jsx(
|
|
14812
16281
|
WidgetRenderer,
|
|
14813
16282
|
{
|
|
@@ -14817,8 +16286,9 @@ const LayoutRenderer = ({
|
|
|
14817
16286
|
values,
|
|
14818
16287
|
isDropped: droppedInputs.has(item.widgetId),
|
|
14819
16288
|
validationStatus,
|
|
14820
|
-
numValidationStatus: toValidationStatus(validationDetails?.[`${item.widgetId}__num`]),
|
|
14821
|
-
denValidationStatus: toValidationStatus(validationDetails?.[`${item.widgetId}__den`]),
|
|
16289
|
+
numValidationStatus: toValidationStatus(validationDetails?.[`${item.widgetId}__num`]?.isCorrect),
|
|
16290
|
+
denValidationStatus: toValidationStatus(validationDetails?.[`${item.widgetId}__den`]?.isCorrect),
|
|
16291
|
+
validationDetail,
|
|
14822
16292
|
droppedInputs,
|
|
14823
16293
|
widgets,
|
|
14824
16294
|
onChange,
|
|
@@ -14885,6 +16355,126 @@ const LayoutRenderer = ({
|
|
|
14885
16355
|
},
|
|
14886
16356
|
resetKey
|
|
14887
16357
|
);
|
|
16358
|
+
case JSON_TYPES.VERTICAL_OPERATION: {
|
|
16359
|
+
const vop = item;
|
|
16360
|
+
const anyRowHasOp = vop.items.some(
|
|
16361
|
+
(x) => x.type === JSON_TYPES.VOP_ROW && x.op !== void 0
|
|
16362
|
+
);
|
|
16363
|
+
let operatorRowIdx = -1;
|
|
16364
|
+
if (!anyRowHasOp && vop.op !== void 0) {
|
|
16365
|
+
const firstSepIdx = vop.items.findIndex((x) => x.type === JSON_TYPES.VOP_SEPARATOR);
|
|
16366
|
+
for (let i = (firstSepIdx === -1 ? vop.items.length : firstSepIdx) - 1; i >= 0; i--) {
|
|
16367
|
+
const curr = vop.items[i];
|
|
16368
|
+
if (curr && curr.type === JSON_TYPES.VOP_ROW) {
|
|
16369
|
+
operatorRowIdx = i;
|
|
16370
|
+
break;
|
|
16371
|
+
}
|
|
16372
|
+
}
|
|
16373
|
+
}
|
|
16374
|
+
const totalCols = vop.cols + 1;
|
|
16375
|
+
return /* @__PURE__ */ jsx(
|
|
16376
|
+
"div",
|
|
16377
|
+
{
|
|
16378
|
+
className: "vop",
|
|
16379
|
+
style: { "--vop-cols": totalCols },
|
|
16380
|
+
dir: "ltr",
|
|
16381
|
+
children: vop.items.map((child, idx) => {
|
|
16382
|
+
if (child.type === JSON_TYPES.VOP_SEPARATOR) {
|
|
16383
|
+
const sep = child;
|
|
16384
|
+
return /* @__PURE__ */ jsx("div", { className: "vop-sep" }, sep.id);
|
|
16385
|
+
}
|
|
16386
|
+
if (child.type === JSON_TYPES.VOP_CARRY_ROW) {
|
|
16387
|
+
const carry = child;
|
|
16388
|
+
const carryLeadingEmpty = Math.max(0, vop.cols - carry.cells.length);
|
|
16389
|
+
return /* @__PURE__ */ jsxs("div", { className: "vop-row vop-carry-row", children: [
|
|
16390
|
+
/* @__PURE__ */ jsx("span", { className: "vop-op-cell" }),
|
|
16391
|
+
Array.from({ length: carryLeadingEmpty }).map((_, i) => /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-empty" }, `cpad-${i}`)),
|
|
16392
|
+
carry.cells.map((cell) => {
|
|
16393
|
+
if (cell.kind === "digit") {
|
|
16394
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-digit", children: cell.value }, cell.id);
|
|
16395
|
+
}
|
|
16396
|
+
if (cell.kind === "label") {
|
|
16397
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-label", children: cell.value }, cell.id);
|
|
16398
|
+
}
|
|
16399
|
+
if (cell.kind === "color") {
|
|
16400
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-color", style: { color: cell.color }, children: cell.value }, cell.id);
|
|
16401
|
+
}
|
|
16402
|
+
if (cell.kind === "bold") {
|
|
16403
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-bold", children: cell.value }, cell.id);
|
|
16404
|
+
}
|
|
16405
|
+
if (cell.kind === "highlight") {
|
|
16406
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-highlight", children: /* @__PURE__ */ jsx("mark", { children: cell.value }) }, cell.id);
|
|
16407
|
+
}
|
|
16408
|
+
if (cell.kind === "empty") {
|
|
16409
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-empty" }, cell.id);
|
|
16410
|
+
}
|
|
16411
|
+
const widget = widgets[cell.widgetId];
|
|
16412
|
+
if (!widget) return /* @__PURE__ */ jsx("span", { className: "vop-cell" }, cell.id);
|
|
16413
|
+
const isCorrect = validationDetails?.[cell.widgetId]?.isCorrect;
|
|
16414
|
+
const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
|
|
16415
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-carry-cell-input", children: /* @__PURE__ */ jsx(
|
|
16416
|
+
WidgetRenderer,
|
|
16417
|
+
{
|
|
16418
|
+
widget,
|
|
16419
|
+
widgetId: cell.widgetId,
|
|
16420
|
+
value: values[cell.widgetId] || "",
|
|
16421
|
+
isDropped: droppedInputs.has(cell.widgetId),
|
|
16422
|
+
validationStatus,
|
|
16423
|
+
onChange,
|
|
16424
|
+
onInputFocus
|
|
16425
|
+
}
|
|
16426
|
+
) }, cell.id);
|
|
16427
|
+
})
|
|
16428
|
+
] }, carry.id);
|
|
16429
|
+
}
|
|
16430
|
+
const row = child;
|
|
16431
|
+
const showOperator = row.op !== void 0 || idx === operatorRowIdx;
|
|
16432
|
+
const displayOp = row.op ?? vop.op;
|
|
16433
|
+
const leadingEmpty = vop.cols - row.cells.length;
|
|
16434
|
+
return /* @__PURE__ */ jsxs("div", { className: "vop-row", children: [
|
|
16435
|
+
/* @__PURE__ */ jsx("span", { className: "vop-op-cell", children: showOperator ? /* @__PURE__ */ jsx("span", { className: "operator vop-op", children: displayOp }) : null }),
|
|
16436
|
+
Array.from({ length: leadingEmpty }).map((_, i) => /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-empty" }, `pad-${i}`)),
|
|
16437
|
+
row.cells.map((cell) => {
|
|
16438
|
+
if (cell.kind === "digit") {
|
|
16439
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-digit", children: cell.value }, cell.id);
|
|
16440
|
+
}
|
|
16441
|
+
if (cell.kind === "label") {
|
|
16442
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-label", children: cell.value }, cell.id);
|
|
16443
|
+
}
|
|
16444
|
+
if (cell.kind === "color") {
|
|
16445
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-color", style: { color: cell.color }, children: cell.value }, cell.id);
|
|
16446
|
+
}
|
|
16447
|
+
if (cell.kind === "bold") {
|
|
16448
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-bold", children: cell.value }, cell.id);
|
|
16449
|
+
}
|
|
16450
|
+
if (cell.kind === "highlight") {
|
|
16451
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-highlight", children: /* @__PURE__ */ jsx("mark", { children: cell.value }) }, cell.id);
|
|
16452
|
+
}
|
|
16453
|
+
if (cell.kind === "empty") {
|
|
16454
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-empty" }, cell.id);
|
|
16455
|
+
}
|
|
16456
|
+
const widget = widgets[cell.widgetId];
|
|
16457
|
+
if (!widget) return /* @__PURE__ */ jsx("span", { className: "vop-cell" }, cell.id);
|
|
16458
|
+
const isCorrect = validationDetails?.[cell.widgetId]?.isCorrect;
|
|
16459
|
+
const validationStatus = isCorrect === true ? "correct" : isCorrect === false ? "incorrect" : null;
|
|
16460
|
+
return /* @__PURE__ */ jsx("span", { className: "vop-cell vop-cell-input", children: /* @__PURE__ */ jsx(
|
|
16461
|
+
WidgetRenderer,
|
|
16462
|
+
{
|
|
16463
|
+
widget,
|
|
16464
|
+
widgetId: cell.widgetId,
|
|
16465
|
+
value: values[cell.widgetId] || "",
|
|
16466
|
+
isDropped: droppedInputs.has(cell.widgetId),
|
|
16467
|
+
validationStatus,
|
|
16468
|
+
onChange,
|
|
16469
|
+
onInputFocus
|
|
16470
|
+
}
|
|
16471
|
+
) }, cell.id);
|
|
16472
|
+
})
|
|
16473
|
+
] }, row.id);
|
|
16474
|
+
})
|
|
16475
|
+
}
|
|
16476
|
+
);
|
|
16477
|
+
}
|
|
14888
16478
|
default:
|
|
14889
16479
|
return null;
|
|
14890
16480
|
}
|
|
@@ -14901,7 +16491,7 @@ const DEFAULT_RULER_TOOL = {
|
|
|
14901
16491
|
function buildInitialValues(json) {
|
|
14902
16492
|
const initial = {};
|
|
14903
16493
|
for (const [id, widget] of Object.entries(json.widgets)) {
|
|
14904
|
-
if (widget.type === JSON_TYPES.MATCHER) initial[id] = [];
|
|
16494
|
+
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) initial[id] = [];
|
|
14905
16495
|
}
|
|
14906
16496
|
return initial;
|
|
14907
16497
|
}
|
|
@@ -14920,10 +16510,12 @@ const ExerciseRenderer = forwardRef(({
|
|
|
14920
16510
|
useImperativeHandle(ref, () => ({
|
|
14921
16511
|
submit: () => {
|
|
14922
16512
|
const result = validate(json, values);
|
|
14923
|
-
const details = {};
|
|
14924
|
-
|
|
14925
|
-
|
|
14926
|
-
|
|
16513
|
+
const details = { ...result.details };
|
|
16514
|
+
const slotValidation = (id, raw, expected) => ({
|
|
16515
|
+
widgetId: id,
|
|
16516
|
+
userValue: typeof raw === "string" ? raw : void 0,
|
|
16517
|
+
isCorrect: typeof raw === "string" && raw !== "" && normalize(raw) === normalize(expected)
|
|
16518
|
+
});
|
|
14927
16519
|
for (const [widgetId, widget] of Object.entries(json.widgets)) {
|
|
14928
16520
|
if (widget.type !== JSON_TYPES.FRACTION) continue;
|
|
14929
16521
|
if (!(widgetId in details)) continue;
|
|
@@ -14931,18 +16523,12 @@ const ExerciseRenderer = forwardRef(({
|
|
|
14931
16523
|
if (!answer) continue;
|
|
14932
16524
|
const [expectedNum, expectedDen] = answer.split("/", 2);
|
|
14933
16525
|
if (!expectedNum || !expectedDen) continue;
|
|
14934
|
-
if (details[widgetId]) {
|
|
14935
|
-
if (widget.numerator === null) details[`${widgetId}__num`] = true;
|
|
14936
|
-
if (widget.denominator === null) details[`${widgetId}__den`] = true;
|
|
16526
|
+
if (details[widgetId]?.isCorrect) {
|
|
16527
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = { widgetId: `${widgetId}__num`, userValue: void 0, isCorrect: true };
|
|
16528
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = { widgetId: `${widgetId}__den`, userValue: void 0, isCorrect: true };
|
|
14937
16529
|
} else {
|
|
14938
|
-
if (widget.numerator === null) {
|
|
14939
|
-
|
|
14940
|
-
details[`${widgetId}__num`] = typeof u === "string" && u !== "" && normalize(u) === normalize(expectedNum);
|
|
14941
|
-
}
|
|
14942
|
-
if (widget.denominator === null) {
|
|
14943
|
-
const u = values[`${widgetId}__den`];
|
|
14944
|
-
details[`${widgetId}__den`] = typeof u === "string" && u !== "" && normalize(u) === normalize(expectedDen);
|
|
14945
|
-
}
|
|
16530
|
+
if (widget.numerator === null) details[`${widgetId}__num`] = slotValidation(`${widgetId}__num`, values[`${widgetId}__num`], expectedNum);
|
|
16531
|
+
if (widget.denominator === null) details[`${widgetId}__den`] = slotValidation(`${widgetId}__den`, values[`${widgetId}__den`], expectedDen);
|
|
14946
16532
|
}
|
|
14947
16533
|
}
|
|
14948
16534
|
setValidationDetails(details);
|
|
@@ -14982,6 +16568,13 @@ const ExerciseRenderer = forwardRef(({
|
|
|
14982
16568
|
onSubmit?.(values);
|
|
14983
16569
|
};
|
|
14984
16570
|
const lineGroups = useMemo(() => {
|
|
16571
|
+
const isRTLInlineWidget = (item) => {
|
|
16572
|
+
if (item.type === JSON_TYPES.WIDGET_REF) {
|
|
16573
|
+
const widget = json.widgets[item.widgetId];
|
|
16574
|
+
return !!widget && RTL_INLINE_WIDGET_TYPES.has(widget.type);
|
|
16575
|
+
}
|
|
16576
|
+
return RTL_INLINE_WIDGET_TYPES.has(item.type);
|
|
16577
|
+
};
|
|
14985
16578
|
const groups = [];
|
|
14986
16579
|
let currentGroup = [];
|
|
14987
16580
|
for (const item of json.layout) {
|
|
@@ -14989,6 +16582,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
14989
16582
|
groups.push({
|
|
14990
16583
|
items: [item],
|
|
14991
16584
|
hasArabic: itemContainsArabic(item),
|
|
16585
|
+
hasRTLInlineWidget: false,
|
|
14992
16586
|
subGroups: []
|
|
14993
16587
|
// Question text doesn't need sub-grouping
|
|
14994
16588
|
});
|
|
@@ -14997,13 +16591,15 @@ const ExerciseRenderer = forwardRef(({
|
|
|
14997
16591
|
if (item.type === JSON_TYPES.TABLE) {
|
|
14998
16592
|
if (currentGroup.length > 0) {
|
|
14999
16593
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
16594
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
15000
16595
|
const subGroups = groupLineItems(currentGroup);
|
|
15001
|
-
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
16596
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
15002
16597
|
currentGroup = [];
|
|
15003
16598
|
}
|
|
15004
16599
|
groups.push({
|
|
15005
16600
|
items: [item],
|
|
15006
16601
|
hasArabic: false,
|
|
16602
|
+
hasRTLInlineWidget: false,
|
|
15007
16603
|
subGroups: []
|
|
15008
16604
|
});
|
|
15009
16605
|
continue;
|
|
@@ -15011,13 +16607,31 @@ const ExerciseRenderer = forwardRef(({
|
|
|
15011
16607
|
if (item.type === JSON_TYPES.NUMBER_LINE) {
|
|
15012
16608
|
if (currentGroup.length > 0) {
|
|
15013
16609
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
16610
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
16611
|
+
const subGroups = groupLineItems(currentGroup);
|
|
16612
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
16613
|
+
currentGroup = [];
|
|
16614
|
+
}
|
|
16615
|
+
groups.push({
|
|
16616
|
+
items: [item],
|
|
16617
|
+
hasArabic: false,
|
|
16618
|
+
hasRTLInlineWidget: false,
|
|
16619
|
+
subGroups: []
|
|
16620
|
+
});
|
|
16621
|
+
continue;
|
|
16622
|
+
}
|
|
16623
|
+
if (item.type === JSON_TYPES.VERTICAL_OPERATION) {
|
|
16624
|
+
if (currentGroup.length > 0) {
|
|
16625
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
16626
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
15014
16627
|
const subGroups = groupLineItems(currentGroup);
|
|
15015
|
-
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
16628
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
15016
16629
|
currentGroup = [];
|
|
15017
16630
|
}
|
|
15018
16631
|
groups.push({
|
|
15019
16632
|
items: [item],
|
|
15020
16633
|
hasArabic: false,
|
|
16634
|
+
hasRTLInlineWidget: false,
|
|
15021
16635
|
subGroups: []
|
|
15022
16636
|
});
|
|
15023
16637
|
continue;
|
|
@@ -15025,8 +16639,9 @@ const ExerciseRenderer = forwardRef(({
|
|
|
15025
16639
|
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
15026
16640
|
if (currentGroup.length > 0) {
|
|
15027
16641
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
16642
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
15028
16643
|
const subGroups = groupLineItems(currentGroup);
|
|
15029
|
-
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
16644
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
15030
16645
|
currentGroup = [];
|
|
15031
16646
|
}
|
|
15032
16647
|
} else {
|
|
@@ -15035,11 +16650,12 @@ const ExerciseRenderer = forwardRef(({
|
|
|
15035
16650
|
}
|
|
15036
16651
|
if (currentGroup.length > 0) {
|
|
15037
16652
|
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
16653
|
+
const hasRTLInlineWidget = currentGroup.some(isRTLInlineWidget);
|
|
15038
16654
|
const subGroups = groupLineItems(currentGroup);
|
|
15039
|
-
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
16655
|
+
groups.push({ items: currentGroup, hasArabic, hasRTLInlineWidget, subGroups });
|
|
15040
16656
|
}
|
|
15041
16657
|
return groups;
|
|
15042
|
-
}, [json.layout]);
|
|
16658
|
+
}, [json.layout, json.widgets]);
|
|
15043
16659
|
const droppedInputs = useMemo(() => new Set(droppedSources.keys()), [droppedSources]);
|
|
15044
16660
|
const dragOptionsWidget = json.widgets["_drag_options"];
|
|
15045
16661
|
const rulerWidget = json.widgets["_ruler"];
|
|
@@ -15057,7 +16673,8 @@ const ExerciseRenderer = forwardRef(({
|
|
|
15057
16673
|
/* @__PURE__ */ jsx("div", { className: "exercise-layout", children: lineGroups.map((group, groupIdx) => /* @__PURE__ */ jsx(
|
|
15058
16674
|
"div",
|
|
15059
16675
|
{
|
|
15060
|
-
className:
|
|
16676
|
+
className: "exercise-line",
|
|
16677
|
+
dir: group.hasArabic && group.hasRTLInlineWidget ? "rtl" : void 0,
|
|
15061
16678
|
children: group.subGroups.length > 0 ? (
|
|
15062
16679
|
// Render sub-groups for regular lines
|
|
15063
16680
|
group.subGroups.map((subGroup, subIdx) => subGroup.isTextGroup ? (
|
|
@@ -15202,6 +16819,7 @@ export {
|
|
|
15202
16819
|
HighlightedTextSchema,
|
|
15203
16820
|
ImageInlineSchema,
|
|
15204
16821
|
ImageItemSchema,
|
|
16822
|
+
InlineDragOptionSchema,
|
|
15205
16823
|
InlineInputJSONSchema,
|
|
15206
16824
|
InlineInputWidgetSchema,
|
|
15207
16825
|
InlineNodeSchema,
|
|
@@ -15214,6 +16832,8 @@ export {
|
|
|
15214
16832
|
MatcherBlockSchema,
|
|
15215
16833
|
MatcherItemSchema,
|
|
15216
16834
|
MatcherRowSchema,
|
|
16835
|
+
MatcherWidgetJSONSchema,
|
|
16836
|
+
MixedValidationSchema,
|
|
15217
16837
|
NumberLineCounterItemSchema,
|
|
15218
16838
|
NumberLineHighlightItemSchema,
|
|
15219
16839
|
NumberLineItemSchema,
|
|
@@ -15222,6 +16842,8 @@ export {
|
|
|
15222
16842
|
NumericKeypadToolSchema,
|
|
15223
16843
|
OperatorItemSchema,
|
|
15224
16844
|
OperatorSchema,
|
|
16845
|
+
PairMatcherBlockSchema,
|
|
16846
|
+
PairMatcherWidgetJSONSchema,
|
|
15225
16847
|
ParseError,
|
|
15226
16848
|
QuestionTextSchema,
|
|
15227
16849
|
RadioOptionsJSONSchema,
|
|
@@ -15239,10 +16861,31 @@ export {
|
|
|
15239
16861
|
VISUAL_FRACTION_MAX_DIVISIONS,
|
|
15240
16862
|
VISUAL_FRACTION_SHAPE,
|
|
15241
16863
|
VISUAL_FRACTION_SHAPES,
|
|
16864
|
+
VOP_OPERATORS,
|
|
15242
16865
|
ValidationError,
|
|
15243
16866
|
ValidationJSONSchema,
|
|
15244
16867
|
ValidationNodeSchema,
|
|
16868
|
+
VerticalOperationBlockSchema,
|
|
16869
|
+
VerticalOperationItemSchema,
|
|
15245
16870
|
VisualFractionInlineSchema,
|
|
16871
|
+
VopBoldCellItemSchema,
|
|
16872
|
+
VopBoldCellSchema,
|
|
16873
|
+
VopCarryRowItemSchema,
|
|
16874
|
+
VopCarryRowSchema,
|
|
16875
|
+
VopCellItemSchema,
|
|
16876
|
+
VopCellSchema,
|
|
16877
|
+
VopColorCellItemSchema,
|
|
16878
|
+
VopColorCellSchema,
|
|
16879
|
+
VopDigitCellSchema,
|
|
16880
|
+
VopEmptyCellSchema,
|
|
16881
|
+
VopHighlightCellItemSchema,
|
|
16882
|
+
VopHighlightCellSchema,
|
|
16883
|
+
VopInputCellSchema,
|
|
16884
|
+
VopLabelCellSchema,
|
|
16885
|
+
VopRowItemSchema,
|
|
16886
|
+
VopRowSchema,
|
|
16887
|
+
VopSeparatorItemSchema,
|
|
16888
|
+
VopSeparatorSchema,
|
|
15246
16889
|
WidgetJSONSchema,
|
|
15247
16890
|
WidgetRefSchema,
|
|
15248
16891
|
WidgetRenderer,
|
|
@@ -15257,17 +16900,31 @@ export {
|
|
|
15257
16900
|
createFreeText,
|
|
15258
16901
|
createHighlightedText,
|
|
15259
16902
|
createImage,
|
|
16903
|
+
createInlineDragOption,
|
|
15260
16904
|
createLineBlock,
|
|
15261
16905
|
createMatcherBlock,
|
|
15262
16906
|
createMatcherItem,
|
|
15263
16907
|
createMatcherRow,
|
|
16908
|
+
createMixedValidation,
|
|
15264
16909
|
createNumericInput,
|
|
15265
16910
|
createNumericKeypad,
|
|
16911
|
+
createPairMatcherBlock,
|
|
15266
16912
|
createSimpleValidation,
|
|
15267
16913
|
createSpace,
|
|
15268
16914
|
createStringInput,
|
|
15269
16915
|
createTextBlock,
|
|
16916
|
+
createVerticalOperationBlock,
|
|
15270
16917
|
createVisualFraction,
|
|
16918
|
+
createVopBoldCell,
|
|
16919
|
+
createVopCarryRow,
|
|
16920
|
+
createVopColorCell,
|
|
16921
|
+
createVopDigitCell,
|
|
16922
|
+
createVopEmptyCell,
|
|
16923
|
+
createVopHighlightCell,
|
|
16924
|
+
createVopInputCell,
|
|
16925
|
+
createVopLabelCell,
|
|
16926
|
+
createVopRow,
|
|
16927
|
+
createVopSeparator,
|
|
15271
16928
|
evaluate,
|
|
15272
16929
|
extractVariables as extractExpressionVariables,
|
|
15273
16930
|
findValidCombination,
|
|
@@ -15287,6 +16944,8 @@ export {
|
|
|
15287
16944
|
isHighlightedText,
|
|
15288
16945
|
isImage,
|
|
15289
16946
|
isImageItem,
|
|
16947
|
+
isInlineDragOption,
|
|
16948
|
+
isInlineDragOptionJSON,
|
|
15290
16949
|
isInlineInput,
|
|
15291
16950
|
isInlineInputJSON,
|
|
15292
16951
|
isInlineNode,
|
|
@@ -15295,9 +16954,13 @@ export {
|
|
|
15295
16954
|
isMatcherBlock,
|
|
15296
16955
|
isMatcherItem,
|
|
15297
16956
|
isMatcherRow,
|
|
16957
|
+
isMatcherWidget,
|
|
16958
|
+
isMixedValidation,
|
|
15298
16959
|
isNumericKeypad,
|
|
15299
16960
|
isNumericKeypadJSON,
|
|
15300
16961
|
isOperatorItem,
|
|
16962
|
+
isPairMatcherBlock,
|
|
16963
|
+
isPairMatcherWidget,
|
|
15301
16964
|
isQuestionText,
|
|
15302
16965
|
isRadioOptionsJSON,
|
|
15303
16966
|
isSimpleValidation,
|
|
@@ -15311,8 +16974,15 @@ export {
|
|
|
15311
16974
|
isToolNode,
|
|
15312
16975
|
isValidExpression,
|
|
15313
16976
|
isValidationNode,
|
|
16977
|
+
isVerticalOperationBlock,
|
|
16978
|
+
isVerticalOperationItem,
|
|
15314
16979
|
isVisualFractionInline,
|
|
16980
|
+
isVopCarryRow,
|
|
16981
|
+
isVopCell,
|
|
16982
|
+
isVopRow,
|
|
16983
|
+
isVopSeparator,
|
|
15315
16984
|
isWidgetRef,
|
|
16985
|
+
normalizeVopOperator,
|
|
15316
16986
|
parse,
|
|
15317
16987
|
parseFractionKey,
|
|
15318
16988
|
parseSafe,
|
|
@@ -15327,6 +16997,7 @@ export {
|
|
|
15327
16997
|
validateExerciseJSONSafe,
|
|
15328
16998
|
validateExpression,
|
|
15329
16999
|
validateInputFormats,
|
|
17000
|
+
validateMixed,
|
|
15330
17001
|
validateSimple,
|
|
15331
17002
|
validateVisualFractionCapacity,
|
|
15332
17003
|
validateWidgetJSON,
|