@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/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.SUBRULE(this.parameterList);
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("FreeText"),
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("Space")
561
+ type: zod.z.literal(AST_TYPES.SPACE)
479
562
  });
480
- const NumberTextInlineSchema = zod.z.object({
481
- type: zod.z.literal("NumberText"),
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("InlineInput"),
570
+ type: zod.z.literal(AST_TYPES.INLINE_INPUT),
488
571
  name: zod.z.string(),
489
- inputType: zod.z.literal("string"),
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("InlineInput"),
577
+ type: zod.z.literal(AST_TYPES.INLINE_INPUT),
495
578
  name: zod.z.string(),
496
- inputType: zod.z.literal("numeric"),
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
- NumberTextInlineSchema,
587
+ HighlightedTextInlineSchema,
505
588
  InlineInputWidgetSchema
506
589
  ]);
507
590
  function createFreeText(value) {
508
591
  return FreeTextInlineSchema.parse({
509
- type: "FreeText",
592
+ type: AST_TYPES.FREE_TEXT,
510
593
  value
511
594
  });
512
595
  }
513
596
  function createSpace() {
514
597
  return SpaceInlineSchema.parse({
515
- type: "Space"
598
+ type: AST_TYPES.SPACE
516
599
  });
517
600
  }
518
- function createNumberText(value, highlight) {
519
- return NumberTextInlineSchema.parse({
520
- type: "NumberText",
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: "InlineInput",
610
+ type: AST_TYPES.INLINE_INPUT,
528
611
  name,
529
- inputType: "string",
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: "InlineInput",
619
+ type: AST_TYPES.INLINE_INPUT,
537
620
  name,
538
- inputType: "numeric",
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("TextBlock"),
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("LineBlock"),
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: "TextBlock",
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: "LineBlock",
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("DragOptions"),
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("NumericKeypad"),
587
- includeDecimal: zod.z.boolean().default(false),
588
- includeNegative: zod.z.boolean().default(false)
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: "DragOptions",
679
+ type: AST_TYPES.DRAG_OPTIONS,
597
680
  options
598
681
  });
599
682
  }
600
- function createNumericKeypad(includeDecimal = false, includeNegative = false) {
683
+ function createNumericKeypad(includeOperators, includeDecimal) {
601
684
  return NumericKeypadToolSchema.parse({
602
- type: "NumericKeypad",
603
- includeDecimal,
604
- includeNegative
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("SimpleValidation")
700
+ type: zod.z.literal(AST_TYPES.SIMPLE_VALIDATION)
618
701
  });
619
702
  const ExpressionValidationSchema = zod.z.object({
620
- type: zod.z.literal("ExpressionValidation"),
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: "SimpleValidation"
712
+ type: AST_TYPES.SIMPLE_VALIDATION
630
713
  });
631
714
  }
632
715
  function createExpressionValidation(expression) {
633
716
  return ExpressionValidationSchema.parse({
634
- type: "ExpressionValidation",
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("ExerciseDocument"),
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: "ExerciseDocument",
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 createNumberText(value, highlight);
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("text"),
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("question-text"),
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("widget-ref"),
994
+ type: zod.z.literal(JSON_TYPES.WIDGET_REF),
911
995
  widgetId: zod.z.string()
912
996
  });
913
- const NumberTextSchema = zod.z.object({
997
+ const HighlightedTextSchema = zod.z.object({
914
998
  id: zod.z.string(),
915
- type: zod.z.literal("number-text"),
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("linebreak")
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("operator"),
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
- NumberTextSchema,
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("group"),
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("inline-input"),
947
- inputType: zod.z.literal("string"),
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("inline-input"),
952
- inputType: zod.z.literal("numeric"),
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("drag-options"),
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("numeric-keypad"),
965
- includeDecimal: zod.z.boolean()
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(["simple", "expression"]),
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 === "question-text";
1090
+ return item.type === JSON_TYPES.QUESTION_TEXT;
1006
1091
  }
1007
- function isTextItem(item) {
1008
- return item.type === "text";
1092
+ function isTextItem$1(item) {
1093
+ return item.type === JSON_TYPES.TEXT;
1009
1094
  }
1010
1095
  function isWidgetRef(item) {
1011
- return item.type === "widget-ref";
1096
+ return item.type === JSON_TYPES.WIDGET_REF;
1012
1097
  }
1013
- function isNumberText(item) {
1014
- return item.type === "number-text";
1098
+ function isHighlightedText(item) {
1099
+ return item.type === JSON_TYPES.HIGHLIGHTED_TEXT;
1015
1100
  }
1016
1101
  function isLinebreak(item) {
1017
- return item.type === "linebreak";
1102
+ return item.type === JSON_TYPES.LINEBREAK;
1018
1103
  }
1019
1104
  function isGroupItem(item) {
1020
- return item.type === "group";
1105
+ return item.type === JSON_TYPES.GROUP;
1021
1106
  }
1022
1107
  function isOperatorItem(item) {
1023
- return item.type === "operator";
1108
+ return item.type === JSON_TYPES.OPERATOR;
1024
1109
  }
1025
1110
  function isInlineInputJSON(widget) {
1026
- return widget.type === "inline-input";
1111
+ return widget.type === JSON_TYPES.INLINE_INPUT;
1027
1112
  }
1028
1113
  function isDragOptionsJSON(widget) {
1029
- return widget.type === "drag-options";
1114
+ return widget.type === JSON_TYPES.DRAG_OPTIONS;
1030
1115
  }
1031
1116
  function isNumericKeypadJSON(widget) {
1032
- return widget.type === "numeric-keypad";
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("question-text"),
1051
- type: "question-text",
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 === "InlineInput") {
1064
- if (inline.inputType === "string") {
1148
+ if (inline.type === AST_TYPES.INLINE_INPUT) {
1149
+ if (inline.inputType === INPUT_TYPES.STRING) {
1065
1150
  widgets[inline.name] = {
1066
- type: "inline-input",
1067
- inputType: "string",
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: "inline-input",
1073
- inputType: "numeric",
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 === "DragOptions") {
1170
+ if (tool.type === AST_TYPES.DRAG_OPTIONS) {
1086
1171
  widgets["_drag_options"] = {
1087
- type: "drag-options",
1172
+ type: JSON_TYPES.DRAG_OPTIONS,
1088
1173
  options: tool.options
1089
1174
  };
1090
- } else if (tool.type === "NumericKeypad") {
1175
+ } else if (tool.type === AST_TYPES.NUMERIC_KEYPAD) {
1091
1176
  widgets["_numeric_keypad"] = {
1092
- type: "numeric-keypad",
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("linebreak"),
1114
- type: "linebreak"
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 "FreeText":
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("text"),
1123
- type: "text",
1217
+ id: idGenerator.generate(JSON_TYPES.TEXT),
1218
+ type: JSON_TYPES.TEXT,
1124
1219
  value: inline.value
1125
1220
  };
1126
- case "Space":
1221
+ case AST_TYPES.SPACE:
1127
1222
  return {
1128
- id: idGenerator.generate("space"),
1129
- type: "space"
1223
+ id: idGenerator.generate(JSON_TYPES.SPACE),
1224
+ type: JSON_TYPES.SPACE
1130
1225
  };
1131
- case "NumberText":
1226
+ case AST_TYPES.HIGHLIGHTED_TEXT:
1132
1227
  return {
1133
- id: idGenerator.generate("number-text"),
1134
- type: "number-text",
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 "InlineInput":
1233
+ case AST_TYPES.INLINE_INPUT:
1139
1234
  return {
1140
- id: idGenerator.generate("widget-ref"),
1141
- type: "widget-ref",
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 === "SimpleValidation") {
1244
+ if (validation.type === AST_TYPES.SIMPLE_VALIDATION) {
1150
1245
  return {
1151
- mode: "simple",
1246
+ mode: VALIDATION_MODES.SIMPLE,
1152
1247
  answers
1153
1248
  };
1154
- } else if (validation.type === "ExpressionValidation") {
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: "expression",
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 "simple":
1579
+ case VALIDATION_MODES.SIMPLE:
1485
1580
  return validateSimple(exercise, userInputs);
1486
- case "expression":
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 === "simple" && exercise.validation.answers) {
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 === "expression" && exercise.validation.variables) {
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 === "inline-input" && widget.inputType === "numeric") {
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", children: [
5645
- ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"].map((digit) => /* @__PURE__ */ jsxRuntime.jsx(
5646
- "button",
5647
- {
5648
- className: "keypad-btn",
5649
- onClick: () => handleDigit(digit),
5650
- children: digit
5651
- },
5652
- digit
5653
- )),
5654
- includeDecimal && /* @__PURE__ */ jsxRuntime.jsx(
5655
- "button",
5656
- {
5657
- className: "keypad-btn",
5658
- onClick: () => handleDigit("."),
5659
- children: "."
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: handleChange
5811
+ onChange: onKeypadChange || (() => {
5812
+ })
5722
5813
  }
5723
5814
  );
5724
5815
  }
5725
5816
  return null;
5726
5817
  };
5727
- function isArabic$1(text) {
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 "question-text": {
5739
- const hasArabic = isArabic$1(item.value);
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 "text":
5743
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-item ${isArabic$1(item.value) ? "text-item-arabic" : "text-item-not-arabic"}`, children: item.value });
5744
- case "number-text":
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: "number-text", children: [
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: "number-text", children: item.value });
5754
- case "widget-ref": {
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 "linebreak":
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 "operator":
5923
+ case JSON_TYPES.OPERATOR:
5771
5924
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "operator", children: item.operator });
5772
- case "group":
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 === "linebreak") {
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
- groups.push({ items: currentGroup, hasArabic });
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
- groups.push({ items: currentGroup, hasArabic });
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.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
5895
- LayoutRenderer,
5896
- {
5897
- item,
5898
- widgets: json.widgets,
5899
- values,
5900
- droppedInputs,
5901
- onChange: handleChange
5902
- },
5903
- item.id
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: (v, w, f) => {
5915
- console.log(v, w, f, values);
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;