@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.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/,
|
|
@@ -130,7 +211,7 @@ const Comma = createToken({
|
|
|
130
211
|
});
|
|
131
212
|
const FreeText = createToken({
|
|
132
213
|
name: "FreeText",
|
|
133
|
-
pattern: /(?:[^A-Z\n\r]|(?![A-Z_]+\[)[A-Z])+/,
|
|
214
|
+
pattern: /(?:[^A-Z\n\r \t+\-−×*÷/=<>?()\[\],]|(?![A-Z_]+\[)[A-Z])+/,
|
|
134
215
|
line_breaks: false
|
|
135
216
|
});
|
|
136
217
|
const allTokens = [
|
|
@@ -372,7 +453,9 @@ class MathExerciseParser extends CstParser {
|
|
|
372
453
|
this.numericKeypad = this.RULE("numericKeypad", () => {
|
|
373
454
|
this.CONSUME(NUMERIC_KEYPAD);
|
|
374
455
|
this.CONSUME(LBracket);
|
|
375
|
-
this.
|
|
456
|
+
this.OPTION(() => {
|
|
457
|
+
this.SUBRULE(this.parameterList);
|
|
458
|
+
});
|
|
376
459
|
this.CONSUME(RBracket);
|
|
377
460
|
this.MANY(() => {
|
|
378
461
|
this.OR([
|
|
@@ -469,29 +552,29 @@ class MathExerciseParser extends CstParser {
|
|
|
469
552
|
}
|
|
470
553
|
const parser = new MathExerciseParser();
|
|
471
554
|
const FreeTextInlineSchema = z.object({
|
|
472
|
-
type: z.literal(
|
|
555
|
+
type: z.literal(AST_TYPES.FREE_TEXT),
|
|
473
556
|
value: z.string()
|
|
474
557
|
});
|
|
475
558
|
const SpaceInlineSchema = z.object({
|
|
476
|
-
type: z.literal(
|
|
559
|
+
type: z.literal(AST_TYPES.SPACE)
|
|
477
560
|
});
|
|
478
|
-
const
|
|
479
|
-
type: z.literal(
|
|
561
|
+
const HighlightedTextInlineSchema = z.object({
|
|
562
|
+
type: z.literal(AST_TYPES.HIGHLIGHTED_TEXT),
|
|
480
563
|
value: z.string(),
|
|
481
564
|
highlight: z.tuple([z.number(), z.number()]).optional()
|
|
482
565
|
});
|
|
483
566
|
const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
|
|
484
567
|
z.object({
|
|
485
|
-
type: z.literal(
|
|
568
|
+
type: z.literal(AST_TYPES.INLINE_INPUT),
|
|
486
569
|
name: z.string(),
|
|
487
|
-
inputType: z.literal(
|
|
570
|
+
inputType: z.literal(INPUT_TYPES.STRING),
|
|
488
571
|
length: z.number().int().positive(),
|
|
489
572
|
answer: z.union([z.string(), z.number()]).optional()
|
|
490
573
|
}),
|
|
491
574
|
z.object({
|
|
492
|
-
type: z.literal(
|
|
575
|
+
type: z.literal(AST_TYPES.INLINE_INPUT),
|
|
493
576
|
name: z.string(),
|
|
494
|
-
inputType: z.literal(
|
|
577
|
+
inputType: z.literal(INPUT_TYPES.NUMERIC),
|
|
495
578
|
digits: z.number().int().positive(),
|
|
496
579
|
answer: z.union([z.string(), z.number()]).optional()
|
|
497
580
|
})
|
|
@@ -499,41 +582,41 @@ const InlineInputWidgetSchema = z.discriminatedUnion("inputType", [
|
|
|
499
582
|
const InlineNodeSchema = z.union([
|
|
500
583
|
FreeTextInlineSchema,
|
|
501
584
|
SpaceInlineSchema,
|
|
502
|
-
|
|
585
|
+
HighlightedTextInlineSchema,
|
|
503
586
|
InlineInputWidgetSchema
|
|
504
587
|
]);
|
|
505
588
|
function createFreeText(value) {
|
|
506
589
|
return FreeTextInlineSchema.parse({
|
|
507
|
-
type:
|
|
590
|
+
type: AST_TYPES.FREE_TEXT,
|
|
508
591
|
value
|
|
509
592
|
});
|
|
510
593
|
}
|
|
511
594
|
function createSpace() {
|
|
512
595
|
return SpaceInlineSchema.parse({
|
|
513
|
-
type:
|
|
596
|
+
type: AST_TYPES.SPACE
|
|
514
597
|
});
|
|
515
598
|
}
|
|
516
|
-
function
|
|
517
|
-
return
|
|
518
|
-
type:
|
|
599
|
+
function createHighlightedText(value, highlight) {
|
|
600
|
+
return HighlightedTextInlineSchema.parse({
|
|
601
|
+
type: AST_TYPES.HIGHLIGHTED_TEXT,
|
|
519
602
|
value,
|
|
520
603
|
highlight
|
|
521
604
|
});
|
|
522
605
|
}
|
|
523
606
|
function createStringInput(name, length, answer) {
|
|
524
607
|
return InlineInputWidgetSchema.parse({
|
|
525
|
-
type:
|
|
608
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
526
609
|
name,
|
|
527
|
-
inputType:
|
|
610
|
+
inputType: INPUT_TYPES.STRING,
|
|
528
611
|
length,
|
|
529
612
|
answer
|
|
530
613
|
});
|
|
531
614
|
}
|
|
532
615
|
function createNumericInput(name, digits, answer) {
|
|
533
616
|
return InlineInputWidgetSchema.parse({
|
|
534
|
-
type:
|
|
617
|
+
type: AST_TYPES.INLINE_INPUT,
|
|
535
618
|
name,
|
|
536
|
-
inputType:
|
|
619
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
537
620
|
digits,
|
|
538
621
|
answer
|
|
539
622
|
});
|
|
@@ -551,22 +634,22 @@ function isInlineNode(value) {
|
|
|
551
634
|
return InlineNodeSchema.safeParse(value).success;
|
|
552
635
|
}
|
|
553
636
|
const TextBlockSchema = z.object({
|
|
554
|
-
type: z.literal(
|
|
637
|
+
type: z.literal(AST_TYPES.TEXT_BLOCK),
|
|
555
638
|
value: z.string()
|
|
556
639
|
});
|
|
557
640
|
const LineBlockSchema = z.object({
|
|
558
|
-
type: z.literal(
|
|
641
|
+
type: z.literal(AST_TYPES.LINE_BLOCK),
|
|
559
642
|
inlines: z.array(InlineNodeSchema)
|
|
560
643
|
});
|
|
561
644
|
function createTextBlock(value) {
|
|
562
645
|
return TextBlockSchema.parse({
|
|
563
|
-
type:
|
|
646
|
+
type: AST_TYPES.TEXT_BLOCK,
|
|
564
647
|
value
|
|
565
648
|
});
|
|
566
649
|
}
|
|
567
650
|
function createLineBlock(inlines) {
|
|
568
651
|
return LineBlockSchema.parse({
|
|
569
|
-
type:
|
|
652
|
+
type: AST_TYPES.LINE_BLOCK,
|
|
570
653
|
inlines
|
|
571
654
|
});
|
|
572
655
|
}
|
|
@@ -577,13 +660,13 @@ function isLineBlock(value) {
|
|
|
577
660
|
return LineBlockSchema.safeParse(value).success;
|
|
578
661
|
}
|
|
579
662
|
const DragOptionsToolSchema = z.object({
|
|
580
|
-
type: z.literal(
|
|
663
|
+
type: z.literal(AST_TYPES.DRAG_OPTIONS),
|
|
581
664
|
options: z.array(z.string()).min(1)
|
|
582
665
|
});
|
|
583
666
|
const NumericKeypadToolSchema = z.object({
|
|
584
|
-
type: z.literal(
|
|
585
|
-
|
|
586
|
-
|
|
667
|
+
type: z.literal(AST_TYPES.NUMERIC_KEYPAD),
|
|
668
|
+
includeOperators: z.boolean().optional(),
|
|
669
|
+
includeDecimal: z.boolean().optional()
|
|
587
670
|
});
|
|
588
671
|
const ToolNodeSchema = z.discriminatedUnion("type", [
|
|
589
672
|
DragOptionsToolSchema,
|
|
@@ -591,15 +674,15 @@ const ToolNodeSchema = z.discriminatedUnion("type", [
|
|
|
591
674
|
]);
|
|
592
675
|
function createDragOptions(options) {
|
|
593
676
|
return DragOptionsToolSchema.parse({
|
|
594
|
-
type:
|
|
677
|
+
type: AST_TYPES.DRAG_OPTIONS,
|
|
595
678
|
options
|
|
596
679
|
});
|
|
597
680
|
}
|
|
598
|
-
function createNumericKeypad(
|
|
681
|
+
function createNumericKeypad(includeOperators, includeDecimal) {
|
|
599
682
|
return NumericKeypadToolSchema.parse({
|
|
600
|
-
type:
|
|
601
|
-
|
|
602
|
-
|
|
683
|
+
type: AST_TYPES.NUMERIC_KEYPAD,
|
|
684
|
+
includeOperators,
|
|
685
|
+
includeDecimal
|
|
603
686
|
});
|
|
604
687
|
}
|
|
605
688
|
function isDragOptions(value) {
|
|
@@ -612,10 +695,10 @@ function isToolNode(value) {
|
|
|
612
695
|
return ToolNodeSchema.safeParse(value).success;
|
|
613
696
|
}
|
|
614
697
|
const SimpleValidationSchema = z.object({
|
|
615
|
-
type: z.literal(
|
|
698
|
+
type: z.literal(AST_TYPES.SIMPLE_VALIDATION)
|
|
616
699
|
});
|
|
617
700
|
const ExpressionValidationSchema = z.object({
|
|
618
|
-
type: z.literal(
|
|
701
|
+
type: z.literal(AST_TYPES.EXPRESSION_VALIDATION),
|
|
619
702
|
expression: z.string()
|
|
620
703
|
});
|
|
621
704
|
const ValidationNodeSchema = z.discriminatedUnion("type", [
|
|
@@ -624,12 +707,12 @@ const ValidationNodeSchema = z.discriminatedUnion("type", [
|
|
|
624
707
|
]);
|
|
625
708
|
function createSimpleValidation() {
|
|
626
709
|
return SimpleValidationSchema.parse({
|
|
627
|
-
type:
|
|
710
|
+
type: AST_TYPES.SIMPLE_VALIDATION
|
|
628
711
|
});
|
|
629
712
|
}
|
|
630
713
|
function createExpressionValidation(expression) {
|
|
631
714
|
return ExpressionValidationSchema.parse({
|
|
632
|
-
type:
|
|
715
|
+
type: AST_TYPES.EXPRESSION_VALIDATION,
|
|
633
716
|
expression
|
|
634
717
|
});
|
|
635
718
|
}
|
|
@@ -643,7 +726,7 @@ function isValidationNode(value) {
|
|
|
643
726
|
return ValidationNodeSchema.safeParse(value).success;
|
|
644
727
|
}
|
|
645
728
|
const ExerciseDocumentSchema = z.object({
|
|
646
|
-
type: z.literal(
|
|
729
|
+
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
647
730
|
question: TextBlockSchema.optional(),
|
|
648
731
|
lines: z.array(LineBlockSchema),
|
|
649
732
|
tools: z.array(ToolNodeSchema).optional(),
|
|
@@ -651,7 +734,7 @@ const ExerciseDocumentSchema = z.object({
|
|
|
651
734
|
});
|
|
652
735
|
function createExerciseDocument(question, lines, validation, tools) {
|
|
653
736
|
return ExerciseDocumentSchema.parse({
|
|
654
|
-
type:
|
|
737
|
+
type: AST_TYPES.EXERCISE_DOCUMENT,
|
|
655
738
|
question,
|
|
656
739
|
lines,
|
|
657
740
|
tools,
|
|
@@ -771,7 +854,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
771
854
|
if (params.start !== void 0 && params.end !== void 0) {
|
|
772
855
|
highlight = [params.start, params.end];
|
|
773
856
|
}
|
|
774
|
-
return
|
|
857
|
+
return createHighlightedText(value, highlight);
|
|
775
858
|
}
|
|
776
859
|
inlineInput(ctx) {
|
|
777
860
|
const params = this.visit(ctx.parameterList);
|
|
@@ -801,9 +884,10 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
801
884
|
return createDragOptions(strings);
|
|
802
885
|
}
|
|
803
886
|
numericKeypad(ctx) {
|
|
804
|
-
const params = this.visit(ctx.parameterList);
|
|
887
|
+
const params = ctx.parameterList ? this.visit(ctx.parameterList) : {};
|
|
888
|
+
const includeOperators = params.includeOperators === true || params.includeOperators === "true";
|
|
805
889
|
const includeDecimal = params.includeDecimal === true || params.includeDecimal === "true";
|
|
806
|
-
return createNumericKeypad(includeDecimal);
|
|
890
|
+
return createNumericKeypad(includeOperators || void 0, includeDecimal || void 0);
|
|
807
891
|
}
|
|
808
892
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
809
893
|
// Validation
|
|
@@ -895,33 +979,33 @@ function toAST(cst) {
|
|
|
895
979
|
}
|
|
896
980
|
const TextItemSchema = z.object({
|
|
897
981
|
id: z.string(),
|
|
898
|
-
type: z.literal(
|
|
982
|
+
type: z.literal(JSON_TYPES.TEXT),
|
|
899
983
|
value: z.string()
|
|
900
984
|
});
|
|
901
985
|
const QuestionTextSchema = z.object({
|
|
902
986
|
id: z.string(),
|
|
903
|
-
type: z.literal(
|
|
987
|
+
type: z.literal(JSON_TYPES.QUESTION_TEXT),
|
|
904
988
|
value: z.string()
|
|
905
989
|
});
|
|
906
990
|
const WidgetRefSchema = z.object({
|
|
907
991
|
id: z.string(),
|
|
908
|
-
type: z.literal(
|
|
992
|
+
type: z.literal(JSON_TYPES.WIDGET_REF),
|
|
909
993
|
widgetId: z.string()
|
|
910
994
|
});
|
|
911
|
-
const
|
|
995
|
+
const HighlightedTextSchema = z.object({
|
|
912
996
|
id: z.string(),
|
|
913
|
-
type: z.literal(
|
|
997
|
+
type: z.literal(JSON_TYPES.HIGHLIGHTED_TEXT),
|
|
914
998
|
value: z.string(),
|
|
915
999
|
highlight: z.tuple([z.number(), z.number()]).optional()
|
|
916
1000
|
});
|
|
917
1001
|
const LinebreakSchema = z.object({
|
|
918
1002
|
id: z.string(),
|
|
919
|
-
type: z.literal(
|
|
1003
|
+
type: z.literal(JSON_TYPES.LINEBREAK)
|
|
920
1004
|
});
|
|
921
1005
|
const OperatorSchema = z.enum(["+", "-", "×", "÷", "=", "<", ">", "?"]);
|
|
922
1006
|
const OperatorItemSchema = z.object({
|
|
923
1007
|
id: z.string(),
|
|
924
|
-
type: z.literal(
|
|
1008
|
+
type: z.literal(JSON_TYPES.OPERATOR),
|
|
925
1009
|
operator: OperatorSchema
|
|
926
1010
|
});
|
|
927
1011
|
const LayoutItemSchema = z.lazy(
|
|
@@ -929,7 +1013,7 @@ const LayoutItemSchema = z.lazy(
|
|
|
929
1013
|
QuestionTextSchema,
|
|
930
1014
|
TextItemSchema,
|
|
931
1015
|
WidgetRefSchema,
|
|
932
|
-
|
|
1016
|
+
HighlightedTextSchema,
|
|
933
1017
|
LinebreakSchema,
|
|
934
1018
|
GroupItemSchema,
|
|
935
1019
|
OperatorItemSchema
|
|
@@ -937,17 +1021,17 @@ const LayoutItemSchema = z.lazy(
|
|
|
937
1021
|
);
|
|
938
1022
|
const GroupItemSchema = z.object({
|
|
939
1023
|
id: z.string(),
|
|
940
|
-
type: z.literal(
|
|
1024
|
+
type: z.literal(JSON_TYPES.GROUP),
|
|
941
1025
|
items: z.array(LayoutItemSchema)
|
|
942
1026
|
});
|
|
943
1027
|
const StringInputJSONSchema = z.object({
|
|
944
|
-
type: z.literal(
|
|
945
|
-
inputType: z.literal(
|
|
1028
|
+
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1029
|
+
inputType: z.literal(INPUT_TYPES.STRING),
|
|
946
1030
|
length: z.number().int().positive()
|
|
947
1031
|
});
|
|
948
1032
|
const NumericInputJSONSchema = z.object({
|
|
949
|
-
type: z.literal(
|
|
950
|
-
inputType: z.literal(
|
|
1033
|
+
type: z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1034
|
+
inputType: z.literal(INPUT_TYPES.NUMERIC),
|
|
951
1035
|
digits: z.number().int().positive()
|
|
952
1036
|
});
|
|
953
1037
|
const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
|
|
@@ -955,12 +1039,13 @@ const InlineInputJSONSchema = z.discriminatedUnion("inputType", [
|
|
|
955
1039
|
NumericInputJSONSchema
|
|
956
1040
|
]);
|
|
957
1041
|
const DragOptionsJSONSchema = z.object({
|
|
958
|
-
type: z.literal(
|
|
1042
|
+
type: z.literal(JSON_TYPES.DRAG_OPTIONS),
|
|
959
1043
|
options: z.array(z.string())
|
|
960
1044
|
});
|
|
961
1045
|
const NumericKeypadJSONSchema = z.object({
|
|
962
|
-
type: z.literal(
|
|
963
|
-
|
|
1046
|
+
type: z.literal(JSON_TYPES.NUMERIC_KEYPAD),
|
|
1047
|
+
includeOperators: z.boolean().optional(),
|
|
1048
|
+
includeDecimal: z.boolean().optional()
|
|
964
1049
|
});
|
|
965
1050
|
const WidgetJSONSchema = z.union([
|
|
966
1051
|
InlineInputJSONSchema,
|
|
@@ -968,7 +1053,7 @@ const WidgetJSONSchema = z.union([
|
|
|
968
1053
|
NumericKeypadJSONSchema
|
|
969
1054
|
]);
|
|
970
1055
|
const ValidationJSONSchema = z.object({
|
|
971
|
-
mode: z.enum([
|
|
1056
|
+
mode: z.enum([VALIDATION_MODES.SIMPLE, VALIDATION_MODES.EXPRESSION]),
|
|
972
1057
|
answers: z.record(z.union([z.string(), z.number()])).optional(),
|
|
973
1058
|
expression: z.string().optional(),
|
|
974
1059
|
variables: z.array(z.string()).optional()
|
|
@@ -1000,34 +1085,34 @@ function validateWidgetJSONSafe(data) {
|
|
|
1000
1085
|
return WidgetJSONSchema.safeParse(data);
|
|
1001
1086
|
}
|
|
1002
1087
|
function isQuestionText(item) {
|
|
1003
|
-
return item.type ===
|
|
1088
|
+
return item.type === JSON_TYPES.QUESTION_TEXT;
|
|
1004
1089
|
}
|
|
1005
|
-
function isTextItem(item) {
|
|
1006
|
-
return item.type ===
|
|
1090
|
+
function isTextItem$1(item) {
|
|
1091
|
+
return item.type === JSON_TYPES.TEXT;
|
|
1007
1092
|
}
|
|
1008
1093
|
function isWidgetRef(item) {
|
|
1009
|
-
return item.type ===
|
|
1094
|
+
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1010
1095
|
}
|
|
1011
|
-
function
|
|
1012
|
-
return item.type ===
|
|
1096
|
+
function isHighlightedText(item) {
|
|
1097
|
+
return item.type === JSON_TYPES.HIGHLIGHTED_TEXT;
|
|
1013
1098
|
}
|
|
1014
1099
|
function isLinebreak(item) {
|
|
1015
|
-
return item.type ===
|
|
1100
|
+
return item.type === JSON_TYPES.LINEBREAK;
|
|
1016
1101
|
}
|
|
1017
1102
|
function isGroupItem(item) {
|
|
1018
|
-
return item.type ===
|
|
1103
|
+
return item.type === JSON_TYPES.GROUP;
|
|
1019
1104
|
}
|
|
1020
1105
|
function isOperatorItem(item) {
|
|
1021
|
-
return item.type ===
|
|
1106
|
+
return item.type === JSON_TYPES.OPERATOR;
|
|
1022
1107
|
}
|
|
1023
1108
|
function isInlineInputJSON(widget) {
|
|
1024
|
-
return widget.type ===
|
|
1109
|
+
return widget.type === JSON_TYPES.INLINE_INPUT;
|
|
1025
1110
|
}
|
|
1026
1111
|
function isDragOptionsJSON(widget) {
|
|
1027
|
-
return widget.type ===
|
|
1112
|
+
return widget.type === JSON_TYPES.DRAG_OPTIONS;
|
|
1028
1113
|
}
|
|
1029
1114
|
function isNumericKeypadJSON(widget) {
|
|
1030
|
-
return widget.type ===
|
|
1115
|
+
return widget.type === JSON_TYPES.NUMERIC_KEYPAD;
|
|
1031
1116
|
}
|
|
1032
1117
|
class LayoutIdGenerator {
|
|
1033
1118
|
constructor() {
|
|
@@ -1045,8 +1130,8 @@ function toJSON(ast) {
|
|
|
1045
1130
|
const layout = [];
|
|
1046
1131
|
if (ast.question) {
|
|
1047
1132
|
layout.push({
|
|
1048
|
-
id: idGenerator.generate(
|
|
1049
|
-
type:
|
|
1133
|
+
id: idGenerator.generate(JSON_TYPES.QUESTION_TEXT),
|
|
1134
|
+
type: JSON_TYPES.QUESTION_TEXT,
|
|
1050
1135
|
value: ast.question.value
|
|
1051
1136
|
});
|
|
1052
1137
|
}
|
|
@@ -1058,17 +1143,17 @@ function toJSON(ast) {
|
|
|
1058
1143
|
const answers = {};
|
|
1059
1144
|
for (const line of ast.lines) {
|
|
1060
1145
|
for (const inline of line.inlines) {
|
|
1061
|
-
if (inline.type ===
|
|
1062
|
-
if (inline.inputType ===
|
|
1146
|
+
if (inline.type === AST_TYPES.INLINE_INPUT) {
|
|
1147
|
+
if (inline.inputType === INPUT_TYPES.STRING) {
|
|
1063
1148
|
widgets[inline.name] = {
|
|
1064
|
-
type:
|
|
1065
|
-
inputType:
|
|
1149
|
+
type: JSON_TYPES.INLINE_INPUT,
|
|
1150
|
+
inputType: INPUT_TYPES.STRING,
|
|
1066
1151
|
length: inline.length
|
|
1067
1152
|
};
|
|
1068
1153
|
} else {
|
|
1069
1154
|
widgets[inline.name] = {
|
|
1070
|
-
type:
|
|
1071
|
-
inputType:
|
|
1155
|
+
type: JSON_TYPES.INLINE_INPUT,
|
|
1156
|
+
inputType: INPUT_TYPES.NUMERIC,
|
|
1072
1157
|
digits: inline.digits
|
|
1073
1158
|
};
|
|
1074
1159
|
}
|
|
@@ -1080,14 +1165,15 @@ function toJSON(ast) {
|
|
|
1080
1165
|
}
|
|
1081
1166
|
if (ast.tools) {
|
|
1082
1167
|
for (const tool of ast.tools) {
|
|
1083
|
-
if (tool.type ===
|
|
1168
|
+
if (tool.type === AST_TYPES.DRAG_OPTIONS) {
|
|
1084
1169
|
widgets["_drag_options"] = {
|
|
1085
|
-
type:
|
|
1170
|
+
type: JSON_TYPES.DRAG_OPTIONS,
|
|
1086
1171
|
options: tool.options
|
|
1087
1172
|
};
|
|
1088
|
-
} else if (tool.type ===
|
|
1173
|
+
} else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
|
|
1089
1174
|
widgets["_numeric_keypad"] = {
|
|
1090
|
-
type:
|
|
1175
|
+
type: JSON_TYPES.NUMERIC_KEYPAD,
|
|
1176
|
+
includeOperators: tool.includeOperators,
|
|
1091
1177
|
includeDecimal: tool.includeDecimal
|
|
1092
1178
|
};
|
|
1093
1179
|
}
|
|
@@ -1108,35 +1194,44 @@ function transformLine(line, idGenerator) {
|
|
|
1108
1194
|
items.push(transformInline(inline, idGenerator));
|
|
1109
1195
|
}
|
|
1110
1196
|
items.push({
|
|
1111
|
-
id: idGenerator.generate(
|
|
1112
|
-
type:
|
|
1197
|
+
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
1198
|
+
type: JSON_TYPES.LINEBREAK
|
|
1113
1199
|
});
|
|
1114
1200
|
return items;
|
|
1115
1201
|
}
|
|
1116
1202
|
function transformInline(inline, idGenerator) {
|
|
1117
1203
|
switch (inline.type) {
|
|
1118
|
-
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
|
+
}
|
|
1119
1214
|
return {
|
|
1120
|
-
id: idGenerator.generate(
|
|
1121
|
-
type:
|
|
1215
|
+
id: idGenerator.generate(JSON_TYPES.TEXT),
|
|
1216
|
+
type: JSON_TYPES.TEXT,
|
|
1122
1217
|
value: inline.value
|
|
1123
1218
|
};
|
|
1124
|
-
case
|
|
1219
|
+
case AST_TYPES.SPACE:
|
|
1125
1220
|
return {
|
|
1126
|
-
id: idGenerator.generate(
|
|
1127
|
-
type:
|
|
1221
|
+
id: idGenerator.generate(JSON_TYPES.SPACE),
|
|
1222
|
+
type: JSON_TYPES.SPACE
|
|
1128
1223
|
};
|
|
1129
|
-
case
|
|
1224
|
+
case AST_TYPES.HIGHLIGHTED_TEXT:
|
|
1130
1225
|
return {
|
|
1131
|
-
id: idGenerator.generate(
|
|
1132
|
-
type:
|
|
1226
|
+
id: idGenerator.generate(JSON_TYPES.HIGHLIGHTED_TEXT),
|
|
1227
|
+
type: JSON_TYPES.HIGHLIGHTED_TEXT,
|
|
1133
1228
|
value: inline.value,
|
|
1134
1229
|
highlight: inline.highlight
|
|
1135
1230
|
};
|
|
1136
|
-
case
|
|
1231
|
+
case AST_TYPES.INLINE_INPUT:
|
|
1137
1232
|
return {
|
|
1138
|
-
id: idGenerator.generate(
|
|
1139
|
-
type:
|
|
1233
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
1234
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
1140
1235
|
widgetId: inline.name
|
|
1141
1236
|
};
|
|
1142
1237
|
default:
|
|
@@ -1144,16 +1239,16 @@ function transformInline(inline, idGenerator) {
|
|
|
1144
1239
|
}
|
|
1145
1240
|
}
|
|
1146
1241
|
function transformValidation(validation, answers, widgets) {
|
|
1147
|
-
if (validation.type ===
|
|
1242
|
+
if (validation.type === AST_TYPES.SIMPLE_VALIDATION) {
|
|
1148
1243
|
return {
|
|
1149
|
-
mode:
|
|
1244
|
+
mode: VALIDATION_MODES.SIMPLE,
|
|
1150
1245
|
answers
|
|
1151
1246
|
};
|
|
1152
|
-
} else if (validation.type ===
|
|
1247
|
+
} else if (validation.type === AST_TYPES.EXPRESSION_VALIDATION) {
|
|
1153
1248
|
const validVariableNames = Object.keys(widgets);
|
|
1154
1249
|
const variables = extractVariables$1(validation.expression, validVariableNames);
|
|
1155
1250
|
return {
|
|
1156
|
-
mode:
|
|
1251
|
+
mode: VALIDATION_MODES.EXPRESSION,
|
|
1157
1252
|
expression: validation.expression,
|
|
1158
1253
|
variables
|
|
1159
1254
|
};
|
|
@@ -1479,9 +1574,9 @@ function findValidCombination(expression, ranges) {
|
|
|
1479
1574
|
}
|
|
1480
1575
|
function validate(exercise, userInputs) {
|
|
1481
1576
|
switch (exercise.validation.mode) {
|
|
1482
|
-
case
|
|
1577
|
+
case VALIDATION_MODES.SIMPLE:
|
|
1483
1578
|
return validateSimple(exercise, userInputs);
|
|
1484
|
-
case
|
|
1579
|
+
case VALIDATION_MODES.EXPRESSION:
|
|
1485
1580
|
return validateExpression(exercise, userInputs);
|
|
1486
1581
|
default:
|
|
1487
1582
|
return {
|
|
@@ -1493,13 +1588,13 @@ function validate(exercise, userInputs) {
|
|
|
1493
1588
|
}
|
|
1494
1589
|
function getMissingInputs(exercise, userInputs) {
|
|
1495
1590
|
const missing = [];
|
|
1496
|
-
if (exercise.validation.mode ===
|
|
1591
|
+
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
1497
1592
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
1498
1593
|
if (!(widgetId in userInputs) || userInputs[widgetId] === "") {
|
|
1499
1594
|
missing.push(widgetId);
|
|
1500
1595
|
}
|
|
1501
1596
|
}
|
|
1502
|
-
} else if (exercise.validation.mode ===
|
|
1597
|
+
} else if (exercise.validation.mode === VALIDATION_MODES.EXPRESSION && exercise.validation.variables) {
|
|
1503
1598
|
for (const varName of exercise.validation.variables) {
|
|
1504
1599
|
if (!(varName in userInputs) || userInputs[varName] === "") {
|
|
1505
1600
|
missing.push(varName);
|
|
@@ -1515,7 +1610,7 @@ function validateInputFormats(exercise, userInputs) {
|
|
|
1515
1610
|
if (value === void 0 || value === "") {
|
|
1516
1611
|
continue;
|
|
1517
1612
|
}
|
|
1518
|
-
if (widget.type ===
|
|
1613
|
+
if (widget.type === JSON_TYPES.INLINE_INPUT && widget.inputType === INPUT_TYPES.NUMERIC) {
|
|
1519
1614
|
const numValue = typeof value === "number" ? value : parseFloat(String(value));
|
|
1520
1615
|
if (!Number.isFinite(numValue)) {
|
|
1521
1616
|
errors[widgetId] = "Must be a valid number";
|
|
@@ -5545,7 +5640,8 @@ const InlineInput = ({
|
|
|
5545
5640
|
inputType,
|
|
5546
5641
|
length,
|
|
5547
5642
|
digits,
|
|
5548
|
-
onChange
|
|
5643
|
+
onChange,
|
|
5644
|
+
onFocus
|
|
5549
5645
|
}) => {
|
|
5550
5646
|
const handleChange = useCallback((newValue, fromDrop = false) => {
|
|
5551
5647
|
onChange(widgetId, newValue, fromDrop);
|
|
@@ -5586,6 +5682,7 @@ const InlineInput = ({
|
|
|
5586
5682
|
className: "inline-input",
|
|
5587
5683
|
value,
|
|
5588
5684
|
onChange: (e) => handleChange(e.target.value, false),
|
|
5685
|
+
onFocus: () => onFocus?.(widgetId),
|
|
5589
5686
|
maxLength: expectedLength,
|
|
5590
5687
|
style: { width: dynamicWidth }
|
|
5591
5688
|
}
|
|
@@ -5630,6 +5727,7 @@ const DragOptions = ({
|
|
|
5630
5727
|
};
|
|
5631
5728
|
const NumericKeypad = ({
|
|
5632
5729
|
value,
|
|
5730
|
+
includeOperators = false,
|
|
5633
5731
|
includeDecimal = false,
|
|
5634
5732
|
onChange
|
|
5635
5733
|
}) => {
|
|
@@ -5639,32 +5737,23 @@ const NumericKeypad = ({
|
|
|
5639
5737
|
const handleBackspace = () => {
|
|
5640
5738
|
onChange(value.slice(0, -1));
|
|
5641
5739
|
};
|
|
5642
|
-
return /* @__PURE__ */ jsxs("div", { className: "numeric-keypad"
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
)),
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
),
|
|
5660
|
-
/* @__PURE__ */ jsx(
|
|
5661
|
-
"button",
|
|
5662
|
-
{
|
|
5663
|
-
className: "keypad-btn",
|
|
5664
|
-
onClick: handleBackspace,
|
|
5665
|
-
children: "⌫"
|
|
5666
|
-
}
|
|
5667
|
-
)
|
|
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: "÷" })
|
|
5668
5757
|
] });
|
|
5669
5758
|
};
|
|
5670
5759
|
function isInlineInputWidget(widget) {
|
|
@@ -5681,11 +5770,10 @@ const WidgetRenderer = ({
|
|
|
5681
5770
|
widgetId,
|
|
5682
5771
|
value,
|
|
5683
5772
|
isDropped = false,
|
|
5684
|
-
onChange
|
|
5773
|
+
onChange,
|
|
5774
|
+
onInputFocus,
|
|
5775
|
+
onKeypadChange
|
|
5685
5776
|
}) => {
|
|
5686
|
-
const handleChange = useCallback((newValue, fromDrop = false) => {
|
|
5687
|
-
onChange(widgetId, newValue, fromDrop);
|
|
5688
|
-
}, [onChange, widgetId]);
|
|
5689
5777
|
if (isInlineInputWidget(widget)) {
|
|
5690
5778
|
return /* @__PURE__ */ jsx(
|
|
5691
5779
|
InlineInput,
|
|
@@ -5696,7 +5784,8 @@ const WidgetRenderer = ({
|
|
|
5696
5784
|
inputType: widget.inputType,
|
|
5697
5785
|
length: widget.inputType === "string" ? widget.length : void 0,
|
|
5698
5786
|
digits: widget.inputType === "numeric" ? widget.digits : void 0,
|
|
5699
|
-
onChange
|
|
5787
|
+
onChange,
|
|
5788
|
+
onFocus: onInputFocus
|
|
5700
5789
|
}
|
|
5701
5790
|
);
|
|
5702
5791
|
}
|
|
@@ -5715,41 +5804,102 @@ const WidgetRenderer = ({
|
|
|
5715
5804
|
NumericKeypad,
|
|
5716
5805
|
{
|
|
5717
5806
|
value,
|
|
5807
|
+
includeOperators: widget.includeOperators,
|
|
5718
5808
|
includeDecimal: widget.includeDecimal,
|
|
5719
|
-
onChange:
|
|
5809
|
+
onChange: onKeypadChange || (() => {
|
|
5810
|
+
})
|
|
5720
5811
|
}
|
|
5721
5812
|
);
|
|
5722
5813
|
}
|
|
5723
5814
|
return null;
|
|
5724
5815
|
};
|
|
5725
|
-
function isArabic
|
|
5816
|
+
function isArabic(text) {
|
|
5726
5817
|
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
5727
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
|
+
}
|
|
5728
5874
|
const LayoutRenderer = ({
|
|
5729
5875
|
item,
|
|
5730
5876
|
widgets,
|
|
5731
5877
|
values,
|
|
5732
5878
|
droppedInputs,
|
|
5733
|
-
onChange
|
|
5879
|
+
onChange,
|
|
5880
|
+
onInputFocus
|
|
5734
5881
|
}) => {
|
|
5735
5882
|
switch (item.type) {
|
|
5736
|
-
case
|
|
5737
|
-
const hasArabic = isArabic
|
|
5883
|
+
case JSON_TYPES.QUESTION_TEXT: {
|
|
5884
|
+
const hasArabic = isArabic(item.value);
|
|
5738
5885
|
return /* @__PURE__ */ jsx("span", { className: `question-text ${hasArabic ? "question-text-arabic" : ""}`, children: item.value });
|
|
5739
5886
|
}
|
|
5740
|
-
case
|
|
5741
|
-
return /* @__PURE__ */ jsx("span", { className: `text-item ${isArabic
|
|
5742
|
-
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;
|
|
5743
5892
|
if (item.highlight) {
|
|
5744
5893
|
const [start, end] = item.highlight;
|
|
5745
|
-
return /* @__PURE__ */ jsxs("span", { className: "
|
|
5894
|
+
return /* @__PURE__ */ jsxs("span", { className: "highlighted-text", dir: dirAttr, children: [
|
|
5746
5895
|
item.value.slice(0, start),
|
|
5747
5896
|
/* @__PURE__ */ jsx("span", { className: "highlight", children: item.value.slice(start, end) }),
|
|
5748
5897
|
item.value.slice(end)
|
|
5749
5898
|
] });
|
|
5750
5899
|
}
|
|
5751
|
-
return /* @__PURE__ */ jsx("span", { className: "
|
|
5752
|
-
|
|
5900
|
+
return /* @__PURE__ */ jsx("span", { className: "highlighted-text", dir: dirAttr, children: item.value });
|
|
5901
|
+
}
|
|
5902
|
+
case JSON_TYPES.WIDGET_REF: {
|
|
5753
5903
|
const widget = widgets[item.widgetId];
|
|
5754
5904
|
if (!widget) return null;
|
|
5755
5905
|
return /* @__PURE__ */ jsx(
|
|
@@ -5759,15 +5909,18 @@ const LayoutRenderer = ({
|
|
|
5759
5909
|
widgetId: item.widgetId,
|
|
5760
5910
|
value: values[item.widgetId] || "",
|
|
5761
5911
|
isDropped: droppedInputs.has(item.widgetId),
|
|
5762
|
-
onChange
|
|
5912
|
+
onChange,
|
|
5913
|
+
onInputFocus
|
|
5763
5914
|
}
|
|
5764
5915
|
);
|
|
5765
5916
|
}
|
|
5766
|
-
case
|
|
5917
|
+
case JSON_TYPES.SPACE:
|
|
5918
|
+
return /* @__PURE__ */ jsx("span", { className: "space", children: " " });
|
|
5919
|
+
case JSON_TYPES.LINEBREAK:
|
|
5767
5920
|
return null;
|
|
5768
|
-
case
|
|
5921
|
+
case JSON_TYPES.OPERATOR:
|
|
5769
5922
|
return /* @__PURE__ */ jsx("span", { className: "operator", children: item.operator });
|
|
5770
|
-
case
|
|
5923
|
+
case JSON_TYPES.GROUP:
|
|
5771
5924
|
return /* @__PURE__ */ jsx("span", { className: "group", children: item.items.map((subItem) => /* @__PURE__ */ jsx(
|
|
5772
5925
|
LayoutRenderer,
|
|
5773
5926
|
{
|
|
@@ -5775,7 +5928,8 @@ const LayoutRenderer = ({
|
|
|
5775
5928
|
widgets,
|
|
5776
5929
|
values,
|
|
5777
5930
|
droppedInputs,
|
|
5778
|
-
onChange
|
|
5931
|
+
onChange,
|
|
5932
|
+
onInputFocus
|
|
5779
5933
|
},
|
|
5780
5934
|
subItem.id
|
|
5781
5935
|
)) });
|
|
@@ -5792,21 +5946,6 @@ const DEFAULT_RULER_TOOL = {
|
|
|
5792
5946
|
width: 300,
|
|
5793
5947
|
allowRotate: true
|
|
5794
5948
|
};
|
|
5795
|
-
function isArabic(text) {
|
|
5796
|
-
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
5797
|
-
}
|
|
5798
|
-
function itemContainsArabic(item) {
|
|
5799
|
-
if (item.type === "text" && "value" in item) {
|
|
5800
|
-
return isArabic(item.value);
|
|
5801
|
-
}
|
|
5802
|
-
if (item.type === "number-text" && "value" in item) {
|
|
5803
|
-
return isArabic(item.value);
|
|
5804
|
-
}
|
|
5805
|
-
if (item.type === "group" && "items" in item) {
|
|
5806
|
-
return item.items.some((subItem) => itemContainsArabic(subItem));
|
|
5807
|
-
}
|
|
5808
|
-
return false;
|
|
5809
|
-
}
|
|
5810
5949
|
const ExerciseRenderer = forwardRef(({
|
|
5811
5950
|
json,
|
|
5812
5951
|
onSubmit,
|
|
@@ -5816,6 +5955,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5816
5955
|
}, ref) => {
|
|
5817
5956
|
const [values, setValues] = useState({});
|
|
5818
5957
|
const [droppedInputs, setDroppedInputs] = useState(/* @__PURE__ */ new Set());
|
|
5958
|
+
const [focusedInputId, setFocusedInputId] = useState(null);
|
|
5819
5959
|
useImperativeHandle(ref, () => ({
|
|
5820
5960
|
submit: () => {
|
|
5821
5961
|
const result = validate(json, values);
|
|
@@ -5826,6 +5966,7 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5826
5966
|
reset: () => {
|
|
5827
5967
|
setValues({});
|
|
5828
5968
|
setDroppedInputs(/* @__PURE__ */ new Set());
|
|
5969
|
+
setFocusedInputId(null);
|
|
5829
5970
|
},
|
|
5830
5971
|
focusFirstInput: () => {
|
|
5831
5972
|
const container = document.querySelector(".math-exercise-renderer");
|
|
@@ -5856,10 +5997,20 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5856
5997
|
const groups = [];
|
|
5857
5998
|
let currentGroup = [];
|
|
5858
5999
|
for (const item of json.layout) {
|
|
5859
|
-
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) {
|
|
5860
6010
|
if (currentGroup.length > 0) {
|
|
5861
6011
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
5862
|
-
|
|
6012
|
+
const subGroups = groupLineItems(currentGroup);
|
|
6013
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
5863
6014
|
currentGroup = [];
|
|
5864
6015
|
}
|
|
5865
6016
|
} else {
|
|
@@ -5868,7 +6019,8 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5868
6019
|
}
|
|
5869
6020
|
if (currentGroup.length > 0) {
|
|
5870
6021
|
const hasArabic = currentGroup.some((item) => itemContainsArabic(item));
|
|
5871
|
-
|
|
6022
|
+
const subGroups = groupLineItems(currentGroup);
|
|
6023
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
5872
6024
|
}
|
|
5873
6025
|
return groups;
|
|
5874
6026
|
}, [json.layout]);
|
|
@@ -5889,17 +6041,52 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5889
6041
|
"div",
|
|
5890
6042
|
{
|
|
5891
6043
|
className: `exercise-line ${group.hasArabic ? "exercise-line-arabic" : ""}`,
|
|
5892
|
-
children: group.
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
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
|
+
)
|
|
5903
6090
|
},
|
|
5904
6091
|
groupIdx
|
|
5905
6092
|
)) }),
|
|
@@ -5908,9 +6095,13 @@ const ExerciseRenderer = forwardRef(({
|
|
|
5908
6095
|
{
|
|
5909
6096
|
widget: numericKeypadWidget,
|
|
5910
6097
|
widgetId: "_numeric_keypad",
|
|
5911
|
-
value: "",
|
|
5912
|
-
onChange: (
|
|
5913
|
-
|
|
6098
|
+
value: focusedInputId ? values[focusedInputId] || "" : "",
|
|
6099
|
+
onChange: () => {
|
|
6100
|
+
},
|
|
6101
|
+
onKeypadChange: (newValue) => {
|
|
6102
|
+
if (focusedInputId) {
|
|
6103
|
+
handleChange(focusedInputId, newValue, false);
|
|
6104
|
+
}
|
|
5914
6105
|
}
|
|
5915
6106
|
}
|
|
5916
6107
|
) })
|
|
@@ -5981,6 +6172,7 @@ function compileWithAST(dsl) {
|
|
|
5981
6172
|
return { ast, json };
|
|
5982
6173
|
}
|
|
5983
6174
|
export {
|
|
6175
|
+
AST_TYPES,
|
|
5984
6176
|
DragOptionsJSONSchema,
|
|
5985
6177
|
DragOptionsToolSchema,
|
|
5986
6178
|
EngineError,
|
|
@@ -5991,16 +6183,17 @@ export {
|
|
|
5991
6183
|
ExpressionValidationSchema,
|
|
5992
6184
|
FreeTextInlineSchema,
|
|
5993
6185
|
GroupItemSchema,
|
|
6186
|
+
HighlightedTextInlineSchema,
|
|
6187
|
+
HighlightedTextSchema,
|
|
5994
6188
|
InlineInputJSONSchema,
|
|
5995
6189
|
InlineInputWidgetSchema,
|
|
5996
6190
|
InlineNodeSchema,
|
|
6191
|
+
JSON_TYPES,
|
|
5997
6192
|
LayoutItemSchema,
|
|
5998
6193
|
LayoutRenderer,
|
|
5999
6194
|
LexerError,
|
|
6000
6195
|
LineBlockSchema,
|
|
6001
6196
|
LinebreakSchema,
|
|
6002
|
-
NumberTextInlineSchema,
|
|
6003
|
-
NumberTextSchema,
|
|
6004
6197
|
NumericKeypadJSONSchema,
|
|
6005
6198
|
NumericKeypadToolSchema,
|
|
6006
6199
|
OperatorItemSchema,
|
|
@@ -6025,8 +6218,8 @@ export {
|
|
|
6025
6218
|
createExerciseDocument,
|
|
6026
6219
|
createExpressionValidation,
|
|
6027
6220
|
createFreeText,
|
|
6221
|
+
createHighlightedText,
|
|
6028
6222
|
createLineBlock,
|
|
6029
|
-
createNumberText,
|
|
6030
6223
|
createNumericInput,
|
|
6031
6224
|
createNumericKeypad,
|
|
6032
6225
|
createSimpleValidation,
|
|
@@ -6043,12 +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
6245
|
isNumericKeypad,
|
|
6053
6246
|
isNumericKeypadJSON,
|
|
6054
6247
|
isOperatorItem,
|
|
@@ -6056,7 +6249,7 @@ export {
|
|
|
6056
6249
|
isSimpleValidation,
|
|
6057
6250
|
isSpace,
|
|
6058
6251
|
isTextBlock,
|
|
6059
|
-
isTextItem,
|
|
6252
|
+
isTextItem$1 as isTextItem,
|
|
6060
6253
|
isToolNode,
|
|
6061
6254
|
isValidExpression,
|
|
6062
6255
|
isValidationNode,
|