@mathscan/math-exercise-engine 1.0.1 → 1.0.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/README.md +56 -41
- package/dist/index.d.ts +228 -159
- package/dist/index.js +413 -222
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +413 -222
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +241 -60
- package/package.json +9 -4
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,87 @@ import { create, all } from "mathjs";
|
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import React, { useLayoutEffect, useEffect, useRef, useMemo, useCallback, useState, createContext, memo, useReducer, useContext, forwardRef, cloneElement, useImperativeHandle } from "react";
|
|
6
6
|
import require$$0, { unstable_batchedUpdates, createPortal } from "react-dom";
|
|
7
|
+
const AST_TYPES = {
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
+
// Inline Elements
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
/** Regular text without special formatting */
|
|
12
|
+
FREE_TEXT: "FreeText",
|
|
13
|
+
/** Explicit spacing element */
|
|
14
|
+
SPACE: "Space",
|
|
15
|
+
/** Text with optional character-range highlighting */
|
|
16
|
+
HIGHLIGHTED_TEXT: "HighlightedText",
|
|
17
|
+
/** Inline input widget (string or numeric) */
|
|
18
|
+
INLINE_INPUT: "InlineInput",
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
+
// Blocks
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
/** Question/instruction text block */
|
|
23
|
+
TEXT_BLOCK: "TextBlock",
|
|
24
|
+
/** Line containing inline elements */
|
|
25
|
+
LINE_BLOCK: "LineBlock",
|
|
26
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
// Tools
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
/** Drag-and-drop options tool */
|
|
30
|
+
DRAG_OPTIONS: "DragOptions",
|
|
31
|
+
/** Numeric keypad input tool */
|
|
32
|
+
NUMERIC_KEYPAD: "NumericKeypad",
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// Validation
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
/** Simple answer matching validation */
|
|
37
|
+
SIMPLE_VALIDATION: "SimpleValidation",
|
|
38
|
+
/** Expression-based validation */
|
|
39
|
+
EXPRESSION_VALIDATION: "ExpressionValidation",
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// Document
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
43
|
+
/** Root exercise document */
|
|
44
|
+
EXERCISE_DOCUMENT: "ExerciseDocument"
|
|
45
|
+
};
|
|
46
|
+
const JSON_TYPES = {
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
// Layout Items
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
/** Question/instruction text */
|
|
51
|
+
QUESTION_TEXT: "question-text",
|
|
52
|
+
/** Regular text element */
|
|
53
|
+
TEXT: "text",
|
|
54
|
+
/** Spacing element */
|
|
55
|
+
SPACE: "space",
|
|
56
|
+
/** Text with optional highlighting */
|
|
57
|
+
HIGHLIGHTED_TEXT: "highlighted-text",
|
|
58
|
+
/** Reference to a widget */
|
|
59
|
+
WIDGET_REF: "widget-ref",
|
|
60
|
+
/** Line break element */
|
|
61
|
+
LINEBREAK: "linebreak",
|
|
62
|
+
/** Grouped layout items */
|
|
63
|
+
GROUP: "group",
|
|
64
|
+
/** Operator symbol */
|
|
65
|
+
OPERATOR: "operator",
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// Widget Types
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
/** Inline input widget (string or numeric) */
|
|
70
|
+
INLINE_INPUT: "inline-input",
|
|
71
|
+
/** Drag-and-drop options widget */
|
|
72
|
+
DRAG_OPTIONS: "drag-options",
|
|
73
|
+
/** Numeric keypad widget */
|
|
74
|
+
NUMERIC_KEYPAD: "numeric-keypad"
|
|
75
|
+
};
|
|
76
|
+
const VALIDATION_MODES = {
|
|
77
|
+
/** Simple answer matching validation */
|
|
78
|
+
SIMPLE: "simple",
|
|
79
|
+
/** Expression-based validation using JavaScript expressions */
|
|
80
|
+
EXPRESSION: "expression"
|
|
81
|
+
};
|
|
82
|
+
const INPUT_TYPES = {
|
|
83
|
+
/** String/text input */
|
|
84
|
+
STRING: "string",
|
|
85
|
+
/** Numeric input (digits only) */
|
|
86
|
+
NUMERIC: "numeric"
|
|
87
|
+
};
|
|
7
88
|
const Newline = createToken({
|
|
8
89
|
name: "Newline",
|
|
9
90
|
pattern: /\r?\n/,
|
|
@@ -29,14 +110,18 @@ const QUESTION_TEXT = createToken({
|
|
|
29
110
|
name: "QUESTION_TEXT",
|
|
30
111
|
pattern: /QUESTION_TEXT/
|
|
31
112
|
});
|
|
32
|
-
const
|
|
33
|
-
name: "
|
|
34
|
-
pattern: /
|
|
113
|
+
const TEXT_HIGHLIGHTER = createToken({
|
|
114
|
+
name: "TEXT_HIGHLIGHTER",
|
|
115
|
+
pattern: /TEXT_HIGHLIGHTER/
|
|
35
116
|
});
|
|
36
117
|
const W_S_I = createToken({
|
|
37
118
|
name: "W_S_I",
|
|
38
119
|
pattern: /W_S_I/
|
|
39
120
|
});
|
|
121
|
+
const STRING_INPUT = createToken({
|
|
122
|
+
name: "STRING_INPUT",
|
|
123
|
+
pattern: /STRING_INPUT/
|
|
124
|
+
});
|
|
40
125
|
const DRAGABLE_BUTTONS_OPTIONS = createToken({
|
|
41
126
|
name: "DRAGABLE_BUTTONS_OPTIONS",
|
|
42
127
|
pattern: /DRAGABLE_BUTTONS_OPTIONS/
|
|
@@ -53,6 +138,10 @@ const W_N_I = createToken({
|
|
|
53
138
|
name: "W_N_I",
|
|
54
139
|
pattern: /W_N_I/
|
|
55
140
|
});
|
|
141
|
+
const NUMERIC_INPUT = createToken({
|
|
142
|
+
name: "NUMERIC_INPUT",
|
|
143
|
+
pattern: /NUMERIC_INPUT/
|
|
144
|
+
});
|
|
56
145
|
const NumberLiteral = createToken({
|
|
57
146
|
name: "NumberLiteral",
|
|
58
147
|
pattern: /-?\d+(\.\d+)?/
|
|
@@ -122,7 +211,7 @@ const Comma = createToken({
|
|
|
122
211
|
});
|
|
123
212
|
const FreeText = createToken({
|
|
124
213
|
name: "FreeText",
|
|
125
|
-
pattern: /(?:[^A-Z\n\r]|(?![A-Z_]+\[)[A-Z])+/,
|
|
214
|
+
pattern: /(?:[^A-Z\n\r \t+\-−×*÷/=<>?()\[\],]|(?![A-Z_]+\[)[A-Z])+/,
|
|
126
215
|
line_breaks: false
|
|
127
216
|
});
|
|
128
217
|
const allTokens = [
|
|
@@ -135,13 +224,14 @@ const allTokens = [
|
|
|
135
224
|
Comment,
|
|
136
225
|
// Component keywords (before Identifier!)
|
|
137
226
|
QUESTION_TEXT,
|
|
138
|
-
|
|
227
|
+
TEXT_HIGHLIGHTER,
|
|
139
228
|
W_S_I,
|
|
229
|
+
STRING_INPUT,
|
|
140
230
|
DRAGABLE_BUTTONS_OPTIONS,
|
|
141
231
|
NUMERIC_KEYPAD,
|
|
142
232
|
WIDGET_ANSWER,
|
|
143
|
-
// Legacy keywords
|
|
144
233
|
W_N_I,
|
|
234
|
+
NUMERIC_INPUT,
|
|
145
235
|
// Literals
|
|
146
236
|
NumberLiteral,
|
|
147
237
|
StringLiteral,
|
|
@@ -319,14 +409,14 @@ class MathExerciseParser extends CstParser {
|
|
|
319
409
|
// Parentheses (for grouping)
|
|
320
410
|
{ ALT: () => this.CONSUME(LParen) },
|
|
321
411
|
{ ALT: () => this.CONSUME(RParen) },
|
|
322
|
-
//
|
|
323
|
-
{ ALT: () => this.SUBRULE(this.
|
|
412
|
+
// TEXT_HIGHLIGHTER (for highlighting)
|
|
413
|
+
{ ALT: () => this.SUBRULE(this.textHighlighter) },
|
|
324
414
|
// W_S_I (inline input widget)
|
|
325
415
|
{ ALT: () => this.SUBRULE(this.inlineInput) }
|
|
326
416
|
]);
|
|
327
417
|
});
|
|
328
|
-
this.
|
|
329
|
-
this.CONSUME(
|
|
418
|
+
this.textHighlighter = this.RULE("textHighlighter", () => {
|
|
419
|
+
this.CONSUME(TEXT_HIGHLIGHTER);
|
|
330
420
|
this.CONSUME(LBracket);
|
|
331
421
|
this.SUBRULE(this.parameterList);
|
|
332
422
|
this.CONSUME(RBracket);
|
|
@@ -334,7 +424,9 @@ class MathExerciseParser extends CstParser {
|
|
|
334
424
|
this.inlineInput = this.RULE("inlineInput", () => {
|
|
335
425
|
this.OR([
|
|
336
426
|
{ ALT: () => this.CONSUME(W_S_I, { LABEL: "stringInput" }) },
|
|
337
|
-
{ ALT: () => this.CONSUME(
|
|
427
|
+
{ ALT: () => this.CONSUME(STRING_INPUT, { LABEL: "stringInput" }) },
|
|
428
|
+
{ ALT: () => this.CONSUME(W_N_I, { LABEL: "numericInput" }) },
|
|
429
|
+
{ ALT: () => this.CONSUME(NUMERIC_INPUT, { LABEL: "numericInput" }) }
|
|
338
430
|
]);
|
|
339
431
|
this.CONSUME(LBracket);
|
|
340
432
|
this.SUBRULE(this.parameterList);
|
|
@@ -361,7 +453,9 @@ class MathExerciseParser extends CstParser {
|
|
|
361
453
|
this.numericKeypad = this.RULE("numericKeypad", () => {
|
|
362
454
|
this.CONSUME(NUMERIC_KEYPAD);
|
|
363
455
|
this.CONSUME(LBracket);
|
|
364
|
-
this.
|
|
456
|
+
this.OPTION(() => {
|
|
457
|
+
this.SUBRULE(this.parameterList);
|
|
458
|
+
});
|
|
365
459
|
this.CONSUME(RBracket);
|
|
366
460
|
this.MANY(() => {
|
|
367
461
|
this.OR([
|
|
@@ -458,29 +552,29 @@ class MathExerciseParser extends CstParser {
|
|
|
458
552
|
}
|
|
459
553
|
const parser = new MathExerciseParser();
|
|
460
554
|
const FreeTextInlineSchema = z.object({
|
|
461
|
-
type: z.literal(
|
|
555
|
+
type: z.literal(AST_TYPES.FREE_TEXT),
|
|
462
556
|
value: z.string()
|
|
463
557
|
});
|
|
464
558
|
const SpaceInlineSchema = z.object({
|
|
465
|
-
type: z.literal(
|
|
559
|
+
type: z.literal(AST_TYPES.SPACE)
|
|
466
560
|
});
|
|
467
|
-
const
|
|
468
|
-
type: z.literal(
|
|
561
|
+
const HighlightedTextInlineSchema = z.object({
|
|
562
|
+
type: z.literal(AST_TYPES.HIGHLIGHTED_TEXT),
|
|
469
563
|
value: z.string(),
|
|
470
564
|
highlight: z.tuple([z.number(), z.number()]).optional()
|
|
471
565
|
});
|
|
472
566
|
const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
|
|
473
567
|
z.object({
|
|
474
|
-
type: z.literal(
|
|
568
|
+
type: z.literal(AST_TYPES.INLINE_INPUT),
|
|
475
569
|
name: z.string(),
|
|
476
|
-
inputType: z.literal(
|
|
570
|
+
inputType: z.literal(INPUT_TYPES.STRING),
|
|
477
571
|
length: z.number().int().positive(),
|
|
478
572
|
answer: z.union([z.string(), z.number()]).optional()
|
|
479
573
|
}),
|
|
480
574
|
z.object({
|
|
481
|
-
type: z.literal(
|
|
575
|
+
type: z.literal(AST_TYPES.INLINE_INPUT),
|
|
482
576
|
name: z.string(),
|
|
483
|
-
inputType: z.literal(
|
|
577
|
+
inputType: z.literal(INPUT_TYPES.NUMERIC),
|
|
484
578
|
digits: z.number().int().positive(),
|
|
485
579
|
answer: z.union([z.string(), z.number()]).optional()
|
|
486
580
|
})
|
|
@@ -488,52 +582,45 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
|
|
|
488
582
|
const InlineNodeSchema = z.union([
|
|
489
583
|
FreeTextInlineSchema,
|
|
490
584
|
SpaceInlineSchema,
|
|
491
|
-
|
|
585
|
+
HighlightedTextInlineSchema,
|
|
492
586
|
InlineInputWidgetSchema
|
|
493
587
|
]);
|
|
494
588
|
function createFreeText(value) {
|
|
495
589
|
return FreeTextInlineSchema.parse({
|
|
496
|
-
type:
|
|
590
|
+
type: AST_TYPES.FREE_TEXT,
|
|
497
591
|
value
|
|
498
592
|
});
|
|
499
593
|
}
|
|
500
594
|
function createSpace() {
|
|
501
595
|
return SpaceInlineSchema.parse({
|
|
502
|
-
type:
|
|
596
|
+
type: AST_TYPES.SPACE
|
|
503
597
|
});
|
|
504
598
|
}
|
|
505
|
-
function
|
|
506
|
-
return
|
|
507
|
-
type:
|
|
599
|
+
function createHighlightedText(value, highlight) {
|
|
600
|
+
return HighlightedTextInlineSchema.parse({
|
|
601
|
+
type: AST_TYPES.HIGHLIGHTED_TEXT,
|
|
508
602
|
value,
|
|
509
603
|
highlight
|
|
510
604
|
});
|
|
511
605
|
}
|
|
512
606
|
function createStringInput(name, length, answer) {
|
|
513
607
|
return InlineInputWidgetSchema.parse({
|
|
514
|
-
type:
|
|
608
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
515
609
|
name,
|
|
516
|
-
inputType:
|
|
610
|
+
inputType: INPUT_TYPES.STRING,
|
|
517
611
|
length,
|
|
518
612
|
answer
|
|
519
613
|
});
|
|
520
614
|
}
|
|
521
615
|
function createNumericInput(name, digits, answer) {
|
|
522
616
|
return InlineInputWidgetSchema.parse({
|
|
523
|
-
type:
|
|
617
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
524
618
|
name,
|
|
525
|
-
inputType:
|
|
619
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
526
620
|
digits,
|
|
527
621
|
answer
|
|
528
622
|
});
|
|
529
623
|
}
|
|
530
|
-
function createInlineInput(name, inputType, lengthOrDigits, answer) {
|
|
531
|
-
if (inputType === "string") {
|
|
532
|
-
return createStringInput(name, lengthOrDigits, answer);
|
|
533
|
-
} else {
|
|
534
|
-
return createNumericInput(name, lengthOrDigits, answer);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
624
|
function isFreeText(value) {
|
|
538
625
|
return FreeTextInlineSchema.safeParse(value).success;
|
|
539
626
|
}
|
|
@@ -547,22 +634,22 @@ function isInlineNode(value) {
|
|
|
547
634
|
return InlineNodeSchema.safeParse(value).success;
|
|
548
635
|
}
|
|
549
636
|
const TextBlockSchema = z.object({
|
|
550
|
-
type: z.literal(
|
|
637
|
+
type: z.literal(AST_TYPES.TEXT_BLOCK),
|
|
551
638
|
value: z.string()
|
|
552
639
|
});
|
|
553
640
|
const LineBlockSchema = z.object({
|
|
554
|
-
type: z.literal(
|
|
641
|
+
type: z.literal(AST_TYPES.LINE_BLOCK),
|
|
555
642
|
inlines: z.array(InlineNodeSchema)
|
|
556
643
|
});
|
|
557
644
|
function createTextBlock(value) {
|
|
558
645
|
return TextBlockSchema.parse({
|
|
559
|
-
type:
|
|
646
|
+
type: AST_TYPES.TEXT_BLOCK,
|
|
560
647
|
value
|
|
561
648
|
});
|
|
562
649
|
}
|
|
563
650
|
function createLineBlock(inlines) {
|
|
564
651
|
return LineBlockSchema.parse({
|
|
565
|
-
type:
|
|
652
|
+
type: AST_TYPES.LINE_BLOCK,
|
|
566
653
|
inlines
|
|
567
654
|
});
|
|
568
655
|
}
|
|
@@ -573,13 +660,13 @@ function isLineBlock(value) {
|
|
|
573
660
|
return LineBlockSchema.safeParse(value).success;
|
|
574
661
|
}
|
|
575
662
|
const DragOptionsToolSchema = z.object({
|
|
576
|
-
type: z.literal(
|
|
663
|
+
type: z.literal(AST_TYPES.DRAG_OPTIONS),
|
|
577
664
|
options: z.array(z.string()).min(1)
|
|
578
665
|
});
|
|
579
666
|
const NumericKeypadToolSchema = z.object({
|
|
580
|
-
type: z.literal(
|
|
581
|
-
|
|
582
|
-
|
|
667
|
+
type: z.literal(AST_TYPES.NUMERIC_KEYPAD),
|
|
668
|
+
includeOperators: z.boolean().optional(),
|
|
669
|
+
includeDecimal: z.boolean().optional()
|
|
583
670
|
});
|
|
584
671
|
const ToolNodeSchema = z.discriminatedUnion("type", [
|
|
585
672
|
DragOptionsToolSchema,
|
|
@@ -587,15 +674,15 @@ const ToolNodeSchema = z.discriminatedUnion("type", [
|
|
|
587
674
|
]);
|
|
588
675
|
function createDragOptions(options) {
|
|
589
676
|
return DragOptionsToolSchema.parse({
|
|
590
|
-
type:
|
|
677
|
+
type: AST_TYPES.DRAG_OPTIONS,
|
|
591
678
|
options
|
|
592
679
|
});
|
|
593
680
|
}
|
|
594
|
-
function createNumericKeypad(
|
|
681
|
+
function createNumericKeypad(includeOperators, includeDecimal) {
|
|
595
682
|
return NumericKeypadToolSchema.parse({
|
|
596
|
-
type:
|
|
597
|
-
|
|
598
|
-
|
|
683
|
+
type: AST_TYPES.NUMERIC_KEYPAD,
|
|
684
|
+
includeOperators,
|
|
685
|
+
includeDecimal
|
|
599
686
|
});
|
|
600
687
|
}
|
|
601
688
|
function isDragOptions(value) {
|
|
@@ -608,10 +695,10 @@ function isToolNode(value) {
|
|
|
608
695
|
return ToolNodeSchema.safeParse(value).success;
|
|
609
696
|
}
|
|
610
697
|
const SimpleValidationSchema = z.object({
|
|
611
|
-
type: z.literal(
|
|
698
|
+
type: z.literal(AST_TYPES.SIMPLE_VALIDATION)
|
|
612
699
|
});
|
|
613
700
|
const ExpressionValidationSchema = z.object({
|
|
614
|
-
type: z.literal(
|
|
701
|
+
type: z.literal(AST_TYPES.EXPRESSION_VALIDATION),
|
|
615
702
|
expression: z.string()
|
|
616
703
|
});
|
|
617
704
|
const ValidationNodeSchema = z.discriminatedUnion("type", [
|
|
@@ -620,12 +707,12 @@ const ValidationNodeSchema = z.discriminatedUnion("type", [
|
|
|
620
707
|
]);
|
|
621
708
|
function createSimpleValidation() {
|
|
622
709
|
return SimpleValidationSchema.parse({
|
|
623
|
-
type:
|
|
710
|
+
type: AST_TYPES.SIMPLE_VALIDATION
|
|
624
711
|
});
|
|
625
712
|
}
|
|
626
713
|
function createExpressionValidation(expression) {
|
|
627
714
|
return ExpressionValidationSchema.parse({
|
|
628
|
-
type:
|
|
715
|
+
type: AST_TYPES.EXPRESSION_VALIDATION,
|
|
629
716
|
expression
|
|
630
717
|
});
|
|
631
718
|
}
|
|
@@ -639,7 +726,7 @@ function isValidationNode(value) {
|
|
|
639
726
|
return ValidationNodeSchema.safeParse(value).success;
|
|
640
727
|
}
|
|
641
728
|
const ExerciseDocumentSchema = z.object({
|
|
642
|
-
type: z.literal(
|
|
729
|
+
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
643
730
|
question: TextBlockSchema.optional(),
|
|
644
731
|
lines: z.array(LineBlockSchema),
|
|
645
732
|
tools: z.array(ToolNodeSchema).optional(),
|
|
@@ -647,7 +734,7 @@ const ExerciseDocumentSchema = z.object({
|
|
|
647
734
|
});
|
|
648
735
|
function createExerciseDocument(question, lines, validation, tools) {
|
|
649
736
|
return ExerciseDocumentSchema.parse({
|
|
650
|
-
type:
|
|
737
|
+
type: AST_TYPES.EXERCISE_DOCUMENT,
|
|
651
738
|
question,
|
|
652
739
|
lines,
|
|
653
740
|
tools,
|
|
@@ -752,19 +839,22 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
752
839
|
const token = ctx.RParen[0];
|
|
753
840
|
return createFreeText(token.image);
|
|
754
841
|
}
|
|
755
|
-
if (ctx.
|
|
756
|
-
return this.visit(ctx.
|
|
842
|
+
if (ctx.textHighlighter) {
|
|
843
|
+
return this.visit(ctx.textHighlighter);
|
|
757
844
|
}
|
|
758
845
|
if (ctx.inlineInput) {
|
|
759
846
|
return this.visit(ctx.inlineInput);
|
|
760
847
|
}
|
|
761
848
|
throw new Error("Unknown inline element");
|
|
762
849
|
}
|
|
763
|
-
|
|
850
|
+
textHighlighter(ctx) {
|
|
764
851
|
const params = this.visit(ctx.parameterList);
|
|
765
852
|
const value = params.value;
|
|
766
|
-
|
|
767
|
-
|
|
853
|
+
let highlight = void 0;
|
|
854
|
+
if (params.start !== void 0 && params.end !== void 0) {
|
|
855
|
+
highlight = [params.start, params.end];
|
|
856
|
+
}
|
|
857
|
+
return createHighlightedText(value, highlight);
|
|
768
858
|
}
|
|
769
859
|
inlineInput(ctx) {
|
|
770
860
|
const params = this.visit(ctx.parameterList);
|
|
@@ -794,9 +884,10 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
794
884
|
return createDragOptions(strings);
|
|
795
885
|
}
|
|
796
886
|
numericKeypad(ctx) {
|
|
797
|
-
const params = this.visit(ctx.parameterList);
|
|
887
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
888
|
+
const includeOperators = params.includeOperators === true || params.includeOperators === "true";
|
|
798
889
|
const includeDecimal = params.includeDecimal === true || params.includeDecimal === "true";
|
|
799
|
-
return createNumericKeypad(includeDecimal);
|
|
890
|
+
return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0);
|
|
800
891
|
}
|
|
801
892
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
802
893
|
// Validation
|
|
@@ -888,33 +979,33 @@ function toAST(cst) {
|
|
|
888
979
|
}
|
|
889
980
|
const TextItemSchema = z.object({
|
|
890
981
|
id: z.string(),
|
|
891
|
-
type: z.literal(
|
|
982
|
+
type: z.literal(JSON_TYPES.TEXT),
|
|
892
983
|
value: z.string()
|
|
893
984
|
});
|
|
894
985
|
const QuestionTextSchema = z.object({
|
|
895
986
|
id: z.string(),
|
|
896
|
-
type: z.literal(
|
|
987
|
+
type: z.literal(JSON_TYPES.QUESTION_TEXT),
|
|
897
988
|
value: z.string()
|
|
898
989
|
});
|
|
899
990
|
const WidgetRefSchema = z.object({
|
|
900
991
|
id: z.string(),
|
|
901
|
-
type: z.literal(
|
|
992
|
+
type: z.literal(JSON_TYPES.WIDGET_REF),
|
|
902
993
|
widgetId: z.string()
|
|
903
994
|
});
|
|
904
|
-
const
|
|
995
|
+
const HighlightedTextSchema = z.object({
|
|
905
996
|
id: z.string(),
|
|
906
|
-
type: z.literal(
|
|
997
|
+
type: z.literal(JSON_TYPES.HIGHLIGHTED_TEXT),
|
|
907
998
|
value: z.string(),
|
|
908
999
|
highlight: z.tuple([z.number(), z.number()]).optional()
|
|
909
1000
|
});
|
|
910
1001
|
const LinebreakSchema = z.object({
|
|
911
1002
|
id: z.string(),
|
|
912
|
-
type: z.literal(
|
|
1003
|
+
type: z.literal(JSON_TYPES.LINEBREAK)
|
|
913
1004
|
});
|
|
914
1005
|
const OperatorSchema = z.enum(["+", "-", "×", "÷", "=", "<", ">", "?"]);
|
|
915
1006
|
const OperatorItemSchema = z.object({
|
|
916
1007
|
id: z.string(),
|
|
917
|
-
type: z.literal(
|
|
1008
|
+
type: z.literal(JSON_TYPES.OPERATOR),
|
|
918
1009
|
operator: OperatorSchema
|
|
919
1010
|
});
|
|
920
1011
|
const LayoutItemSchema = z.lazy(
|
|
@@ -922,7 +1013,7 @@ const LayoutItemSchema = z.lazy(
|
|
|
922
1013
|
QuestionTextSchema,
|
|
923
1014
|
TextItemSchema,
|
|
924
1015
|
WidgetRefSchema,
|
|
925
|
-
|
|
1016
|
+
HighlightedTextSchema,
|
|
926
1017
|
LinebreakSchema,
|
|
927
1018
|
GroupItemSchema,
|
|
928
1019
|
OperatorItemSchema
|
|
@@ -930,17 +1021,17 @@ const LayoutItemSchema = z.lazy(
|
|
|
930
1021
|
);
|
|
931
1022
|
const GroupItemSchema = z.object({
|
|
932
1023
|
id: z.string(),
|
|
933
|
-
type: z.literal(
|
|
1024
|
+
type: z.literal(JSON_TYPES.GROUP),
|
|
934
1025
|
items: z.array(LayoutItemSchema)
|
|
935
1026
|
});
|
|
936
1027
|
const StringInputJSONSchema = z.object({
|
|
937
|
-
type: z.literal(
|
|
938
|
-
inputType: z.literal(
|
|
1028
|
+
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1029
|
+
inputType: z.literal(INPUT_TYPES.STRING),
|
|
939
1030
|
length: z.number().int().positive()
|
|
940
1031
|
});
|
|
941
1032
|
const NumericInputJSONSchema = z.object({
|
|
942
|
-
type: z.literal(
|
|
943
|
-
inputType: z.literal(
|
|
1033
|
+
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1034
|
+
inputType: z.literal(INPUT_TYPES.NUMERIC),
|
|
944
1035
|
digits: z.number().int().positive()
|
|
945
1036
|
});
|
|
946
1037
|
const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
|
|
@@ -948,12 +1039,13 @@ const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
|
|
|
948
1039
|
NumericInputJSONSchema
|
|
949
1040
|
]);
|
|
950
1041
|
const DragOptionsJSONSchema = z.object({
|
|
951
|
-
type: z.literal(
|
|
1042
|
+
type: z.literal(JSON_TYPES.DRAG_OPTIONS),
|
|
952
1043
|
options: z.array(z.string())
|
|
953
1044
|
});
|
|
954
1045
|
const NumericKeypadJSONSchema = z.object({
|
|
955
|
-
type: z.literal(
|
|
956
|
-
|
|
1046
|
+
type: z.literal(JSON_TYPES.NUMERIC_KEYPAD),
|
|
1047
|
+
includeOperators: z.boolean().optional(),
|
|
1048
|
+
includeDecimal: z.boolean().optional()
|
|
957
1049
|
});
|
|
958
1050
|
const WidgetJSONSchema = z.union([
|
|
959
1051
|
InlineInputJSONSchema,
|
|
@@ -961,7 +1053,7 @@ const WidgetJSONSchema = z.union([
|
|
|
961
1053
|
NumericKeypadJSONSchema
|
|
962
1054
|
]);
|
|
963
1055
|
const ValidationJSONSchema = z.object({
|
|
964
|
-
mode: z.enum([
|
|
1056
|
+
mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
|
|
965
1057
|
answers: z.record(z.union([z.string(), z.number()])).optional(),
|
|
966
1058
|
expression: z.string().optional(),
|
|
967
1059
|
variables: z.array(z.string()).optional()
|
|
@@ -993,40 +1085,34 @@ function validateWidgetJSONSafe(data) {
|
|
|
993
1085
|
return WidgetJSONSchema.safeParse(data);
|
|
994
1086
|
}
|
|
995
1087
|
function isQuestionText(item) {
|
|
996
|
-
return item.type ===
|
|
1088
|
+
return item.type === JSON_TYPES.QUESTION_TEXT;
|
|
997
1089
|
}
|
|
998
|
-
function isTextItem(item) {
|
|
999
|
-
return item.type ===
|
|
1090
|
+
function isTextItem$1(item) {
|
|
1091
|
+
return item.type === JSON_TYPES.TEXT;
|
|
1000
1092
|
}
|
|
1001
1093
|
function isWidgetRef(item) {
|
|
1002
|
-
return item.type ===
|
|
1094
|
+
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1003
1095
|
}
|
|
1004
|
-
function
|
|
1005
|
-
return item.type ===
|
|
1096
|
+
function isHighlightedText(item) {
|
|
1097
|
+
return item.type === JSON_TYPES.HIGHLIGHTED_TEXT;
|
|
1006
1098
|
}
|
|
1007
1099
|
function isLinebreak(item) {
|
|
1008
|
-
return item.type ===
|
|
1100
|
+
return item.type === JSON_TYPES.LINEBREAK;
|
|
1009
1101
|
}
|
|
1010
1102
|
function isGroupItem(item) {
|
|
1011
|
-
return item.type ===
|
|
1103
|
+
return item.type === JSON_TYPES.GROUP;
|
|
1012
1104
|
}
|
|
1013
1105
|
function isOperatorItem(item) {
|
|
1014
|
-
return item.type ===
|
|
1106
|
+
return item.type === JSON_TYPES.OPERATOR;
|
|
1015
1107
|
}
|
|
1016
1108
|
function isInlineInputJSON(widget) {
|
|
1017
|
-
return widget.type ===
|
|
1109
|
+
return widget.type === JSON_TYPES.INLINE_INPUT;
|
|
1018
1110
|
}
|
|
1019
1111
|
function isDragOptionsJSON(widget) {
|
|
1020
|
-
return widget.type ===
|
|
1112
|
+
return widget.type === JSON_TYPES.DRAG_OPTIONS;
|
|
1021
1113
|
}
|
|
1022
1114
|
function isNumericKeypadJSON(widget) {
|
|
1023
|
-
return widget.type ===
|
|
1024
|
-
}
|
|
1025
|
-
function isWidgetPlaceholder(item) {
|
|
1026
|
-
return item.type === "widget-ref";
|
|
1027
|
-
}
|
|
1028
|
-
function isNumericInputJSON(widget) {
|
|
1029
|
-
return widget.type === "inline-input";
|
|
1115
|
+
return widget.type === JSON_TYPES.NUMERIC_KEYPAD;
|
|
1030
1116
|
}
|
|
1031
1117
|
class LayoutIdGenerator {
|
|
1032
1118
|
constructor() {
|
|
@@ -1044,8 +1130,8 @@ function toJSON(ast) {
|
|
|
1044
1130
|
const layout = [];
|
|
1045
1131
|
if (ast.question) {
|
|
1046
1132
|
layout.push({
|
|
1047
|
-
id: idGenerator.generate(
|
|
1048
|
-
type:
|
|
1133
|
+
id: idGenerator.generate(JSON_TYPES.QUESTION_TEXT),
|
|
1134
|
+
type: JSON_TYPES.QUESTION_TEXT,
|
|
1049
1135
|
value: ast.question.value
|
|
1050
1136
|
});
|
|
1051
1137
|
}
|
|
@@ -1057,17 +1143,17 @@ function toJSON(ast) {
|
|
|
1057
1143
|
const answers = {};
|
|
1058
1144
|
for (const line of ast.lines) {
|
|
1059
1145
|
for (const inline of line.inlines) {
|
|
1060
|
-
if (inline.type ===
|
|
1061
|
-
if (inline.inputType ===
|
|
1146
|
+
if (inline.type === AST_TYPES.INLINE_INPUT) {
|
|
1147
|
+
if (inline.inputType === INPUT_TYPES.STRING) {
|
|
1062
1148
|
widgets[inline.name] = {
|
|
1063
|
-
type:
|
|
1064
|
-
inputType:
|
|
1149
|
+
type: JSON_TYPES.INLINE_INPUT,
|
|
1150
|
+
inputType: INPUT_TYPES.STRING,
|
|
1065
1151
|
length: inline.length
|
|
1066
1152
|
};
|
|
1067
1153
|
} else {
|
|
1068
1154
|
widgets[inline.name] = {
|
|
1069
|
-
type:
|
|
1070
|
-
inputType:
|
|
1155
|
+
type: JSON_TYPES.INLINE_INPUT,
|
|
1156
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
1071
1157
|
digits: inline.digits
|
|
1072
1158
|
};
|
|
1073
1159
|
}
|
|
@@ -1079,14 +1165,15 @@ function toJSON(ast) {
|
|
|
1079
1165
|
}
|
|
1080
1166
|
if (ast.tools) {
|
|
1081
1167
|
for (const tool of ast.tools) {
|
|
1082
|
-
if (tool.type ===
|
|
1168
|
+
if (tool.type === AST_TYPES.DRAG_OPTIONS) {
|
|
1083
1169
|
widgets["_drag_options"] = {
|
|
1084
|
-
type:
|
|
1170
|
+
type: JSON_TYPES.DRAG_OPTIONS,
|
|
1085
1171
|
options: tool.options
|
|
1086
1172
|
};
|
|
1087
|
-
} else if (tool.type ===
|
|
1173
|
+
} else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
|
|
1088
1174
|
widgets["_numeric_keypad"] = {
|
|
1089
|
-
type:
|
|
1175
|
+
type: JSON_TYPES.NUMERIC_KEYPAD,
|
|
1176
|
+
includeOperators: tool.includeOperators,
|
|
1090
1177
|
includeDecimal: tool.includeDecimal
|
|
1091
1178
|
};
|
|
1092
1179
|
}
|
|
@@ -1107,35 +1194,44 @@ function transformLine(line, idGenerator) {
|
|
|
1107
1194
|
items.push(transformInline(inline, idGenerator));
|
|
1108
1195
|
}
|
|
1109
1196
|
items.push({
|
|
1110
|
-
id: idGenerator.generate(
|
|
1111
|
-
type:
|
|
1197
|
+
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
1198
|
+
type: JSON_TYPES.LINEBREAK
|
|
1112
1199
|
});
|
|
1113
1200
|
return items;
|
|
1114
1201
|
}
|
|
1115
1202
|
function transformInline(inline, idGenerator) {
|
|
1116
1203
|
switch (inline.type) {
|
|
1117
|
-
case
|
|
1204
|
+
case AST_TYPES.FREE_TEXT:
|
|
1205
|
+
const trimmed = inline.value.trim();
|
|
1206
|
+
const operators = ["+", "-", "×", "÷", "=", "<", ">", "?"];
|
|
1207
|
+
if (operators.includes(trimmed) && inline.value.trim() === inline.value) {
|
|
1208
|
+
return {
|
|
1209
|
+
id: idGenerator.generate("operator"),
|
|
1210
|
+
type: "operator",
|
|
1211
|
+
operator: trimmed
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1118
1214
|
return {
|
|
1119
|
-
id: idGenerator.generate(
|
|
1120
|
-
type:
|
|
1215
|
+
id: idGenerator.generate(JSON_TYPES.TEXT),
|
|
1216
|
+
type: JSON_TYPES.TEXT,
|
|
1121
1217
|
value: inline.value
|
|
1122
1218
|
};
|
|
1123
|
-
case
|
|
1219
|
+
case AST_TYPES.SPACE:
|
|
1124
1220
|
return {
|
|
1125
|
-
id: idGenerator.generate(
|
|
1126
|
-
type:
|
|
1221
|
+
id: idGenerator.generate(JSON_TYPES.SPACE),
|
|
1222
|
+
type: JSON_TYPES.SPACE
|
|
1127
1223
|
};
|
|
1128
|
-
case
|
|
1224
|
+
case AST_TYPES.HIGHLIGHTED_TEXT:
|
|
1129
1225
|
return {
|
|
1130
|
-
id: idGenerator.generate(
|
|
1131
|
-
type:
|
|
1226
|
+
id: idGenerator.generate(JSON_TYPES.HIGHLIGHTED_TEXT),
|
|
1227
|
+
type: JSON_TYPES.HIGHLIGHTED_TEXT,
|
|
1132
1228
|
value: inline.value,
|
|
1133
1229
|
highlight: inline.highlight
|
|
1134
1230
|
};
|
|
1135
|
-
case
|
|
1231
|
+
case AST_TYPES.INLINE_INPUT:
|
|
1136
1232
|
return {
|
|
1137
|
-
id: idGenerator.generate(
|
|
1138
|
-
type:
|
|
1233
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
1234
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
1139
1235
|
widgetId: inline.name
|
|
1140
1236
|
};
|
|
1141
1237
|
default:
|
|
@@ -1143,16 +1239,16 @@ function transformInline(inline, idGenerator) {
|
|
|
1143
1239
|
}
|
|
1144
1240
|
}
|
|
1145
1241
|
function transformValidation(validation, answers, widgets) {
|
|
1146
|
-
if (validation.type ===
|
|
1242
|
+
if (validation.type === AST_TYPES.SIMPLE_VALIDATION) {
|
|
1147
1243
|
return {
|
|
1148
|
-
mode:
|
|
1244
|
+
mode: VALIDATION_MODES.SIMPLE,
|
|
1149
1245
|
answers
|
|
1150
1246
|
};
|
|
1151
|
-
} else if (validation.type ===
|
|
1247
|
+
} else if (validation.type === AST_TYPES.EXPRESSION_VALIDATION) {
|
|
1152
1248
|
const validVariableNames = Object.keys(widgets);
|
|
1153
1249
|
const variables = extractVariables$1(validation.expression, validVariableNames);
|
|
1154
1250
|
return {
|
|
1155
|
-
mode:
|
|
1251
|
+
mode: VALIDATION_MODES.EXPRESSION,
|
|
1156
1252
|
expression: validation.expression,
|
|
1157
1253
|
variables
|
|
1158
1254
|
};
|
|
@@ -1478,9 +1574,9 @@ function findValidCombination(expression, ranges) {
|
|
|
1478
1574
|
}
|
|
1479
1575
|
function validate(exercise, userInputs) {
|
|
1480
1576
|
switch (exercise.validation.mode) {
|
|
1481
|
-
case
|
|
1577
|
+
case VALIDATION_MODES.SIMPLE:
|
|
1482
1578
|
return validateSimple(exercise, userInputs);
|
|
1483
|
-
case
|
|
1579
|
+
case VALIDATION_MODES.EXPRESSION:
|
|
1484
1580
|
return validateExpression(exercise, userInputs);
|
|
1485
1581
|
default:
|
|
1486
1582
|
return {
|
|
@@ -1492,13 +1588,13 @@ function validate(exercise, userInputs) {
|
|
|
1492
1588
|
}
|
|
1493
1589
|
function getMissingInputs(exercise, userInputs) {
|
|
1494
1590
|
const missing = [];
|
|
1495
|
-
if (exercise.validation.mode ===
|
|
1591
|
+
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
1496
1592
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
1497
1593
|
if (!(widgetId in userInputs) || userInputs[widgetId] === "") {
|
|
1498
1594
|
missing.push(widgetId);
|
|
1499
1595
|
}
|
|
1500
1596
|
}
|
|
1501
|
-
} else if (exercise.validation.mode ===
|
|
1597
|
+
} else if (exercise.validation.mode === VALIDATION_MODES.EXPRESSION && exercise.validation.variables) {
|
|
1502
1598
|
for (const varName of exercise.validation.variables) {
|
|
1503
1599
|
if (!(varName in userInputs) || userInputs[varName] === "") {
|
|
1504
1600
|
missing.push(varName);
|
|
@@ -1514,7 +1610,7 @@ function validateInputFormats(exercise, userInputs) {
|
|
|
1514
1610
|
if (value === void 0 || value === "") {
|
|
1515
1611
|
continue;
|
|
1516
1612
|
}
|
|
1517
|
-
if (widget.type ===
|
|
1613
|
+
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.inputType === INPUT_TYPES.NUMERIC) {
|
|
1518
1614
|
const numValue = typeof value === "number" ? value : parseFloat(String(value));
|
|
1519
1615
|
if (!Number.isFinite(numValue)) {
|
|
1520
1616
|
errors[widgetId] = "Must be a valid number";
|
|
@@ -5544,7 +5640,8 @@ const InlineInput = ({
|
|
|
5544
5640
|
inputType,
|
|
5545
5641
|
length,
|
|
5546
5642
|
digits,
|
|
5547
|
-
onChange
|
|
5643
|
+
onChange,
|
|
5644
|
+
onFocus
|
|
5548
5645
|
}) => {
|
|
5549
5646
|
const handleChange = useCallback((newValue, fromDrop = false) => {
|
|
5550
5647
|
onChange(widgetId, newValue, fromDrop);
|
|
@@ -5585,6 +5682,7 @@ const InlineInput = ({
|
|
|
5585
5682
|
className: "inline-input",
|
|
5586
5683
|
value,
|
|
5587
5684
|
onChange: (e) => handleChange(e.target.value, false),
|
|
5685
|
+
onFocus: () => onFocus?.(widgetId),
|
|
5588
5686
|
maxLength: expectedLength,
|
|
5589
5687
|
style: { width: dynamicWidth }
|
|
5590
5688
|
}
|
|
@@ -5629,6 +5727,7 @@ const DragOptions = ({
|
|
|
5629
5727
|
};
|
|
5630
5728
|
const NumericKeypad = ({
|
|
5631
5729
|
value,
|
|
5730
|
+
includeOperators = false,
|
|
5632
5731
|
includeDecimal = false,
|
|
5633
5732
|
onChange
|
|
5634
5733
|
}) => {
|
|
@@ -5638,32 +5737,23 @@ const NumericKeypad = ({
|
|
|
5638
5737
|
const handleBackspace = () => {
|
|
5639
5738
|
onChange(value.slice(0, -1));
|
|
5640
5739
|
};
|
|
5641
|
-
return /* @__PURE__ */ jsxs("div", { className: "numeric-keypad"
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
)),
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
),
|
|
5659
|
-
/* @__PURE__ */ jsx(
|
|
5660
|
-
"button",
|
|
5661
|
-
{
|
|
5662
|
-
className: "keypad-btn",
|
|
5663
|
-
onClick: handleBackspace,
|
|
5664
|
-
children: "⌫"
|
|
5665
|
-
}
|
|
5666
|
-
)
|
|
5740
|
+
return /* @__PURE__ */ jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
|
|
5741
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
|
|
5742
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
|
|
5743
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
|
|
5744
|
+
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
|
|
5745
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
|
|
5746
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
|
|
5747
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
|
|
5748
|
+
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
|
|
5749
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
|
|
5750
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
|
|
5751
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
|
|
5752
|
+
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
|
|
5753
|
+
includeDecimal ? /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsx("div", {}),
|
|
5754
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
|
|
5755
|
+
/* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
|
|
5756
|
+
includeOperators && /* @__PURE__ */ jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
5667
5757
|
] });
|
|
5668
5758
|
};
|
|
5669
5759
|
function isInlineInputWidget(widget) {
|
|
@@ -5680,11 +5770,10 @@ const WidgetRenderer = ({
|
|
|
5680
5770
|
widgetId,
|
|
5681
5771
|
value,
|
|
5682
5772
|
isDropped = false,
|
|
5683
|
-
onChange
|
|
5773
|
+
onChange,
|
|
5774
|
+
onInputFocus,
|
|
5775
|
+
onKeypadChange
|
|
5684
5776
|
}) => {
|
|
5685
|
-
const handleChange = useCallback((newValue, fromDrop = false) => {
|
|
5686
|
-
onChange(widgetId, newValue, fromDrop);
|
|
5687
|
-
}, [onChange, widgetId]);
|
|
5688
5777
|
if (isInlineInputWidget(widget)) {
|
|
5689
5778
|
return /* @__PURE__ */ jsx(
|
|
5690
5779
|
InlineInput,
|
|
@@ -5695,7 +5784,8 @@ const WidgetRenderer = ({
|
|
|
5695
5784
|
inputType: widget.inputType,
|
|
5696
5785
|
length: widget.inputType === "string" ? widget.length : void 0,
|
|
5697
5786
|
digits: widget.inputType === "numeric" ? widget.digits : void 0,
|
|
5698
|
-
onChange
|
|
5787
|
+
onChange,
|
|
5788
|
+
onFocus: onInputFocus
|
|
5699
5789
|
}
|
|
5700
5790
|
);
|
|
5701
5791
|
}
|
|
@@ -5714,41 +5804,102 @@ const WidgetRenderer = ({
|
|
|
5714
5804
|
NumericKeypad,
|
|
5715
5805
|
{
|
|
5716
5806
|
value,
|
|
5807
|
+
includeOperators: widget.includeOperators,
|
|
5717
5808
|
includeDecimal: widget.includeDecimal,
|
|
5718
|
-
onChange:
|
|
5809
|
+
onChange: onKeypadChange || (() => {
|
|
5810
|
+
})
|
|
5719
5811
|
}
|
|
5720
5812
|
);
|
|
5721
5813
|
}
|
|
5722
5814
|
return null;
|
|
5723
5815
|
};
|
|
5724
|
-
function isArabic
|
|
5816
|
+
function isArabic(text) {
|
|
5725
5817
|
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
5726
5818
|
}
|
|
5819
|
+
function hasLatinChars(text) {
|
|
5820
|
+
return /[A-Za-z]/.test(text);
|
|
5821
|
+
}
|
|
5822
|
+
function itemContainsArabic(item) {
|
|
5823
|
+
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
5824
|
+
return isArabic(item.value);
|
|
5825
|
+
}
|
|
5826
|
+
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
5827
|
+
return isArabic(item.value);
|
|
5828
|
+
}
|
|
5829
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
5830
|
+
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
5831
|
+
}
|
|
5832
|
+
return false;
|
|
5833
|
+
}
|
|
5834
|
+
function itemContainsLatin(item) {
|
|
5835
|
+
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
5836
|
+
return hasLatinChars(item.value);
|
|
5837
|
+
}
|
|
5838
|
+
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
5839
|
+
return hasLatinChars(item.value);
|
|
5840
|
+
}
|
|
5841
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
5842
|
+
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
5843
|
+
}
|
|
5844
|
+
return false;
|
|
5845
|
+
}
|
|
5846
|
+
function isTextItem(item) {
|
|
5847
|
+
return item.type === JSON_TYPES.TEXT || item.type === JSON_TYPES.HIGHLIGHTED_TEXT || item.type === JSON_TYPES.OPERATOR || item.type === JSON_TYPES.SPACE || item.type === JSON_TYPES.GROUP;
|
|
5848
|
+
}
|
|
5849
|
+
function groupLineItems(items) {
|
|
5850
|
+
const groups = [];
|
|
5851
|
+
let currentTextGroup = [];
|
|
5852
|
+
for (const item of items) {
|
|
5853
|
+
if (isTextItem(item)) {
|
|
5854
|
+
currentTextGroup.push(item);
|
|
5855
|
+
} else {
|
|
5856
|
+
if (currentTextGroup.length > 0) {
|
|
5857
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
5858
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
5859
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
5860
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
5861
|
+
currentTextGroup = [];
|
|
5862
|
+
}
|
|
5863
|
+
groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
if (currentTextGroup.length > 0) {
|
|
5867
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
5868
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
5869
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
5870
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
5871
|
+
}
|
|
5872
|
+
return groups;
|
|
5873
|
+
}
|
|
5727
5874
|
const LayoutRenderer = ({
|
|
5728
5875
|
item,
|
|
5729
5876
|
widgets,
|
|
5730
5877
|
values,
|
|
5731
5878
|
droppedInputs,
|
|
5732
|
-
onChange
|
|
5879
|
+
onChange,
|
|
5880
|
+
onInputFocus
|
|
5733
5881
|
}) => {
|
|
5734
5882
|
switch (item.type) {
|
|
5735
|
-
case
|
|
5736
|
-
const hasArabic = isArabic
|
|
5883
|
+
case JSON_TYPES.QUESTION_TEXT: {
|
|
5884
|
+
const hasArabic = isArabic(item.value);
|
|
5737
5885
|
return /* @__PURE__ */ jsx("span", { className: `question-text ${hasArabic ? "question-text-arabic" : ""}`, children: item.value });
|
|
5738
5886
|
}
|
|
5739
|
-
case
|
|
5740
|
-
return /* @__PURE__ */ jsx("span", { className: `text-item ${isArabic
|
|
5741
|
-
case
|
|
5887
|
+
case JSON_TYPES.TEXT:
|
|
5888
|
+
return /* @__PURE__ */ jsx("span", { className: `text-item ${isArabic(item.value) ? "text-item-arabic" : "text-item-not-arabic"}`, children: item.value });
|
|
5889
|
+
case JSON_TYPES.HIGHLIGHTED_TEXT: {
|
|
5890
|
+
const hasArabic = isArabic(item.value);
|
|
5891
|
+
const dirAttr = !hasArabic ? "ltr" : void 0;
|
|
5742
5892
|
if (item.highlight) {
|
|
5743
5893
|
const [start, end] = item.highlight;
|
|
5744
|
-
return /* @__PURE__ */ jsxs("span", { className: "
|
|
5894
|
+
return /* @__PURE__ */ jsxs("span", { className: "highlighted-text", dir: dirAttr, children: [
|
|
5745
5895
|
item.value.slice(0, start),
|
|
5746
|
-
/* @__PURE__ */ jsx("
|
|
5896
|
+
/* @__PURE__ */ jsx("span", { className: "highlight", children: item.value.slice(start, end) }),
|
|
5747
5897
|
item.value.slice(end)
|
|
5748
5898
|
] });
|
|
5749
5899
|
}
|
|
5750
|
-
return /* @__PURE__ */ jsx("span", { className: "
|
|
5751
|
-
|
|
5900
|
+
return /* @__PURE__ */ jsx("span", { className: "highlighted-text", dir: dirAttr, children: item.value });
|
|
5901
|
+
}
|
|
5902
|
+
case JSON_TYPES.WIDGET_REF: {
|
|
5752
5903
|
const widget = widgets[item.widgetId];
|
|
5753
5904
|
if (!widget) return null;
|
|
5754
5905
|
return /* @__PURE__ */ jsx(
|
|
@@ -5758,15 +5909,18 @@ const LayoutRenderer = ({
|
|
|
5758
5909
|
widgetId: item.widgetId,
|
|
5759
5910
|
value: values[item.widgetId] || "",
|
|
5760
5911
|
isDropped: droppedInputs.has(item.widgetId),
|
|
5761
|
-
onChange
|
|
5912
|
+
onChange,
|
|
5913
|
+
onInputFocus
|
|
5762
5914
|
}
|
|
5763
5915
|
);
|
|
5764
5916
|
}
|
|
5765
|
-
case
|
|
5917
|
+
case JSON_TYPES.SPACE:
|
|
5918
|
+
return /* @__PURE__ */ jsx("span", { className: "space", children: " " });
|
|
5919
|
+
case JSON_TYPES.LINEBREAK:
|
|
5766
5920
|
return null;
|
|
5767
|
-
case
|
|
5921
|
+
case JSON_TYPES.OPERATOR:
|
|
5768
5922
|
return /* @__PURE__ */ jsx("span", { className: "operator", children: item.operator });
|
|
5769
|
-
case
|
|
5923
|
+
case JSON_TYPES.GROUP:
|
|
5770
5924
|
return /* @__PURE__ */ jsx("span", { className: "group", children: item.items.map((subItem) => /* @__PURE__ */ jsx(
|
|
5771
5925
|
LayoutRenderer,
|
|
5772
5926
|
{
|
|
@@ -5774,7 +5928,8 @@ const LayoutRenderer = ({
|
|
|
5774
5928
|
widgets,
|
|
5775
5929
|
values,
|
|
5776
5930
|
droppedInputs,
|
|
5777
|
-
onChange
|
|
5931
|
+
onChange,
|
|
5932
|
+
onInputFocus
|
|
5778
5933
|
},
|
|
5779
5934
|
subItem.id
|
|
5780
5935
|
)) });
|
|
@@ -5791,21 +5946,6 @@ const DEFAULT_RULER_TOOL = {
|
|
|
5791
5946
|
width: 300,
|
|
5792
5947
|
allowRotate: true
|
|
5793
5948
|
};
|
|
5794
|
-
function isArabic(text) {
|
|
5795
|
-
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
5796
|
-
}
|
|
5797
|
-
function itemContainsArabic(item) {
|
|
5798
|
-
if (item.type === "text" && "value" in item) {
|
|
5799
|
-
return isArabic(item.value);
|
|
5800
|
-
}
|
|
5801
|
-
if (item.type === "number-text" && "value" in item) {
|
|
5802
|
-
return isArabic(item.value);
|
|
5803
|
-
}
|
|
5804
|
-
if (item.type === "group" && "items" in item) {
|
|
5805
|
-
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
5806
|
-
}
|
|
5807
|
-
return false;
|
|
5808
|
-
}
|
|
5809
5949
|
const ExerciseRenderer = forwardRef(({
|
|
5810
5950
|
json,
|
|
5811
5951
|
onSubmit,
|
|
@@ -5815,6 +5955,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5815
5955
|
}, ref) => {
|
|
5816
5956
|
const [values, setValues] = useState({});
|
|
5817
5957
|
const [droppedInputs, setDroppedInputs] = useState(/* @__PURE__ */ new Set());
|
|
5958
|
+
const [focusedInputId, setFocusedInputId] = useState(null);
|
|
5818
5959
|
useImperativeHandle(ref, () => ({
|
|
5819
5960
|
submit: () => {
|
|
5820
5961
|
const result = validate(json, values);
|
|
@@ -5825,6 +5966,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5825
5966
|
reset: () => {
|
|
5826
5967
|
setValues({});
|
|
5827
5968
|
setDroppedInputs(/* @__PURE__ */ new Set());
|
|
5969
|
+
setFocusedInputId(null);
|
|
5828
5970
|
},
|
|
5829
5971
|
focusFirstInput: () => {
|
|
5830
5972
|
const container = document.querySelector(".math-exercise-renderer");
|
|
@@ -5855,10 +5997,20 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5855
5997
|
const groups = [];
|
|
5856
5998
|
let currentGroup = [];
|
|
5857
5999
|
for (const item of json.layout) {
|
|
5858
|
-
if (item.type ===
|
|
6000
|
+
if (item.type === JSON_TYPES.QUESTION_TEXT) {
|
|
6001
|
+
groups.push({
|
|
6002
|
+
items: [item],
|
|
6003
|
+
hasArabic: itemContainsArabic(item),
|
|
6004
|
+
subGroups: []
|
|
6005
|
+
// Question text doesn't need sub-grouping
|
|
6006
|
+
});
|
|
6007
|
+
continue;
|
|
6008
|
+
}
|
|
6009
|
+
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
5859
6010
|
if (currentGroup.length > 0) {
|
|
5860
6011
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
5861
|
-
|
|
6012
|
+
const subGroups = groupLineItems(currentGroup);
|
|
6013
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
5862
6014
|
currentGroup = [];
|
|
5863
6015
|
}
|
|
5864
6016
|
} else {
|
|
@@ -5867,7 +6019,8 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5867
6019
|
}
|
|
5868
6020
|
if (currentGroup.length > 0) {
|
|
5869
6021
|
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
5870
|
-
|
|
6022
|
+
const subGroups = groupLineItems(currentGroup);
|
|
6023
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
5871
6024
|
}
|
|
5872
6025
|
return groups;
|
|
5873
6026
|
}, [json.layout]);
|
|
@@ -5888,17 +6041,52 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5888
6041
|
"div",
|
|
5889
6042
|
{
|
|
5890
6043
|
className: `exercise-line ${group.hasArabic ? "exercise-line-arabic" : ""}`,
|
|
5891
|
-
children: group.
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
6044
|
+
children: group.subGroups.length > 0 ? (
|
|
6045
|
+
// Render sub-groups for regular lines
|
|
6046
|
+
group.subGroups.map((subGroup, subIdx) => subGroup.isTextGroup ? (
|
|
6047
|
+
// Text group - only apply RTL for pure Arabic (no mixed languages)
|
|
6048
|
+
/* @__PURE__ */ jsx("span", { className: "text-group", dir: subGroup.shouldApplyRTL ? "rtl" : void 0, children: subGroup.items.map((item) => /* @__PURE__ */ jsx(
|
|
6049
|
+
LayoutRenderer,
|
|
6050
|
+
{
|
|
6051
|
+
item,
|
|
6052
|
+
widgets: json.widgets,
|
|
6053
|
+
values,
|
|
6054
|
+
droppedInputs,
|
|
6055
|
+
onChange: handleChange,
|
|
6056
|
+
onInputFocus: setFocusedInputId
|
|
6057
|
+
},
|
|
6058
|
+
item.id
|
|
6059
|
+
)) }, subIdx)
|
|
6060
|
+
) : (
|
|
6061
|
+
// Widget group - no dir wrapper, render directly
|
|
6062
|
+
subGroup.items.map((item) => /* @__PURE__ */ jsx(
|
|
6063
|
+
LayoutRenderer,
|
|
6064
|
+
{
|
|
6065
|
+
item,
|
|
6066
|
+
widgets: json.widgets,
|
|
6067
|
+
values,
|
|
6068
|
+
droppedInputs,
|
|
6069
|
+
onChange: handleChange,
|
|
6070
|
+
onInputFocus: setFocusedInputId
|
|
6071
|
+
},
|
|
6072
|
+
item.id
|
|
6073
|
+
))
|
|
6074
|
+
))
|
|
6075
|
+
) : (
|
|
6076
|
+
// For QUESTION_TEXT items (no sub-groups)
|
|
6077
|
+
group.items.map((item) => /* @__PURE__ */ jsx(
|
|
6078
|
+
LayoutRenderer,
|
|
6079
|
+
{
|
|
6080
|
+
item,
|
|
6081
|
+
widgets: json.widgets,
|
|
6082
|
+
values,
|
|
6083
|
+
droppedInputs,
|
|
6084
|
+
onChange: handleChange,
|
|
6085
|
+
onInputFocus: setFocusedInputId
|
|
6086
|
+
},
|
|
6087
|
+
item.id
|
|
6088
|
+
))
|
|
6089
|
+
)
|
|
5902
6090
|
},
|
|
5903
6091
|
groupIdx
|
|
5904
6092
|
)) }),
|
|
@@ -5907,9 +6095,13 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5907
6095
|
{
|
|
5908
6096
|
widget: numericKeypadWidget,
|
|
5909
6097
|
widgetId: "_numeric_keypad",
|
|
5910
|
-
value: "",
|
|
5911
|
-
onChange: (
|
|
5912
|
-
|
|
6098
|
+
value: focusedInputId ? values[focusedInputId] || "" : "",
|
|
6099
|
+
onChange: () => {
|
|
6100
|
+
},
|
|
6101
|
+
onKeypadChange: (newValue) => {
|
|
6102
|
+
if (focusedInputId) {
|
|
6103
|
+
handleChange(focusedInputId, newValue, false);
|
|
6104
|
+
}
|
|
5913
6105
|
}
|
|
5914
6106
|
}
|
|
5915
6107
|
) })
|
|
@@ -5980,6 +6172,7 @@ function compileWithAST(dsl) {
|
|
|
5980
6172
|
return { ast, json };
|
|
5981
6173
|
}
|
|
5982
6174
|
export {
|
|
6175
|
+
AST_TYPES,
|
|
5983
6176
|
DragOptionsJSONSchema,
|
|
5984
6177
|
DragOptionsToolSchema,
|
|
5985
6178
|
EngineError,
|
|
@@ -5990,16 +6183,17 @@ export {
|
|
|
5990
6183
|
ExpressionValidationSchema,
|
|
5991
6184
|
FreeTextInlineSchema,
|
|
5992
6185
|
GroupItemSchema,
|
|
6186
|
+
HighlightedTextInlineSchema,
|
|
6187
|
+
HighlightedTextSchema,
|
|
5993
6188
|
InlineInputJSONSchema,
|
|
5994
6189
|
InlineInputWidgetSchema,
|
|
5995
6190
|
InlineNodeSchema,
|
|
6191
|
+
JSON_TYPES,
|
|
5996
6192
|
LayoutItemSchema,
|
|
5997
6193
|
LayoutRenderer,
|
|
5998
6194
|
LexerError,
|
|
5999
6195
|
LineBlockSchema,
|
|
6000
6196
|
LinebreakSchema,
|
|
6001
|
-
NumberTextInlineSchema,
|
|
6002
|
-
NumberTextSchema,
|
|
6003
6197
|
NumericKeypadJSONSchema,
|
|
6004
6198
|
NumericKeypadToolSchema,
|
|
6005
6199
|
OperatorItemSchema,
|
|
@@ -6024,9 +6218,8 @@ export {
|
|
|
6024
6218
|
createExerciseDocument,
|
|
6025
6219
|
createExpressionValidation,
|
|
6026
6220
|
createFreeText,
|
|
6027
|
-
|
|
6221
|
+
createHighlightedText,
|
|
6028
6222
|
createLineBlock,
|
|
6029
|
-
createNumberText,
|
|
6030
6223
|
createNumericInput,
|
|
6031
6224
|
createNumericKeypad,
|
|
6032
6225
|
createSimpleValidation,
|
|
@@ -6043,13 +6236,12 @@ export {
|
|
|
6043
6236
|
isExpressionValidation,
|
|
6044
6237
|
isFreeText,
|
|
6045
6238
|
isGroupItem,
|
|
6239
|
+
isHighlightedText,
|
|
6046
6240
|
isInlineInput,
|
|
6047
6241
|
isInlineInputJSON,
|
|
6048
6242
|
isInlineNode,
|
|
6049
6243
|
isLineBlock,
|
|
6050
6244
|
isLinebreak,
|
|
6051
|
-
isNumberText,
|
|
6052
|
-
isNumericInputJSON,
|
|
6053
6245
|
isNumericKeypad,
|
|
6054
6246
|
isNumericKeypadJSON,
|
|
6055
6247
|
isOperatorItem,
|
|
@@ -6057,11 +6249,10 @@ export {
|
|
|
6057
6249
|
isSimpleValidation,
|
|
6058
6250
|
isSpace,
|
|
6059
6251
|
isTextBlock,
|
|
6060
|
-
isTextItem,
|
|
6252
|
+
isTextItem$1 as isTextItem,
|
|
6061
6253
|
isToolNode,
|
|
6062
6254
|
isValidExpression,
|
|
6063
6255
|
isValidationNode,
|
|
6064
|
-
isWidgetPlaceholder,
|
|
6065
6256
|
isWidgetRef,
|
|
6066
6257
|
parse,
|
|
6067
6258
|
parseSafe,
|