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