@mathscan/math-exercise-engine 1.0.2 → 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 +2 -2
- package/dist/index.d.ts +230 -140
- package/dist/index.js +384 -191
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +384 -191
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +240 -57
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,87 @@ const mathjs = require("mathjs");
|
|
|
6
6
|
const jsxRuntime = require("react/jsx-runtime");
|
|
7
7
|
const React = require("react");
|
|
8
8
|
const require$$0 = require("react-dom");
|
|
9
|
+
const AST_TYPES = {
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// Inline Elements
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
/** Regular text without special formatting */
|
|
14
|
+
FREE_TEXT: "FreeText",
|
|
15
|
+
/** Explicit spacing element */
|
|
16
|
+
SPACE: "Space",
|
|
17
|
+
/** Text with optional character-range highlighting */
|
|
18
|
+
HIGHLIGHTED_TEXT: "HighlightedText",
|
|
19
|
+
/** Inline input widget (string or numeric) */
|
|
20
|
+
INLINE_INPUT: "InlineInput",
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// Blocks
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
/** Question/instruction text block */
|
|
25
|
+
TEXT_BLOCK: "TextBlock",
|
|
26
|
+
/** Line containing inline elements */
|
|
27
|
+
LINE_BLOCK: "LineBlock",
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// Tools
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
+
/** Drag-and-drop options tool */
|
|
32
|
+
DRAG_OPTIONS: "DragOptions",
|
|
33
|
+
/** Numeric keypad input tool */
|
|
34
|
+
NUMERIC_KEYPAD: "NumericKeypad",
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
// Validation
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
/** Simple answer matching validation */
|
|
39
|
+
SIMPLE_VALIDATION: "SimpleValidation",
|
|
40
|
+
/** Expression-based validation */
|
|
41
|
+
EXPRESSION_VALIDATION: "ExpressionValidation",
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
43
|
+
// Document
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
45
|
+
/** Root exercise document */
|
|
46
|
+
EXERCISE_DOCUMENT: "ExerciseDocument"
|
|
47
|
+
};
|
|
48
|
+
const JSON_TYPES = {
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
// Layout Items
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
52
|
+
/** Question/instruction text */
|
|
53
|
+
QUESTION_TEXT: "question-text",
|
|
54
|
+
/** Regular text element */
|
|
55
|
+
TEXT: "text",
|
|
56
|
+
/** Spacing element */
|
|
57
|
+
SPACE: "space",
|
|
58
|
+
/** Text with optional highlighting */
|
|
59
|
+
HIGHLIGHTED_TEXT: "highlighted-text",
|
|
60
|
+
/** Reference to a widget */
|
|
61
|
+
WIDGET_REF: "widget-ref",
|
|
62
|
+
/** Line break element */
|
|
63
|
+
LINEBREAK: "linebreak",
|
|
64
|
+
/** Grouped layout items */
|
|
65
|
+
GROUP: "group",
|
|
66
|
+
/** Operator symbol */
|
|
67
|
+
OPERATOR: "operator",
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
// Widget Types
|
|
70
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
71
|
+
/** Inline input widget (string or numeric) */
|
|
72
|
+
INLINE_INPUT: "inline-input",
|
|
73
|
+
/** Drag-and-drop options widget */
|
|
74
|
+
DRAG_OPTIONS: "drag-options",
|
|
75
|
+
/** Numeric keypad widget */
|
|
76
|
+
NUMERIC_KEYPAD: "numeric-keypad"
|
|
77
|
+
};
|
|
78
|
+
const VALIDATION_MODES = {
|
|
79
|
+
/** Simple answer matching validation */
|
|
80
|
+
SIMPLE: "simple",
|
|
81
|
+
/** Expression-based validation using JavaScript expressions */
|
|
82
|
+
EXPRESSION: "expression"
|
|
83
|
+
};
|
|
84
|
+
const INPUT_TYPES = {
|
|
85
|
+
/** String/text input */
|
|
86
|
+
STRING: "string",
|
|
87
|
+
/** Numeric input (digits only) */
|
|
88
|
+
NUMERIC: "numeric"
|
|
89
|
+
};
|
|
9
90
|
const Newline = chevrotain.createToken({
|
|
10
91
|
name: "Newline",
|
|
11
92
|
pattern: /\r?\n/,
|
|
@@ -132,7 +213,7 @@ const Comma = chevrotain.createToken({
|
|
|
132
213
|
});
|
|
133
214
|
const FreeText = chevrotain.createToken({
|
|
134
215
|
name: "FreeText",
|
|
135
|
-
pattern: /(?:[^A-Z\n\r]|(?![A-Z_]+\[)[A-Z])+/,
|
|
216
|
+
pattern: /(?:[^A-Z\n\r \t+\-−×*÷/=<>?()\[\],]|(?![A-Z_]+\[)[A-Z])+/,
|
|
136
217
|
line_breaks: false
|
|
137
218
|
});
|
|
138
219
|
const allTokens = [
|
|
@@ -374,7 +455,9 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
374
455
|
this.numericKeypad = this.RULE("numericKeypad", () => {
|
|
375
456
|
this.CONSUME(NUMERIC_KEYPAD);
|
|
376
457
|
this.CONSUME(LBracket);
|
|
377
|
-
this.
|
|
458
|
+
this.OPTION(() => {
|
|
459
|
+
this.SUBRULE(this.parameterList);
|
|
460
|
+
});
|
|
378
461
|
this.CONSUME(RBracket);
|
|
379
462
|
this.MANY(() => {
|
|
380
463
|
this.OR([
|
|
@@ -471,29 +554,29 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
471
554
|
}
|
|
472
555
|
const parser = new MathExerciseParser();
|
|
473
556
|
const FreeTextInlineSchema = zod.z.object({
|
|
474
|
-
type: zod.z.literal(
|
|
557
|
+
type: zod.z.literal(AST_TYPES.FREE_TEXT),
|
|
475
558
|
value: zod.z.string()
|
|
476
559
|
});
|
|
477
560
|
const SpaceInlineSchema = zod.z.object({
|
|
478
|
-
type: zod.z.literal(
|
|
561
|
+
type: zod.z.literal(AST_TYPES.SPACE)
|
|
479
562
|
});
|
|
480
|
-
const
|
|
481
|
-
type: zod.z.literal(
|
|
563
|
+
const HighlightedTextInlineSchema = zod.z.object({
|
|
564
|
+
type: zod.z.literal(AST_TYPES.HIGHLIGHTED_TEXT),
|
|
482
565
|
value: zod.z.string(),
|
|
483
566
|
highlight: zod.z.tuple([zod.z.number(), zod.z.number()]).optional()
|
|
484
567
|
});
|
|
485
568
|
const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
|
|
486
569
|
zod.z.object({
|
|
487
|
-
type: zod.z.literal(
|
|
570
|
+
type: zod.z.literal(AST_TYPES.INLINE_INPUT),
|
|
488
571
|
name: zod.z.string(),
|
|
489
|
-
inputType: zod.z.literal(
|
|
572
|
+
inputType: zod.z.literal(INPUT_TYPES.STRING),
|
|
490
573
|
length: zod.z.number().int().positive(),
|
|
491
574
|
answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
|
|
492
575
|
}),
|
|
493
576
|
zod.z.object({
|
|
494
|
-
type: zod.z.literal(
|
|
577
|
+
type: zod.z.literal(AST_TYPES.INLINE_INPUT),
|
|
495
578
|
name: zod.z.string(),
|
|
496
|
-
inputType: zod.z.literal(
|
|
579
|
+
inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
|
|
497
580
|
digits: zod.z.number().int().positive(),
|
|
498
581
|
answer: zod.z.union([zod.z.string(), zod.z.number()]).optional()
|
|
499
582
|
})
|
|
@@ -501,41 +584,41 @@ const InlineInputWidgetSchema = zod.z.discriminatedUnion("inputType", [
|
|
|
501
584
|
const InlineNodeSchema = zod.z.union([
|
|
502
585
|
FreeTextInlineSchema,
|
|
503
586
|
SpaceInlineSchema,
|
|
504
|
-
|
|
587
|
+
HighlightedTextInlineSchema,
|
|
505
588
|
InlineInputWidgetSchema
|
|
506
589
|
]);
|
|
507
590
|
function createFreeText(value) {
|
|
508
591
|
return FreeTextInlineSchema.parse({
|
|
509
|
-
type:
|
|
592
|
+
type: AST_TYPES.FREE_TEXT,
|
|
510
593
|
value
|
|
511
594
|
});
|
|
512
595
|
}
|
|
513
596
|
function createSpace() {
|
|
514
597
|
return SpaceInlineSchema.parse({
|
|
515
|
-
type:
|
|
598
|
+
type: AST_TYPES.SPACE
|
|
516
599
|
});
|
|
517
600
|
}
|
|
518
|
-
function
|
|
519
|
-
return
|
|
520
|
-
type:
|
|
601
|
+
function createHighlightedText(value, highlight) {
|
|
602
|
+
return HighlightedTextInlineSchema.parse({
|
|
603
|
+
type: AST_TYPES.HIGHLIGHTED_TEXT,
|
|
521
604
|
value,
|
|
522
605
|
highlight
|
|
523
606
|
});
|
|
524
607
|
}
|
|
525
608
|
function createStringInput(name, length, answer) {
|
|
526
609
|
return InlineInputWidgetSchema.parse({
|
|
527
|
-
type:
|
|
610
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
528
611
|
name,
|
|
529
|
-
inputType:
|
|
612
|
+
inputType: INPUT_TYPES.STRING,
|
|
530
613
|
length,
|
|
531
614
|
answer
|
|
532
615
|
});
|
|
533
616
|
}
|
|
534
617
|
function createNumericInput(name, digits, answer) {
|
|
535
618
|
return InlineInputWidgetSchema.parse({
|
|
536
|
-
type:
|
|
619
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
537
620
|
name,
|
|
538
|
-
inputType:
|
|
621
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
539
622
|
digits,
|
|
540
623
|
answer
|
|
541
624
|
});
|
|
@@ -553,22 +636,22 @@ function isInlineNode(value) {
|
|
|
553
636
|
return InlineNodeSchema.safeParse(value).success;
|
|
554
637
|
}
|
|
555
638
|
const TextBlockSchema = zod.z.object({
|
|
556
|
-
type: zod.z.literal(
|
|
639
|
+
type: zod.z.literal(AST_TYPES.TEXT_BLOCK),
|
|
557
640
|
value: zod.z.string()
|
|
558
641
|
});
|
|
559
642
|
const LineBlockSchema = zod.z.object({
|
|
560
|
-
type: zod.z.literal(
|
|
643
|
+
type: zod.z.literal(AST_TYPES.LINE_BLOCK),
|
|
561
644
|
inlines: zod.z.array(InlineNodeSchema)
|
|
562
645
|
});
|
|
563
646
|
function createTextBlock(value) {
|
|
564
647
|
return TextBlockSchema.parse({
|
|
565
|
-
type:
|
|
648
|
+
type: AST_TYPES.TEXT_BLOCK,
|
|
566
649
|
value
|
|
567
650
|
});
|
|
568
651
|
}
|
|
569
652
|
function createLineBlock(inlines) {
|
|
570
653
|
return LineBlockSchema.parse({
|
|
571
|
-
type:
|
|
654
|
+
type: AST_TYPES.LINE_BLOCK,
|
|
572
655
|
inlines
|
|
573
656
|
});
|
|
574
657
|
}
|
|
@@ -579,13 +662,13 @@ function isLineBlock(value) {
|
|
|
579
662
|
return LineBlockSchema.safeParse(value).success;
|
|
580
663
|
}
|
|
581
664
|
const DragOptionsToolSchema = zod.z.object({
|
|
582
|
-
type: zod.z.literal(
|
|
665
|
+
type: zod.z.literal(AST_TYPES.DRAG_OPTIONS),
|
|
583
666
|
options: zod.z.array(zod.z.string()).min(1)
|
|
584
667
|
});
|
|
585
668
|
const NumericKeypadToolSchema = zod.z.object({
|
|
586
|
-
type: zod.z.literal(
|
|
587
|
-
|
|
588
|
-
|
|
669
|
+
type: zod.z.literal(AST_TYPES.NUMERIC_KEYPAD),
|
|
670
|
+
includeOperators: zod.z.boolean().optional(),
|
|
671
|
+
includeDecimal: zod.z.boolean().optional()
|
|
589
672
|
});
|
|
590
673
|
const ToolNodeSchema = zod.z.discriminatedUnion("type", [
|
|
591
674
|
DragOptionsToolSchema,
|
|
@@ -593,15 +676,15 @@ const ToolNodeSchema = zod.z.discriminatedUnion("type", [
|
|
|
593
676
|
]);
|
|
594
677
|
function createDragOptions(options) {
|
|
595
678
|
return DragOptionsToolSchema.parse({
|
|
596
|
-
type:
|
|
679
|
+
type: AST_TYPES.DRAG_OPTIONS,
|
|
597
680
|
options
|
|
598
681
|
});
|
|
599
682
|
}
|
|
600
|
-
function createNumericKeypad(
|
|
683
|
+
function createNumericKeypad(includeOperators, includeDecimal) {
|
|
601
684
|
return NumericKeypadToolSchema.parse({
|
|
602
|
-
type:
|
|
603
|
-
|
|
604
|
-
|
|
685
|
+
type: AST_TYPES.NUMERIC_KEYPAD,
|
|
686
|
+
includeOperators,
|
|
687
|
+
includeDecimal
|
|
605
688
|
});
|
|
606
689
|
}
|
|
607
690
|
function isDragOptions(value) {
|
|
@@ -614,10 +697,10 @@ function isToolNode(value) {
|
|
|
614
697
|
return ToolNodeSchema.safeParse(value).success;
|
|
615
698
|
}
|
|
616
699
|
const SimpleValidationSchema = zod.z.object({
|
|
617
|
-
type: zod.z.literal(
|
|
700
|
+
type: zod.z.literal(AST_TYPES.SIMPLE_VALIDATION)
|
|
618
701
|
});
|
|
619
702
|
const ExpressionValidationSchema = zod.z.object({
|
|
620
|
-
type: zod.z.literal(
|
|
703
|
+
type: zod.z.literal(AST_TYPES.EXPRESSION_VALIDATION),
|
|
621
704
|
expression: zod.z.string()
|
|
622
705
|
});
|
|
623
706
|
const ValidationNodeSchema = zod.z.discriminatedUnion("type", [
|
|
@@ -626,12 +709,12 @@ const ValidationNodeSchema = zod.z.discriminatedUnion("type", [
|
|
|
626
709
|
]);
|
|
627
710
|
function createSimpleValidation() {
|
|
628
711
|
return SimpleValidationSchema.parse({
|
|
629
|
-
type:
|
|
712
|
+
type: AST_TYPES.SIMPLE_VALIDATION
|
|
630
713
|
});
|
|
631
714
|
}
|
|
632
715
|
function createExpressionValidation(expression) {
|
|
633
716
|
return ExpressionValidationSchema.parse({
|
|
634
|
-
type:
|
|
717
|
+
type: AST_TYPES.EXPRESSION_VALIDATION,
|
|
635
718
|
expression
|
|
636
719
|
});
|
|
637
720
|
}
|
|
@@ -645,7 +728,7 @@ function isValidationNode(value) {
|
|
|
645
728
|
return ValidationNodeSchema.safeParse(value).success;
|
|
646
729
|
}
|
|
647
730
|
const ExerciseDocumentSchema = zod.z.object({
|
|
648
|
-
type: zod.z.literal(
|
|
731
|
+
type: zod.z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
649
732
|
question: TextBlockSchema.optional(),
|
|
650
733
|
lines: zod.z.array(LineBlockSchema),
|
|
651
734
|
tools: zod.z.array(ToolNodeSchema).optional(),
|
|
@@ -653,7 +736,7 @@ const ExerciseDocumentSchema = zod.z.object({
|
|
|
653
736
|
});
|
|
654
737
|
function createExerciseDocument(question, lines, validation, tools) {
|
|
655
738
|
return ExerciseDocumentSchema.parse({
|
|
656
|
-
type:
|
|
739
|
+
type: AST_TYPES.EXERCISE_DOCUMENT,
|
|
657
740
|
question,
|
|
658
741
|
lines,
|
|
659
742
|
tools,
|
|
@@ -773,7 +856,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
773
856
|
if (params.start !== void 0 && params.end !== void 0) {
|
|
774
857
|
highlight = [params.start, params.end];
|
|
775
858
|
}
|
|
776
|
-
return
|
|
859
|
+
return createHighlightedText(value, highlight);
|
|
777
860
|
}
|
|
778
861
|
inlineInput(ctx) {
|
|
779
862
|
const params = this.visit(ctx.parameterList);
|
|
@@ -803,9 +886,10 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
803
886
|
return createDragOptions(strings);
|
|
804
887
|
}
|
|
805
888
|
numericKeypad(ctx) {
|
|
806
|
-
const params = this.visit(ctx.parameterList);
|
|
889
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
890
|
+
const includeOperators = params.includeOperators === true || params.includeOperators === "true";
|
|
807
891
|
const includeDecimal = params.includeDecimal === true || params.includeDecimal === "true";
|
|
808
|
-
return createNumericKeypad(includeDecimal);
|
|
892
|
+
return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0);
|
|
809
893
|
}
|
|
810
894
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
811
895
|
// Validation
|
|
@@ -897,33 +981,33 @@ function toAST(cst) {
|
|
|
897
981
|
}
|
|
898
982
|
const TextItemSchema = zod.z.object({
|
|
899
983
|
id: zod.z.string(),
|
|
900
|
-
type: zod.z.literal(
|
|
984
|
+
type: zod.z.literal(JSON_TYPES.TEXT),
|
|
901
985
|
value: zod.z.string()
|
|
902
986
|
});
|
|
903
987
|
const QuestionTextSchema = zod.z.object({
|
|
904
988
|
id: zod.z.string(),
|
|
905
|
-
type: zod.z.literal(
|
|
989
|
+
type: zod.z.literal(JSON_TYPES.QUESTION_TEXT),
|
|
906
990
|
value: zod.z.string()
|
|
907
991
|
});
|
|
908
992
|
const WidgetRefSchema = zod.z.object({
|
|
909
993
|
id: zod.z.string(),
|
|
910
|
-
type: zod.z.literal(
|
|
994
|
+
type: zod.z.literal(JSON_TYPES.WIDGET_REF),
|
|
911
995
|
widgetId: zod.z.string()
|
|
912
996
|
});
|
|
913
|
-
const
|
|
997
|
+
const HighlightedTextSchema = zod.z.object({
|
|
914
998
|
id: zod.z.string(),
|
|
915
|
-
type: zod.z.literal(
|
|
999
|
+
type: zod.z.literal(JSON_TYPES.HIGHLIGHTED_TEXT),
|
|
916
1000
|
value: zod.z.string(),
|
|
917
1001
|
highlight: zod.z.tuple([zod.z.number(), zod.z.number()]).optional()
|
|
918
1002
|
});
|
|
919
1003
|
const LinebreakSchema = zod.z.object({
|
|
920
1004
|
id: zod.z.string(),
|
|
921
|
-
type: zod.z.literal(
|
|
1005
|
+
type: zod.z.literal(JSON_TYPES.LINEBREAK)
|
|
922
1006
|
});
|
|
923
1007
|
const OperatorSchema = zod.z.enum(["+", "-", "×", "÷", "=", "<", ">", "?"]);
|
|
924
1008
|
const OperatorItemSchema = zod.z.object({
|
|
925
1009
|
id: zod.z.string(),
|
|
926
|
-
type: zod.z.literal(
|
|
1010
|
+
type: zod.z.literal(JSON_TYPES.OPERATOR),
|
|
927
1011
|
operator: OperatorSchema
|
|
928
1012
|
});
|
|
929
1013
|
const LayoutItemSchema = zod.z.lazy(
|
|
@@ -931,7 +1015,7 @@ const LayoutItemSchema = zod.z.lazy(
|
|
|
931
1015
|
QuestionTextSchema,
|
|
932
1016
|
TextItemSchema,
|
|
933
1017
|
WidgetRefSchema,
|
|
934
|
-
|
|
1018
|
+
HighlightedTextSchema,
|
|
935
1019
|
LinebreakSchema,
|
|
936
1020
|
GroupItemSchema,
|
|
937
1021
|
OperatorItemSchema
|
|
@@ -939,17 +1023,17 @@ const LayoutItemSchema = zod.z.lazy(
|
|
|
939
1023
|
);
|
|
940
1024
|
const GroupItemSchema = zod.z.object({
|
|
941
1025
|
id: zod.z.string(),
|
|
942
|
-
type: zod.z.literal(
|
|
1026
|
+
type: zod.z.literal(JSON_TYPES.GROUP),
|
|
943
1027
|
items: zod.z.array(LayoutItemSchema)
|
|
944
1028
|
});
|
|
945
1029
|
const StringInputJSONSchema = zod.z.object({
|
|
946
|
-
type: zod.z.literal(
|
|
947
|
-
inputType: zod.z.literal(
|
|
1030
|
+
type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1031
|
+
inputType: zod.z.literal(INPUT_TYPES.STRING),
|
|
948
1032
|
length: zod.z.number().int().positive()
|
|
949
1033
|
});
|
|
950
1034
|
const NumericInputJSONSchema = zod.z.object({
|
|
951
|
-
type: zod.z.literal(
|
|
952
|
-
inputType: zod.z.literal(
|
|
1035
|
+
type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1036
|
+
inputType: zod.z.literal(INPUT_TYPES.NUMERIC),
|
|
953
1037
|
digits: zod.z.number().int().positive()
|
|
954
1038
|
});
|
|
955
1039
|
const InlineInputJSONSchema = zod.z.discriminatedUnion("inputType", [
|
|
@@ -957,12 +1041,13 @@ const InlineInputJSONSchema = zod.z.discriminatedUnion("inputType", [
|
|
|
957
1041
|
NumericInputJSONSchema
|
|
958
1042
|
]);
|
|
959
1043
|
const DragOptionsJSONSchema = zod.z.object({
|
|
960
|
-
type: zod.z.literal(
|
|
1044
|
+
type: zod.z.literal(JSON_TYPES.DRAG_OPTIONS),
|
|
961
1045
|
options: zod.z.array(zod.z.string())
|
|
962
1046
|
});
|
|
963
1047
|
const NumericKeypadJSONSchema = zod.z.object({
|
|
964
|
-
type: zod.z.literal(
|
|
965
|
-
|
|
1048
|
+
type: zod.z.literal(JSON_TYPES.NUMERIC_KEYPAD),
|
|
1049
|
+
includeOperators: zod.z.boolean().optional(),
|
|
1050
|
+
includeDecimal: zod.z.boolean().optional()
|
|
966
1051
|
});
|
|
967
1052
|
const WidgetJSONSchema = zod.z.union([
|
|
968
1053
|
InlineInputJSONSchema,
|
|
@@ -970,7 +1055,7 @@ const WidgetJSONSchema = zod.z.union([
|
|
|
970
1055
|
NumericKeypadJSONSchema
|
|
971
1056
|
]);
|
|
972
1057
|
const ValidationJSONSchema = zod.z.object({
|
|
973
|
-
mode: zod.z.enum([
|
|
1058
|
+
mode: zod.z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
|
|
974
1059
|
answers: zod.z.record(zod.z.union([zod.z.string(), zod.z.number()])).optional(),
|
|
975
1060
|
expression: zod.z.string().optional(),
|
|
976
1061
|
variables: zod.z.array(zod.z.string()).optional()
|
|
@@ -1002,34 +1087,34 @@ function validateWidgetJSONSafe(data) {
|
|
|
1002
1087
|
return WidgetJSONSchema.safeParse(data);
|
|
1003
1088
|
}
|
|
1004
1089
|
function isQuestionText(item) {
|
|
1005
|
-
return item.type ===
|
|
1090
|
+
return item.type === JSON_TYPES.QUESTION_TEXT;
|
|
1006
1091
|
}
|
|
1007
|
-
function isTextItem(item) {
|
|
1008
|
-
return item.type ===
|
|
1092
|
+
function isTextItem$1(item) {
|
|
1093
|
+
return item.type === JSON_TYPES.TEXT;
|
|
1009
1094
|
}
|
|
1010
1095
|
function isWidgetRef(item) {
|
|
1011
|
-
return item.type ===
|
|
1096
|
+
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1012
1097
|
}
|
|
1013
|
-
function
|
|
1014
|
-
return item.type ===
|
|
1098
|
+
function isHighlightedText(item) {
|
|
1099
|
+
return item.type === JSON_TYPES.HIGHLIGHTED_TEXT;
|
|
1015
1100
|
}
|
|
1016
1101
|
function isLinebreak(item) {
|
|
1017
|
-
return item.type ===
|
|
1102
|
+
return item.type === JSON_TYPES.LINEBREAK;
|
|
1018
1103
|
}
|
|
1019
1104
|
function isGroupItem(item) {
|
|
1020
|
-
return item.type ===
|
|
1105
|
+
return item.type === JSON_TYPES.GROUP;
|
|
1021
1106
|
}
|
|
1022
1107
|
function isOperatorItem(item) {
|
|
1023
|
-
return item.type ===
|
|
1108
|
+
return item.type === JSON_TYPES.OPERATOR;
|
|
1024
1109
|
}
|
|
1025
1110
|
function isInlineInputJSON(widget) {
|
|
1026
|
-
return widget.type ===
|
|
1111
|
+
return widget.type === JSON_TYPES.INLINE_INPUT;
|
|
1027
1112
|
}
|
|
1028
1113
|
function isDragOptionsJSON(widget) {
|
|
1029
|
-
return widget.type ===
|
|
1114
|
+
return widget.type === JSON_TYPES.DRAG_OPTIONS;
|
|
1030
1115
|
}
|
|
1031
1116
|
function isNumericKeypadJSON(widget) {
|
|
1032
|
-
return widget.type ===
|
|
1117
|
+
return widget.type === JSON_TYPES.NUMERIC_KEYPAD;
|
|
1033
1118
|
}
|
|
1034
1119
|
class LayoutIdGenerator {
|
|
1035
1120
|
constructor() {
|
|
@@ -1047,8 +1132,8 @@ function toJSON(ast) {
|
|
|
1047
1132
|
const layout = [];
|
|
1048
1133
|
if (ast.question) {
|
|
1049
1134
|
layout.push({
|
|
1050
|
-
id: idGenerator.generate(
|
|
1051
|
-
type:
|
|
1135
|
+
id: idGenerator.generate(JSON_TYPES.QUESTION_TEXT),
|
|
1136
|
+
type: JSON_TYPES.QUESTION_TEXT,
|
|
1052
1137
|
value: ast.question.value
|
|
1053
1138
|
});
|
|
1054
1139
|
}
|
|
@@ -1060,17 +1145,17 @@ function toJSON(ast) {
|
|
|
1060
1145
|
const answers = {};
|
|
1061
1146
|
for (const line of ast.lines) {
|
|
1062
1147
|
for (const inline of line.inlines) {
|
|
1063
|
-
if (inline.type ===
|
|
1064
|
-
if (inline.inputType ===
|
|
1148
|
+
if (inline.type === AST_TYPES.INLINE_INPUT) {
|
|
1149
|
+
if (inline.inputType === INPUT_TYPES.STRING) {
|
|
1065
1150
|
widgets[inline.name] = {
|
|
1066
|
-
type:
|
|
1067
|
-
inputType:
|
|
1151
|
+
type: JSON_TYPES.INLINE_INPUT,
|
|
1152
|
+
inputType: INPUT_TYPES.STRING,
|
|
1068
1153
|
length: inline.length
|
|
1069
1154
|
};
|
|
1070
1155
|
} else {
|
|
1071
1156
|
widgets[inline.name] = {
|
|
1072
|
-
type:
|
|
1073
|
-
inputType:
|
|
1157
|
+
type: JSON_TYPES.INLINE_INPUT,
|
|
1158
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
1074
1159
|
digits: inline.digits
|
|
1075
1160
|
};
|
|
1076
1161
|
}
|
|
@@ -1082,14 +1167,15 @@ function toJSON(ast) {
|
|
|
1082
1167
|
}
|
|
1083
1168
|
if (ast.tools) {
|
|
1084
1169
|
for (const tool of ast.tools) {
|
|
1085
|
-
if (tool.type ===
|
|
1170
|
+
if (tool.type === AST_TYPES.DRAG_OPTIONS) {
|
|
1086
1171
|
widgets["_drag_options"] = {
|
|
1087
|
-
type:
|
|
1172
|
+
type: JSON_TYPES.DRAG_OPTIONS,
|
|
1088
1173
|
options: tool.options
|
|
1089
1174
|
};
|
|
1090
|
-
} else if (tool.type ===
|
|
1175
|
+
} else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
|
|
1091
1176
|
widgets["_numeric_keypad"] = {
|
|
1092
|
-
type:
|
|
1177
|
+
type: JSON_TYPES.NUMERIC_KEYPAD,
|
|
1178
|
+
includeOperators: tool.includeOperators,
|
|
1093
1179
|
includeDecimal: tool.includeDecimal
|
|
1094
1180
|
};
|
|
1095
1181
|
}
|
|
@@ -1110,35 +1196,44 @@ function transformLine(line, idGenerator) {
|
|
|
1110
1196
|
items.push(transformInline(inline, idGenerator));
|
|
1111
1197
|
}
|
|
1112
1198
|
items.push({
|
|
1113
|
-
id: idGenerator.generate(
|
|
1114
|
-
type:
|
|
1199
|
+
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
1200
|
+
type: JSON_TYPES.LINEBREAK
|
|
1115
1201
|
});
|
|
1116
1202
|
return items;
|
|
1117
1203
|
}
|
|
1118
1204
|
function transformInline(inline, idGenerator) {
|
|
1119
1205
|
switch (inline.type) {
|
|
1120
|
-
case
|
|
1206
|
+
case AST_TYPES.FREE_TEXT:
|
|
1207
|
+
const trimmed = inline.value.trim();
|
|
1208
|
+
const operators = ["+", "-", "×", "÷", "=", "<", ">", "?"];
|
|
1209
|
+
if (operators.includes(trimmed) && inline.value.trim() === inline.value) {
|
|
1210
|
+
return {
|
|
1211
|
+
id: idGenerator.generate("operator"),
|
|
1212
|
+
type: "operator",
|
|
1213
|
+
operator: trimmed
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1121
1216
|
return {
|
|
1122
|
-
id: idGenerator.generate(
|
|
1123
|
-
type:
|
|
1217
|
+
id: idGenerator.generate(JSON_TYPES.TEXT),
|
|
1218
|
+
type: JSON_TYPES.TEXT,
|
|
1124
1219
|
value: inline.value
|
|
1125
1220
|
};
|
|
1126
|
-
case
|
|
1221
|
+
case AST_TYPES.SPACE:
|
|
1127
1222
|
return {
|
|
1128
|
-
id: idGenerator.generate(
|
|
1129
|
-
type:
|
|
1223
|
+
id: idGenerator.generate(JSON_TYPES.SPACE),
|
|
1224
|
+
type: JSON_TYPES.SPACE
|
|
1130
1225
|
};
|
|
1131
|
-
case
|
|
1226
|
+
case AST_TYPES.HIGHLIGHTED_TEXT:
|
|
1132
1227
|
return {
|
|
1133
|
-
id: idGenerator.generate(
|
|
1134
|
-
type:
|
|
1228
|
+
id: idGenerator.generate(JSON_TYPES.HIGHLIGHTED_TEXT),
|
|
1229
|
+
type: JSON_TYPES.HIGHLIGHTED_TEXT,
|
|
1135
1230
|
value: inline.value,
|
|
1136
1231
|
highlight: inline.highlight
|
|
1137
1232
|
};
|
|
1138
|
-
case
|
|
1233
|
+
case AST_TYPES.INLINE_INPUT:
|
|
1139
1234
|
return {
|
|
1140
|
-
id: idGenerator.generate(
|
|
1141
|
-
type:
|
|
1235
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
1236
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
1142
1237
|
widgetId: inline.name
|
|
1143
1238
|
};
|
|
1144
1239
|
default:
|
|
@@ -1146,16 +1241,16 @@ function transformInline(inline, idGenerator) {
|
|
|
1146
1241
|
}
|
|
1147
1242
|
}
|
|
1148
1243
|
function transformValidation(validation, answers, widgets) {
|
|
1149
|
-
if (validation.type ===
|
|
1244
|
+
if (validation.type === AST_TYPES.SIMPLE_VALIDATION) {
|
|
1150
1245
|
return {
|
|
1151
|
-
mode:
|
|
1246
|
+
mode: VALIDATION_MODES.SIMPLE,
|
|
1152
1247
|
answers
|
|
1153
1248
|
};
|
|
1154
|
-
} else if (validation.type ===
|
|
1249
|
+
} else if (validation.type === AST_TYPES.EXPRESSION_VALIDATION) {
|
|
1155
1250
|
const validVariableNames = Object.keys(widgets);
|
|
1156
1251
|
const variables = extractVariables$1(validation.expression, validVariableNames);
|
|
1157
1252
|
return {
|
|
1158
|
-
mode:
|
|
1253
|
+
mode: VALIDATION_MODES.EXPRESSION,
|
|
1159
1254
|
expression: validation.expression,
|
|
1160
1255
|
variables
|
|
1161
1256
|
};
|
|
@@ -1481,9 +1576,9 @@ function findValidCombination(expression, ranges) {
|
|
|
1481
1576
|
}
|
|
1482
1577
|
function validate(exercise, userInputs) {
|
|
1483
1578
|
switch (exercise.validation.mode) {
|
|
1484
|
-
case
|
|
1579
|
+
case VALIDATION_MODES.SIMPLE:
|
|
1485
1580
|
return validateSimple(exercise, userInputs);
|
|
1486
|
-
case
|
|
1581
|
+
case VALIDATION_MODES.EXPRESSION:
|
|
1487
1582
|
return validateExpression(exercise, userInputs);
|
|
1488
1583
|
default:
|
|
1489
1584
|
return {
|
|
@@ -1495,13 +1590,13 @@ function validate(exercise, userInputs) {
|
|
|
1495
1590
|
}
|
|
1496
1591
|
function getMissingInputs(exercise, userInputs) {
|
|
1497
1592
|
const missing = [];
|
|
1498
|
-
if (exercise.validation.mode ===
|
|
1593
|
+
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
1499
1594
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
1500
1595
|
if (!(widgetId in userInputs) || userInputs[widgetId] === "") {
|
|
1501
1596
|
missing.push(widgetId);
|
|
1502
1597
|
}
|
|
1503
1598
|
}
|
|
1504
|
-
} else if (exercise.validation.mode ===
|
|
1599
|
+
} else if (exercise.validation.mode === VALIDATION_MODES.EXPRESSION && exercise.validation.variables) {
|
|
1505
1600
|
for (const varName of exercise.validation.variables) {
|
|
1506
1601
|
if (!(varName in userInputs) || userInputs[varName] === "") {
|
|
1507
1602
|
missing.push(varName);
|
|
@@ -1517,7 +1612,7 @@ function validateInputFormats(exercise, userInputs) {
|
|
|
1517
1612
|
if (value === void 0 || value === "") {
|
|
1518
1613
|
continue;
|
|
1519
1614
|
}
|
|
1520
|
-
if (widget.type ===
|
|
1615
|
+
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.inputType === INPUT_TYPES.NUMERIC) {
|
|
1521
1616
|
const numValue = typeof value === "number" ? value : parseFloat(String(value));
|
|
1522
1617
|
if (!Number.isFinite(numValue)) {
|
|
1523
1618
|
errors[widgetId] = "Must be a valid number";
|
|
@@ -5547,7 +5642,8 @@ const InlineInput = ({
|
|
|
5547
5642
|
inputType,
|
|
5548
5643
|
length,
|
|
5549
5644
|
digits,
|
|
5550
|
-
onChange
|
|
5645
|
+
onChange,
|
|
5646
|
+
onFocus
|
|
5551
5647
|
}) => {
|
|
5552
5648
|
const handleChange = React.useCallback((newValue, fromDrop = false) => {
|
|
5553
5649
|
onChange(widgetId, newValue, fromDrop);
|
|
@@ -5588,6 +5684,7 @@ const InlineInput = ({
|
|
|
5588
5684
|
className: "inline-input",
|
|
5589
5685
|
value,
|
|
5590
5686
|
onChange: (e) => handleChange(e.target.value, false),
|
|
5687
|
+
onFocus: () => onFocus?.(widgetId),
|
|
5591
5688
|
maxLength: expectedLength,
|
|
5592
5689
|
style: { width: dynamicWidth }
|
|
5593
5690
|
}
|
|
@@ -5632,6 +5729,7 @@ const DragOptions = ({
|
|
|
5632
5729
|
};
|
|
5633
5730
|
const NumericKeypad = ({
|
|
5634
5731
|
value,
|
|
5732
|
+
includeOperators = false,
|
|
5635
5733
|
includeDecimal = false,
|
|
5636
5734
|
onChange
|
|
5637
5735
|
}) => {
|
|
@@ -5641,32 +5739,23 @@ const NumericKeypad = ({
|
|
|
5641
5739
|
const handleBackspace = () => {
|
|
5642
5740
|
onChange(value.slice(0, -1));
|
|
5643
5741
|
};
|
|
5644
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "numeric-keypad"
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
)),
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
),
|
|
5662
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5663
|
-
"button",
|
|
5664
|
-
{
|
|
5665
|
-
className: "keypad-btn",
|
|
5666
|
-
onClick: handleBackspace,
|
|
5667
|
-
children: "⌫"
|
|
5668
|
-
}
|
|
5669
|
-
)
|
|
5742
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `numeric-keypad ${includeOperators ? "numeric-keypad-with-operators" : ""}`, children: [
|
|
5743
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("1"), children: "1" }),
|
|
5744
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("2"), children: "2" }),
|
|
5745
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("3"), children: "3" }),
|
|
5746
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("+"), children: "+" }),
|
|
5747
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("4"), children: "4" }),
|
|
5748
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("5"), children: "5" }),
|
|
5749
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("6"), children: "6" }),
|
|
5750
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("-"), children: "-" }),
|
|
5751
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("7"), children: "7" }),
|
|
5752
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("8"), children: "8" }),
|
|
5753
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("9"), children: "9" }),
|
|
5754
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("×"), children: "×" }),
|
|
5755
|
+
includeDecimal ? /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-decimal", onClick: () => handleDigit(","), children: "," }) : /* @__PURE__ */ jsxRuntime.jsx("div", {}),
|
|
5756
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-digit", onClick: () => handleDigit("0"), children: "0" }),
|
|
5757
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-backspace", onClick: handleBackspace, children: "⌫" }),
|
|
5758
|
+
includeOperators && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "keypad-btn keypad-btn-operator", onClick: () => handleDigit("÷"), children: "÷" })
|
|
5670
5759
|
] });
|
|
5671
5760
|
};
|
|
5672
5761
|
function isInlineInputWidget(widget) {
|
|
@@ -5683,11 +5772,10 @@ const WidgetRenderer = ({
|
|
|
5683
5772
|
widgetId,
|
|
5684
5773
|
value,
|
|
5685
5774
|
isDropped = false,
|
|
5686
|
-
onChange
|
|
5775
|
+
onChange,
|
|
5776
|
+
onInputFocus,
|
|
5777
|
+
onKeypadChange
|
|
5687
5778
|
}) => {
|
|
5688
|
-
const handleChange = React.useCallback((newValue, fromDrop = false) => {
|
|
5689
|
-
onChange(widgetId, newValue, fromDrop);
|
|
5690
|
-
}, [onChange, widgetId]);
|
|
5691
5779
|
if (isInlineInputWidget(widget)) {
|
|
5692
5780
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
5693
5781
|
InlineInput,
|
|
@@ -5698,7 +5786,8 @@ const WidgetRenderer = ({
|
|
|
5698
5786
|
inputType: widget.inputType,
|
|
5699
5787
|
length: widget.inputType === "string" ? widget.length : void 0,
|
|
5700
5788
|
digits: widget.inputType === "numeric" ? widget.digits : void 0,
|
|
5701
|
-
onChange
|
|
5789
|
+
onChange,
|
|
5790
|
+
onFocus: onInputFocus
|
|
5702
5791
|
}
|
|
5703
5792
|
);
|
|
5704
5793
|
}
|
|
@@ -5717,41 +5806,102 @@ const WidgetRenderer = ({
|
|
|
5717
5806
|
NumericKeypad,
|
|
5718
5807
|
{
|
|
5719
5808
|
value,
|
|
5809
|
+
includeOperators: widget.includeOperators,
|
|
5720
5810
|
includeDecimal: widget.includeDecimal,
|
|
5721
|
-
onChange:
|
|
5811
|
+
onChange: onKeypadChange || (() => {
|
|
5812
|
+
})
|
|
5722
5813
|
}
|
|
5723
5814
|
);
|
|
5724
5815
|
}
|
|
5725
5816
|
return null;
|
|
5726
5817
|
};
|
|
5727
|
-
function isArabic
|
|
5818
|
+
function isArabic(text) {
|
|
5728
5819
|
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
5729
5820
|
}
|
|
5821
|
+
function hasLatinChars(text) {
|
|
5822
|
+
return /[A-Za-z]/.test(text);
|
|
5823
|
+
}
|
|
5824
|
+
function itemContainsArabic(item) {
|
|
5825
|
+
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
5826
|
+
return isArabic(item.value);
|
|
5827
|
+
}
|
|
5828
|
+
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
5829
|
+
return isArabic(item.value);
|
|
5830
|
+
}
|
|
5831
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
5832
|
+
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
5833
|
+
}
|
|
5834
|
+
return false;
|
|
5835
|
+
}
|
|
5836
|
+
function itemContainsLatin(item) {
|
|
5837
|
+
if (item.type === JSON_TYPES.TEXT && "value" in item) {
|
|
5838
|
+
return hasLatinChars(item.value);
|
|
5839
|
+
}
|
|
5840
|
+
if (item.type === JSON_TYPES.HIGHLIGHTED_TEXT && "value" in item) {
|
|
5841
|
+
return hasLatinChars(item.value);
|
|
5842
|
+
}
|
|
5843
|
+
if (item.type === JSON_TYPES.GROUP && "items" in item) {
|
|
5844
|
+
return item.items.some((subItem) => itemContainsLatin(subItem));
|
|
5845
|
+
}
|
|
5846
|
+
return false;
|
|
5847
|
+
}
|
|
5848
|
+
function isTextItem(item) {
|
|
5849
|
+
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;
|
|
5850
|
+
}
|
|
5851
|
+
function groupLineItems(items) {
|
|
5852
|
+
const groups = [];
|
|
5853
|
+
let currentTextGroup = [];
|
|
5854
|
+
for (const item of items) {
|
|
5855
|
+
if (isTextItem(item)) {
|
|
5856
|
+
currentTextGroup.push(item);
|
|
5857
|
+
} else {
|
|
5858
|
+
if (currentTextGroup.length > 0) {
|
|
5859
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
5860
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
5861
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
5862
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
5863
|
+
currentTextGroup = [];
|
|
5864
|
+
}
|
|
5865
|
+
groups.push({ items: [item], isTextGroup: false, hasArabic: false, hasLatin: false, shouldApplyRTL: false });
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
if (currentTextGroup.length > 0) {
|
|
5869
|
+
const hasArabic = currentTextGroup.some(itemContainsArabic);
|
|
5870
|
+
const hasLatin = currentTextGroup.some(itemContainsLatin);
|
|
5871
|
+
const shouldApplyRTL = hasArabic && !hasLatin;
|
|
5872
|
+
groups.push({ items: currentTextGroup, isTextGroup: true, hasArabic, hasLatin, shouldApplyRTL });
|
|
5873
|
+
}
|
|
5874
|
+
return groups;
|
|
5875
|
+
}
|
|
5730
5876
|
const LayoutRenderer = ({
|
|
5731
5877
|
item,
|
|
5732
5878
|
widgets,
|
|
5733
5879
|
values,
|
|
5734
5880
|
droppedInputs,
|
|
5735
|
-
onChange
|
|
5881
|
+
onChange,
|
|
5882
|
+
onInputFocus
|
|
5736
5883
|
}) => {
|
|
5737
5884
|
switch (item.type) {
|
|
5738
|
-
case
|
|
5739
|
-
const hasArabic = isArabic
|
|
5885
|
+
case JSON_TYPES.QUESTION_TEXT: {
|
|
5886
|
+
const hasArabic = isArabic(item.value);
|
|
5740
5887
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `question-text ${hasArabic ? "question-text-arabic" : ""}`, children: item.value });
|
|
5741
5888
|
}
|
|
5742
|
-
case
|
|
5743
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-item ${isArabic
|
|
5744
|
-
case
|
|
5889
|
+
case JSON_TYPES.TEXT:
|
|
5890
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-item ${isArabic(item.value) ? "text-item-arabic" : "text-item-not-arabic"}`, children: item.value });
|
|
5891
|
+
case JSON_TYPES.HIGHLIGHTED_TEXT: {
|
|
5892
|
+
const hasArabic = isArabic(item.value);
|
|
5893
|
+
const dirAttr = !hasArabic ? "ltr" : void 0;
|
|
5745
5894
|
if (item.highlight) {
|
|
5746
5895
|
const [start, end] = item.highlight;
|
|
5747
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "
|
|
5896
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "highlighted-text", dir: dirAttr, children: [
|
|
5748
5897
|
item.value.slice(0, start),
|
|
5749
5898
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "highlight", children: item.value.slice(start, end) }),
|
|
5750
5899
|
item.value.slice(end)
|
|
5751
5900
|
] });
|
|
5752
5901
|
}
|
|
5753
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "
|
|
5754
|
-
|
|
5902
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "highlighted-text", dir: dirAttr, children: item.value });
|
|
5903
|
+
}
|
|
5904
|
+
case JSON_TYPES.WIDGET_REF: {
|
|
5755
5905
|
const widget = widgets[item.widgetId];
|
|
5756
5906
|
if (!widget) return null;
|
|
5757
5907
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -5761,15 +5911,18 @@ const LayoutRenderer = ({
|
|
|
5761
5911
|
widgetId: item.widgetId,
|
|
5762
5912
|
value: values[item.widgetId] || "",
|
|
5763
5913
|
isDropped: droppedInputs.has(item.widgetId),
|
|
5764
|
-
onChange
|
|
5914
|
+
onChange,
|
|
5915
|
+
onInputFocus
|
|
5765
5916
|
}
|
|
5766
5917
|
);
|
|
5767
5918
|
}
|
|
5768
|
-
case
|
|
5919
|
+
case JSON_TYPES.SPACE:
|
|
5920
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "space", children: " " });
|
|
5921
|
+
case JSON_TYPES.LINEBREAK:
|
|
5769
5922
|
return null;
|
|
5770
|
-
case
|
|
5923
|
+
case JSON_TYPES.OPERATOR:
|
|
5771
5924
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "operator", children: item.operator });
|
|
5772
|
-
case
|
|
5925
|
+
case JSON_TYPES.GROUP:
|
|
5773
5926
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "group", children: item.items.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
5774
5927
|
LayoutRenderer,
|
|
5775
5928
|
{
|
|
@@ -5777,7 +5930,8 @@ const LayoutRenderer = ({
|
|
|
5777
5930
|
widgets,
|
|
5778
5931
|
values,
|
|
5779
5932
|
droppedInputs,
|
|
5780
|
-
onChange
|
|
5933
|
+
onChange,
|
|
5934
|
+
onInputFocus
|
|
5781
5935
|
},
|
|
5782
5936
|
subItem.id
|
|
5783
5937
|
)) });
|
|
@@ -5794,21 +5948,6 @@ const DEFAULT_RULER_TOOL = {
|
|
|
5794
5948
|
width: 300,
|
|
5795
5949
|
allowRotate: true
|
|
5796
5950
|
};
|
|
5797
|
-
function isArabic(text) {
|
|
5798
|
-
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
5799
|
-
}
|
|
5800
|
-
function itemContainsArabic(item) {
|
|
5801
|
-
if (item.type === "text" && "value" in item) {
|
|
5802
|
-
return isArabic(item.value);
|
|
5803
|
-
}
|
|
5804
|
-
if (item.type === "number-text" && "value" in item) {
|
|
5805
|
-
return isArabic(item.value);
|
|
5806
|
-
}
|
|
5807
|
-
if (item.type === "group" && "items" in item) {
|
|
5808
|
-
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
5809
|
-
}
|
|
5810
|
-
return false;
|
|
5811
|
-
}
|
|
5812
5951
|
const ExerciseRenderer = React.forwardRef(({
|
|
5813
5952
|
json,
|
|
5814
5953
|
onSubmit,
|
|
@@ -5818,6 +5957,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
5818
5957
|
}, ref) => {
|
|
5819
5958
|
const [values, setValues] = React.useState({});
|
|
5820
5959
|
const [droppedInputs, setDroppedInputs] = React.useState(/* @__PURE__ */ new Set());
|
|
5960
|
+
const [focusedInputId, setFocusedInputId] = React.useState(null);
|
|
5821
5961
|
React.useImperativeHandle(ref, () => ({
|
|
5822
5962
|
submit: () => {
|
|
5823
5963
|
const result = validate(json, values);
|
|
@@ -5828,6 +5968,7 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
5828
5968
|
reset: () => {
|
|
5829
5969
|
setValues({});
|
|
5830
5970
|
setDroppedInputs(/* @__PURE__ */ new Set());
|
|
5971
|
+
setFocusedInputId(null);
|
|
5831
5972
|
},
|
|
5832
5973
|
focusFirstInput: () => {
|
|
5833
5974
|
const container = document.querySelector(".math-exercise-renderer");
|
|
@@ -5858,10 +5999,20 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
5858
5999
|
const groups = [];
|
|
5859
6000
|
let currentGroup = [];
|
|
5860
6001
|
for (const item of json.layout) {
|
|
5861
|
-
if (item.type ===
|
|
6002
|
+
if (item.type === JSON_TYPES.QUESTION_TEXT) {
|
|
6003
|
+
groups.push({
|
|
6004
|
+
items: [item],
|
|
6005
|
+
hasArabic: itemContainsArabic(item),
|
|
6006
|
+
subGroups: []
|
|
6007
|
+
// Question text doesn't need sub-grouping
|
|
6008
|
+
});
|
|
6009
|
+
continue;
|
|
6010
|
+
}
|
|
6011
|
+
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
5862
6012
|
if (currentGroup.length > 0) {
|
|
5863
6013
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
5864
|
-
|
|
6014
|
+
const subGroups = groupLineItems(currentGroup);
|
|
6015
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
5865
6016
|
currentGroup = [];
|
|
5866
6017
|
}
|
|
5867
6018
|
} else {
|
|
@@ -5870,7 +6021,8 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
5870
6021
|
}
|
|
5871
6022
|
if (currentGroup.length > 0) {
|
|
5872
6023
|
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
5873
|
-
|
|
6024
|
+
const subGroups = groupLineItems(currentGroup);
|
|
6025
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
5874
6026
|
}
|
|
5875
6027
|
return groups;
|
|
5876
6028
|
}, [json.layout]);
|
|
@@ -5891,17 +6043,52 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
5891
6043
|
"div",
|
|
5892
6044
|
{
|
|
5893
6045
|
className: `exercise-line ${group.hasArabic ? "exercise-line-arabic" : ""}`,
|
|
5894
|
-
children: group.
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
6046
|
+
children: group.subGroups.length > 0 ? (
|
|
6047
|
+
// Render sub-groups for regular lines
|
|
6048
|
+
group.subGroups.map((subGroup, subIdx) => subGroup.isTextGroup ? (
|
|
6049
|
+
// Text group - only apply RTL for pure Arabic (no mixed languages)
|
|
6050
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-group", dir: subGroup.shouldApplyRTL ? "rtl" : void 0, children: subGroup.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6051
|
+
LayoutRenderer,
|
|
6052
|
+
{
|
|
6053
|
+
item,
|
|
6054
|
+
widgets: json.widgets,
|
|
6055
|
+
values,
|
|
6056
|
+
droppedInputs,
|
|
6057
|
+
onChange: handleChange,
|
|
6058
|
+
onInputFocus: setFocusedInputId
|
|
6059
|
+
},
|
|
6060
|
+
item.id
|
|
6061
|
+
)) }, subIdx)
|
|
6062
|
+
) : (
|
|
6063
|
+
// Widget group - no dir wrapper, render directly
|
|
6064
|
+
subGroup.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6065
|
+
LayoutRenderer,
|
|
6066
|
+
{
|
|
6067
|
+
item,
|
|
6068
|
+
widgets: json.widgets,
|
|
6069
|
+
values,
|
|
6070
|
+
droppedInputs,
|
|
6071
|
+
onChange: handleChange,
|
|
6072
|
+
onInputFocus: setFocusedInputId
|
|
6073
|
+
},
|
|
6074
|
+
item.id
|
|
6075
|
+
))
|
|
6076
|
+
))
|
|
6077
|
+
) : (
|
|
6078
|
+
// For QUESTION_TEXT items (no sub-groups)
|
|
6079
|
+
group.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6080
|
+
LayoutRenderer,
|
|
6081
|
+
{
|
|
6082
|
+
item,
|
|
6083
|
+
widgets: json.widgets,
|
|
6084
|
+
values,
|
|
6085
|
+
droppedInputs,
|
|
6086
|
+
onChange: handleChange,
|
|
6087
|
+
onInputFocus: setFocusedInputId
|
|
6088
|
+
},
|
|
6089
|
+
item.id
|
|
6090
|
+
))
|
|
6091
|
+
)
|
|
5905
6092
|
},
|
|
5906
6093
|
groupIdx
|
|
5907
6094
|
)) }),
|
|
@@ -5910,9 +6097,13 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
5910
6097
|
{
|
|
5911
6098
|
widget: numericKeypadWidget,
|
|
5912
6099
|
widgetId: "_numeric_keypad",
|
|
5913
|
-
value: "",
|
|
5914
|
-
onChange: (
|
|
5915
|
-
|
|
6100
|
+
value: focusedInputId ? values[focusedInputId] || "" : "",
|
|
6101
|
+
onChange: () => {
|
|
6102
|
+
},
|
|
6103
|
+
onKeypadChange: (newValue) => {
|
|
6104
|
+
if (focusedInputId) {
|
|
6105
|
+
handleChange(focusedInputId, newValue, false);
|
|
6106
|
+
}
|
|
5916
6107
|
}
|
|
5917
6108
|
}
|
|
5918
6109
|
) })
|
|
@@ -5982,6 +6173,7 @@ function compileWithAST(dsl) {
|
|
|
5982
6173
|
const json = toJSON(ast);
|
|
5983
6174
|
return { ast, json };
|
|
5984
6175
|
}
|
|
6176
|
+
exports.AST_TYPES = AST_TYPES;
|
|
5985
6177
|
exports.DragOptionsJSONSchema = DragOptionsJSONSchema;
|
|
5986
6178
|
exports.DragOptionsToolSchema = DragOptionsToolSchema;
|
|
5987
6179
|
exports.EngineError = EngineError;
|
|
@@ -5992,16 +6184,17 @@ exports.ExerciseRenderer = ExerciseRenderer;
|
|
|
5992
6184
|
exports.ExpressionValidationSchema = ExpressionValidationSchema;
|
|
5993
6185
|
exports.FreeTextInlineSchema = FreeTextInlineSchema;
|
|
5994
6186
|
exports.GroupItemSchema = GroupItemSchema;
|
|
6187
|
+
exports.HighlightedTextInlineSchema = HighlightedTextInlineSchema;
|
|
6188
|
+
exports.HighlightedTextSchema = HighlightedTextSchema;
|
|
5995
6189
|
exports.InlineInputJSONSchema = InlineInputJSONSchema;
|
|
5996
6190
|
exports.InlineInputWidgetSchema = InlineInputWidgetSchema;
|
|
5997
6191
|
exports.InlineNodeSchema = InlineNodeSchema;
|
|
6192
|
+
exports.JSON_TYPES = JSON_TYPES;
|
|
5998
6193
|
exports.LayoutItemSchema = LayoutItemSchema;
|
|
5999
6194
|
exports.LayoutRenderer = LayoutRenderer;
|
|
6000
6195
|
exports.LexerError = LexerError;
|
|
6001
6196
|
exports.LineBlockSchema = LineBlockSchema;
|
|
6002
6197
|
exports.LinebreakSchema = LinebreakSchema;
|
|
6003
|
-
exports.NumberTextInlineSchema = NumberTextInlineSchema;
|
|
6004
|
-
exports.NumberTextSchema = NumberTextSchema;
|
|
6005
6198
|
exports.NumericKeypadJSONSchema = NumericKeypadJSONSchema;
|
|
6006
6199
|
exports.NumericKeypadToolSchema = NumericKeypadToolSchema;
|
|
6007
6200
|
exports.OperatorItemSchema = OperatorItemSchema;
|
|
@@ -6026,8 +6219,8 @@ exports.createDragOptions = createDragOptions;
|
|
|
6026
6219
|
exports.createExerciseDocument = createExerciseDocument;
|
|
6027
6220
|
exports.createExpressionValidation = createExpressionValidation;
|
|
6028
6221
|
exports.createFreeText = createFreeText;
|
|
6222
|
+
exports.createHighlightedText = createHighlightedText;
|
|
6029
6223
|
exports.createLineBlock = createLineBlock;
|
|
6030
|
-
exports.createNumberText = createNumberText;
|
|
6031
6224
|
exports.createNumericInput = createNumericInput;
|
|
6032
6225
|
exports.createNumericKeypad = createNumericKeypad;
|
|
6033
6226
|
exports.createSimpleValidation = createSimpleValidation;
|
|
@@ -6044,12 +6237,12 @@ exports.isExerciseDocument = isExerciseDocument;
|
|
|
6044
6237
|
exports.isExpressionValidation = isExpressionValidation;
|
|
6045
6238
|
exports.isFreeText = isFreeText;
|
|
6046
6239
|
exports.isGroupItem = isGroupItem;
|
|
6240
|
+
exports.isHighlightedText = isHighlightedText;
|
|
6047
6241
|
exports.isInlineInput = isInlineInput;
|
|
6048
6242
|
exports.isInlineInputJSON = isInlineInputJSON;
|
|
6049
6243
|
exports.isInlineNode = isInlineNode;
|
|
6050
6244
|
exports.isLineBlock = isLineBlock;
|
|
6051
6245
|
exports.isLinebreak = isLinebreak;
|
|
6052
|
-
exports.isNumberText = isNumberText;
|
|
6053
6246
|
exports.isNumericKeypad = isNumericKeypad;
|
|
6054
6247
|
exports.isNumericKeypadJSON = isNumericKeypadJSON;
|
|
6055
6248
|
exports.isOperatorItem = isOperatorItem;
|
|
@@ -6057,7 +6250,7 @@ exports.isQuestionText = isQuestionText;
|
|
|
6057
6250
|
exports.isSimpleValidation = isSimpleValidation;
|
|
6058
6251
|
exports.isSpace = isSpace;
|
|
6059
6252
|
exports.isTextBlock = isTextBlock;
|
|
6060
|
-
exports.isTextItem = isTextItem;
|
|
6253
|
+
exports.isTextItem = isTextItem$1;
|
|
6061
6254
|
exports.isToolNode = isToolNode;
|
|
6062
6255
|
exports.isValidExpression = isValidExpression;
|
|
6063
6256
|
exports.isValidationNode = isValidationNode;
|