@mathscan/math-exercise-engine 1.0.1 → 1.0.3

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